详解ES6 Symbol 的用途
Symbol唯一的用途就是标识对象属性,表明对象支持的功能。相比于字符属性名,Symbol的区别在于唯一,可避免名字冲突。这样Symbol就给出了唯一标识类型信息的一种方式,从这个角度看有点类似C++的Traits。
解决了什么问题
在JavaScript中要判断一个对象支持的功能,常常需要做一些DuckTest。比如经常需要判断一个对象是否可以按照数组的方式去迭代,这类对象称为Array-like。lodash中是这样判断的:
functionisArrayLike(value){ returnvalue!=null&&isLength(value.length)&&!isFunction(value); }
在ES6中提出一个@@iterator方法,所有支持迭代的对象(比如Array、Map、Set)都要实现。@@iterator方法的属性键为Symbol.iterator而非字符串。这样只要对象定义有Symbol.iterator属性就可以用for...of进行迭代。比如:
if(Symbol.iteratorinarr){ for(letnofarr)console.log(n) }
其他用例
上述例子中Symbol标识了这个对象是可迭代的(Iterables),是一个典型的Symbol用例。详情可以参考ES6迭代器一文。此外利用Symbol还可以做很多其他事情,例如:
常量枚举
JavaScript没有枚举类型,常量概念也通常用字符串或数字表示。例如:
constCOLOR_GREEN=1 constCOLOR_RED=2 functionisSafe(trafficLight){ if(trafficLight===COLOR_RED)returnfalse if(trafficLight===COLOR_GREEN)returntrue thrownewError(`invalidtrafficLight:${trafficLight}`) }
- 我们需要认真地排列这些常量的值。如果不小心有两个值重复会很难调试,就像#definefalsetrue引起的问题一样。
- 取值可能重复。如果有另一处定义了BUSY=1并不小心把BUSY传入,干脆isSafe(1),理想的枚举概念应该抛出异常,但上述代码无法检测。
Symbol给出了解决方案:
constCOLOR_GREEN=Symbol('green') constCOLOR_RED=Symbol('red')
即使字符串写错或重复也不重要,因为每次调用Symbol()都会给出独一无二的值。这样就可以确保所有isSafe()调用都传入这两个Symbol之一。
私有属性
由于没有访问限制,JavaScript曾经有一个惯例:私有属性以下划线起始来命名。这样不仅无法隐藏这些名字,而且会搞坏代码风格。可以利用Symbol来隐藏这些私有属性:
letspeak=Symbol('speak') classPerson{ [speak](){ console.log('harttle') } }
如下几种访问都获取不到speak属性:
letp=newPerson() Object.keys(p)//[] Object.getOwnPropertyNames(p)//[] for(letkeyinp)console.log(key)//
但Symbol只能隐藏这些函数,并不能阻止未授权访问。仍然可以通过Object.getOwnPerpertySymbols(),Reflect.ownKeys(p)来枚举到speak属性。
新的基本类型
Symbol是新的基本类型,从此JavaScript有7种类型:
- Number
- Boolean
- String
- undefined
- null
- Symbol
- Object
转换为字符串
Symbol支持symbol.toString()方法以及String(symbol),但不能通过+转换为字符串,也不能直接用于模板字符串输出。后两种情况都会产生TypeError,是为了避免把它当做字符串属性名来使用。
转换为数字
不可转换为数字。Number(symbol)或四则运算都会产生TypeError。
转换为布尔
Boolean(symbol)和取非运算都OK。这是为了方便判断是否包含属性。
包裹对象
Symbol是基本类型,但不能用newSymbol(sym)来包裹成对象,需要使用Object(sym)。除了判等不成立外,包裹对象的使用与原基本类型几乎相同:
letsym=Symbol('author') letobj={ [sym]:'harttle' } letwrapped=Object(sym) wrappedinstanceofSymbol//true,真的是true!!! obj[sym]//'harttle' obj[wrapped]//'harttle'
常见的Symbol
文章最前面的例子提到的Symbol.iterator是一个内置Symbol。除此之外常见的内置Symbol还有:
Symbol.match
Symbol.match在String.prototype.match()中用于获取RegExp对象的匹配方法。我们来改写一下Symbol.match标识的方法,
观察String.prototype.match()的表现,下面的例子来自MDN:
//https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@match classRegExp1extendsRegExp{ [Symbol.match](str){ varresult=RegExp.prototype[Symbol.match].call(this,str); returnresult?'VALID':'INVALID'; } } console.log('2012-07-02'.match(newRegExp1('([0-9]+)-([0-9]+)-([0-9]+)'))); //expectedoutput:"VALID" Symbol.toPrimitive
在对象进行运算时经常会变成"[objectObject]",这是对象转换为字符串(基本数据类型)的默认行为,定义在Object.prototype.toString。比如这个对象:
varcount={ value:3 }; count+2//"[objectObject]2"
这个对象也在表示一个数字,怎么让它可以参加四则运算呢?给它加一个Symbol.toPrimitive属性,来改变它转换为基本类型的行为:
count[Symbol.toPrimitive]=function(){ returnthis.value }; count+2//5
更多内置Symbol请参考MDN文档:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol#Well-known_symbols
跨Realm使用
JavaScriptRealm是指当前代码片段运行的上下文,包括全局变量,比如Array,Date这些全局函数。在打开新标签页、加载iframe或加载Worker进程时,都会产生多个JavaScriptRealm。跨Realm通信时这些全局变量是不同的,例如从iframe中传递给数组arr给父窗口,父窗口中收到的arrinstanceofArray为false,因为它的原型是iframe中的那个Array。
但是一个对象在iframe中可以迭代(Iterable),那么在父窗口中也应当能被迭代。这就要求Symbol可以跨Realm,当然Symbol.iterator可以。如果你定义的Symbol也需要跨Realm,请使用SymbolRegistryAPI:
//在SymbolRegistry中注册一个跨RealmSymbol letsym=Symbol.for('foo') //获取Symbol的键值字符串 Symbol.keyFor(sym)//'foo'
内置的跨RealmSymbol其实不在SymbolRegistry中:
Symbol.keyFor(Symbol.iterator) //undefined
总结
以上所述是小编给大家介绍的ES6Symbol的用途,希望对大家有所帮助,如果大家有任何疑问欢迎给我留言,小编会及时回复大家的!