在 JS 已有的基本类型之外,ES6 引入了一种新的基本类型:符号(Symbol)。符号起初被设计用于创建对象私有成员,而这也是JS开发者期待已久的特性。
创建符号值
符号没有字面量形式,这在JS的基本类型中是独一无二的。你可以使用全局 Symbol
函数来创建一个符号值,正如下面这个例子:
1 | let firstName = Symbol() |
此代码创建了一个符号类型的 firstName
变量,并将它作为 person
对象的一个属性,而每次访问该属性都要使用这个符号值。
由于符号值是基本类型的值,因此调用 new Symbol()
将会抛出错误。你可以通过 new Object(yourSymbol)
来创建一个符号实例,但尚不清楚这能有什么作用。
Symbol
函数还可以接受一个额外的参数用于描述符号值,该描述并不能用来访问对应属性,但它能用于调试,例如:
1 | let firstName = Symbol('first name') |
符号的描述信息被存储在内部属性 [[Description]]
中,当符号的 toString()
方法被显式或隐式调用时,该属性都会被读取。
由于符号是基本类型的值,你可以使用 typeof
运算符来判断一个变量是否为符号。ES6 扩充了 typeof
的功能以便让它在作用于符号值的时候能够返回 symbol
。
使用符号值
你可以在任意能使用“需计算属性名”的场合使用符号。此外还可以在 Object.defineProperty()
或 Object.defineProperties()
调用中使用它。
由于符号不存在字面量形式,所以如果以符号作为对象的属性名,就算该属性的 enumerable
被设置为 true
,该属性也无法用 for-in
循环,并且不会显示在 Object.keys()
的结果中。但是你可以使用 in
操作符来判断该属性是否存在!
1 | let firstName = Symbol('first name') |
共享符号值
你或许想在不同的代码段中使用相同的符号值,例如:假设在应用中需要在两个不同的对象类型中使用同一个符号属性,用来表示一个唯一标识符。跨越文件或代码来追踪符号值是很困难并且易错的,为此,ES6 提供了“全局符号注册表”供你在任意时间点进行访问。
若你想创建共享符号值,应使用 Symbol.for()
方法而不是 Symbol()
方法。Symbol.for()
方法仅接受单个字符串类型的参数,作为目标符号值的标识符,同时此参数也会成为该符号的描述信息。例如:
1 | let uid = Symbol.for('uid') |
Symbol.for()
方法首先会搜索全局符号注册表,看是否存在一个键值为 "uid"
的符号值。若是,该方法会返回这个已存在的符号值。否则,会创建一个新的符号值,并使用该键值将其记录到全局符号注册表中,然后返回这个新的符号值。这就意味着此后使用同一个键值去调用 Symbol.for()
方法都将返回同一个符号值,就像下面这个例子:
1 | let uid = Symbol.for('uid') |
共享符号值还有另一个独特用法,你可以使用 Symbol.keyFor()
方法在全局符号注册表中根据符号值检索出对应的键值,例如:
1 | let uid = Symbol.for('uid') |
注意:使用符号值 uid
与 uid2
都返回了键值 "uid"
,而符号值 uid3
在全局符号注册表中并不存在,因此没有关联的键值,Symbol.keyFot()
只会返回 undefined
。
符号值的转换
类型转换是 JS 语言重要的一部分,能够非常灵活地将一种数据类型转换为另一种。然而符号类型在进行转换时非常不灵活,因为其他类型缺乏与符号值的合理等价,尤其是符号值无法被转换为字符串值或数值。因此将符号作为属性所达成的效果,是其他类型所无法替代的。
在之前的例子中使用了 console.log()
来展示符号值的输出,能这么做是由于自动调用了符号值的 String()
方法来产生输出。你也可以直接调用 String()
方法来获取相同的结果,例如:
1 | let uid = Symbol.for('uid') |
String()
方法调用了 uid.toString()
来获取符号的字符串描述信息。但若你想直接将符号转换为字符串,则会引发错误:
1 | let uid = Symbol.for('uid') |
将uid
与空字符串相连接,会首先要求把uid
转换为一个字符串,而这会引发错误,从而阻止了转换行为。
相似地,你也不能将符号转换为数值,对符号使用所有数学运算符都会引发错误,例如:
1 | let uid = Symbol.for('uid') |
此例试图把符号值除以 1,同样引发了错误。无论对符号使用哪种数学运算符都会导致错误,但使用逻辑运算符则不会,因为符号值在运算符中会被认为等价于 true
。
检索符号属性
只能使用 ES6 新增的 Object.getOwnPropertySymbols()
方法用来检索对象的符号属性。Object.keys()
和 Object.getOwnPropertyNames()
方法都不行。
使用知名符号暴露内部方法
ES6 定义了“知名符号”来代表JS中一些公共行为,而这些行为此前被认为只能是内部操作。每一个知名符号都对应全局 Symbol
对象的一个属性,这些知名符号是:
Symbol.hasInstance
:供instanceof
运算符使用的一个方法,用于判断对象继承关系。Symbol.isConcatSpreadable
:一个布尔类型值,在集合对象作为参数传递给Array.prototype.concat()
方法时,指示是否要将该集合的元素扁平化。Symbol.iterator
:返回迭代器的一个方法。Symbol.match
:供String.prototype.match()
函数使用的一个方法,用于比较字符串。Symbol.replace
:供String.prototype.replace()
函数使用的一个方法,用于替换子字符串。Symbol.search
:供String.prototype.search()
函数使用的一个方法,用于定位子字符串。Symbol.species
:用于产生派生对象的构造器。Symbol.split
:供String.prototype.split()
函数使用的一个方法,用于分割字符串。Symbol.toPrimitive
:返回对象所对应的基本类型值的一个方法。Symbol.toStringTag
:供String.prototype.toString()
函数使用的一个方法,用于创建对象的描述信息。Symbol.unscopables
:一个对象,该对象的属性指示了那些属性名不允许被包含在with
语句中。
下面将介绍其中的一些知名符号。
Symbol.hasInstance
每个函数都具有一个 Symbol.hasInstance
方法,用于判断指定对象是否为本函数的一个实例。这个方法定义在 Function.prototype
上,因此所有函数都继承了面对 instanceof
运算符时的默认行为。Symbol.hasInstance
属性自身是不可写入、不可配置、不可枚举的,从而保证它不会被错误地重写。
Symbol.hasInstance
方法只接受单个参数,即需要检测的值。如果该值是本函数的一个实例,则方法会返回 true
。为了理解该方法是如何工作的,可研究下述代码:
1 | obj instanceof Array |
ES6 从本质上将 instanceof
运算符重定义为上述方法调用的简写语法,这样使用 instanceof
便会出发一次方法调用,实际上允许你改变该运算符的工作。
假设你想定义一个函数,使得任意对象都不会被判断为该函数的一个实例,你可以采用硬编码的方式来让该函数的 Symbol.hasInstance
方法始终返回 false
,就像这样:
1 | function MyObject () { |
上例中通过 Object.defineProperty()
方法在 MyObject
对象上设置了 Symbol.hasInstance
属性,从而屏蔽了原型上不可写入的 Symbol.hasInstance
属性。
Symbol.isConcatSpreadable
首先请看下面数组 concat()
方法的例子:
1 | let colors1 = [ 'red', 'green' ] |
concat()
方法会区别对待自己接收到的参数,如果参数为数组类型,那么它会自动的将数组扁平化(即分离数组中的元素)。而其他非数组类型的参数无需如此处理。在 ES6 之前,没有任何手段可以改变这种行为。
Symbol.isConcatSpreadable
属性是一个布尔类型的属性,它默认情况下并不会作为任意常规对象的属性。它只出现在特定类型的对象上,用来标示该对象作为 concat()
参数时应如何工作。
成功使用这个属性的前提条件是拥有该属性的对象,要在两个方面与数组类似:拥有数值类型的键和拥有 length
属性。
当该属性为 true
时,将该属性所属对象传递给 concat()
方法时,将所属对象扁平化。当该属性为 false
时,所属对象不会被扁平化。请看下面的例子:
1 | let obj1 = { |
Symbol.match、Symbol.replace、Symbol.search 与 Symbol.split
在 JS 中,字符串与正则表达式有着密切的联系,尤其是字符串具有几个可以接受正则表达式作为参数的方法:match、replace、search和split方法。
在 ES6 之前这些方法的实现细节对开发者是隐藏的,使得开发者无法将自定义对象模拟成正则表达式(并将它们传递给字符串的这些方法)。而 ES6 定义了 4 个符号以及对应的方法,将原生行为外包到内置的 RegExp
对象上。
Symbol.match
:此函数接受一个字符串参数,并返回一个包含匹配结果的数组。若匹配失败,则返回null
。Symbol.replace
:此函数接受一个字符串参数与一个替换用的字符串,并返回替换后的结果字符串。Symbol.search
:此函数接受一个字符串参数,并返回匹配结果的数值索引。若匹配失败,则返回 -1。Symbol.split
:此函数接受一个字符串参数,并返回一个用匹配值分割而成的字符串数组。
在对象上定义这些属性,允许你创建能过进行模式匹配的对象,而无需使用这则表达式,并且允许在任何需要正则表达式的方法中使用该对象。这里有一个例子,展示了这些符号的使用:
1 | // 有效等价于/^.{10}$/ |
Symbol.toPrimitive
Symbol.toPrimitive
方法被定义在所有常规类型的原型上,规定了在对象被转换为基本类型值的时候会发生什么。当需要转换时,Symbol.toPrimitive
会被调用,并按照规范传入一个提示性的字符串参数。该参数有 3 种可能:当参数值为 number
的时候,应当返回一个数值。当参数值为 string
的时候,应当返回一个字符串。而当参数为 default
的时候,对返回值类型没有特别要求。
对于大部分常规对象,“数值模式”依次会有下述行为:
- 调用
valueOf()
方法,如果方法返回值是一个基本类型值,那么返回它。 - 否则,调用
toString()
方法,如果方法返回值是一个基本类型值,那么返回它。 - 否则,抛出一个错误。
类似的,对于大部分常规对象,“字符串模式”依次会有下述行为:
- 调用
toString()
方法,如果方法返回值是一个基本类型值,那么返回它。 - 否则,调用
valueOf()
方法,如果方法返回值是一个基本类型值,那么返回它。 - 否则,抛出一个错误。
在多数情况下,常规对象的默认模式都等价于数值模式(只有 Date
类型例外,它默认使用字符串模式)。通过定义 Symbol.toPrimitive
方法,你可以重写这些默认的转换行为。
使用 Symbol.toPrimitive
属性并将一个函数赋值给它,便可以重写默认的转换行为,例如:
1 | function Temperature (degrees) { |
Symbol.toStringTag
JS 最有趣的课题之一是在多个不同的全局执行环境中使用,这种情况会在浏览器页面包含内联帧(iframe)的时候出现,此时页面与内联帧均拥有各自的全局执行环境。大多数情况下这并不是一个问题,使用一些轻量级的转换操作就能够在不同的运行环境之间传递数据。问题出现在想要识别目标对象到底是什么类型的时候,而此时该对象已经在环境之间经历了传递。
该问题的典型例子就是从内联帧向容器页面传递数组,或者反过来。在 ES6 术语中,内联帧与包含它的容器页面分别拥有一个不同的“域”,以作为 JS 的运行环境,每个“域”都拥有各自的全局作用域以及各自的全局对象拷贝。无论哪个“域”创建的数组都是正规的数组,但当它跨域进行传递时,使用 instanceof Array
进行检测却会得到 false
的结果,因为该数组是由另外一个“域”的数组构造器创建的,有别于当前“域”的数组构造器。
识别问题的变通解决方案
变通的解决方案为 Object.prototype.toString.call()
。
ES6 给出的答案
ES6 通过 Symbol.toStringTag
重定义了相关行为,该符号是对象的一个属性,定义了 Object.prototype.toString.call()
被调用时应当返回什么值。
1 | function Person (name) { |
Symbol.unscopables
尽管将来的代码无疑会停用 with
语句,但 ES6 仍然在非严格模式中提供了对于 with
语句的支持,以便向下兼容。为此需要寻找方法让使用 with
语句的代码能够适当地继续工作。为了理解这个任务的复杂性,可研究如下代码:
1 | let values = [1, 2, 3] |
在此例中,...values
引用了 with
语句之外的变量 values
。
但ES6为数组添加了一个 values
方法(迭代器与生成器的知识),这意味着在 ES6 的环境中,with
语句内部的 values
并不会指向 with
语句之外的变量 values
,而是会指向数组的 values
方法,从而会破坏代码的意图。这也是 Symbol.unscopables
符号出现的理由。
Symbol.unscopables
符号在 Array.prototype
上使用,以指定哪些属性不允许在 with
语句内被绑定。Symbol.unscopables
属性是一个对象,当提供该属性时,它的键就是用于忽略 with
语句绑定的标识符,键值为 true
代表屏蔽绑定。以下是数组的 Symbol.unscopables
属性的默认值:
1 | // 默认内置在 ES6 中 |