js温故(二):symbol
自ES6
中symbol
问世以来,个人在项目中并没有太多机会使用到,小公司业务项目没有给symbol
太多的登场机会。因此之前也只稍微知晓了概念,没有详细了解。如今重温js
,自然要重新认识一下这独一无二的symbol
。如你所知,symbol
是原始值(基本数据类型),其每个实例是独一无二且不可变的,一般作为对象属性使用,确保对象属性独一无二,避免属性冲突。
[toc]
1. 基本用法
符号只能通过函数来创建实例。最基本的就是Symbol()
函数:可以使用Symbol()
函数来创建一个符号实例。有以下几点需要注意:
Symbol()
函数不与new
操作符搭配,即new Symbol()
是不合法的。这是为了防止创建Symbol
包装对象;Symbol()
函数可以传入字符串作为键(也可以不传),此时传入的字符串键只起到描述的作用,并不影响symbol
实例的值。因此,传入相同的字符串键的symbol
实例,其值也不相等而且相互之间也没啥关联。1
2
3
4
5let symbol1 = Symbol("a");
let symbol2 = Symbol("a");
// false
symbol1 === symbol2;
2. 全局符号注册表
如果运行时不同部分的代码需要共享和重用符号实例,则可以用一个字符串作为键,使用 Symbol.for()
方法在全局符号注册表中创建并注册符号。与Symbol()
函数不同,此时,传入的字符串键会影响符号实例的值,一个字符串对应一个独一无二的符号实例。在全局符号注册表中使用同一个字符串键来注册的符号实例之间完全等价,即是同一个符号实例,从而实现共享和重用。
1 | const symbol3 = Symbol.for("a"); |
如果全局符号表中定义的符号没有传入字符串键,则相当于传入了undefined
作为键。因此,所有没有传入字符串键的全局符号完全等价,是同一个符号实例。而由于全局注册表中的符号必须用字符串键来创建,因此传入的任何值都会被转换为字符串,undefined
也会被转换为字符串“undefined”
。如下栗子中的所有symbol
实例的描述都是Symbol(undefined)
。
1 | const symbol5 = Symbol.for(); |
通过全局注册表来实现符号共享与重用的原理是,当第一次用某个字符串键在全局注册表中创建symbol
实例时,见到到注册表中没有该字符串键对应的符号实例,则使用该字符串键创建一个符号实例,并在注册表中保存;当后续试图使用同样的字符串键来在注册表中创建符号实例时,检测到注册表中已经有了对应的符号实例,因此直接返回该符号实例,而不是重新创建,从而实现符号实例的共享与重用。这个过程完全可以用对象或者字典来模拟:
1 | // 全局作用域中创建一个空的注册表 |
使用Symbol.keyFor()
我们也可以通过符号实例来查询注册表中对应的字符串键,该方法接收一个符号实例,返回其对应的字符串。
1 | const symbol9 = Symbol.for("symbol 9 号"); |
同样的,在模拟了Symbol.for()
的基础上,我们也可以模拟Symbol.keyFor()
方法:
1 | Symbol.custom_keyFor = (symbol) => { |
可见,符号实例仍是唯一的,所谓全局注册表,不过是一个对象/字典实例而已。
3. 使用符号作为属性
我们指知道,使用符号作为属性,能避免属性重名引起的冲突。凡是可以使用字符串或数值作为属性的地方,也都可以使用符号来作为属性,包括了 对象字面量属性 ,Object.defineProperty()
以及 Object.defineProperties
定义的属性。
当符号用在对象字面量属性中时,只能使用计算属性语法,即中括号语法 obj[symbol]
。
1 | let s1 = Symbol("s"); |
而在Object.defineProperty()
以及 Object.defineProperties
中使用符号时, 不使用计算属性语法。
1 | // s1不使用计算属性语法 |
我们通常用Object.getOwnPropertyNames()
来获取对象的常规属性数组;类似的,使用Object.getOwnPropertySymbols()
可以获取对象的符号属性数组;使用Reflect.ownKeys()
可以得到两种类型的属性数组;此外,Object.getOwnPropertyDescriptors()
可以得到包含常规和符号属性描述符的对象。
1 | const s1 = Symbol("s1"); |
4. 常用内置符号
ES6
引入了一批常用的内置符号,供开发者访问、重写等。通过重新定义内置符号,可以改变原生结构的行为。例如,for-of
循环会在相关对象上使用Symbol.iterator
属性,如果我们在自定义对象上重新定义Symbol.iterator
属性,就可以改变for-of
在遍历该对象时的行为。内置符号只是全局函数Symbol
的字符串属性而已,各自指向一个符号实例。所有的内置符号属性都是不可改写、不可枚举、不可配置的。符号在ES
规范中的名称,一般是由前缀@@
加上字符串属性,如@@iterator
指Symbol.iterator
。
(1)Symbol.asyncIterator
这个符号表示实现异步迭代器for-await-of
的函数。异步循环时,会调用以Symbol.asyncIterator
为键的函数,并期望这个函数返回一个实现迭代器API
的对象。很多时候,返回的对象是实现该API
的异步生成器。
1 | // 声明一个类 |
(2)Symbol.hasInstance
该符号属性表示一个方法,用以判断 构造函数/类 是否认可一个对象是其实例,这个方法定义在Function
的原型即Function.prototype
上。在使用 instanceof
操作符时,会调用该函数。
1 | const arr = [1, 2, 3]; |
(3)Symbol.isConcatSpreadable
根据ES
规范,这个符号作为一个属性,表示一个布尔值,定义在具体的对象上,用以根据对象的类型决定对象是否应该用Array.prototype.concat
来打平其数组元素:若该值为true
,则会将类数组对象的数组元素打平之后再进行数组拼接操作;否则将类数组对象作为一个整体与数组进行拼接。
数组对象默认会打平到已有的数组中;类数组对象由该值决定是否打平到已有数组中;其它不是类数组对象的对象,该值会被忽略。
1 | // 类数组对象 |
(4)Symbol.iterater
该符号作为一个属性,表示一个方法,供for-of
语句使用,返回对象默认的迭代器。简而言之,该符号属性表示实现迭代器API
的函数。
for-of
语句循环时,会调用以Symbol.iterater
为键的函数,并默认该函数会返回一个实现迭代器API
的对象。很多时候,返回的对象是实现该API
的Generator
。技术上来说,返回的对象应该调用其next()
方法陆续返回值。可以显示地调用next()
方法返回,也可以通过生成器函数返回。
在执行for-of
循环时,会沿着原型链查找以Symbol.iterator
为键的方法。下面的示例改写了Array
的原型上的Symbol.iterator
方法,仅供理解与娱乐。
1 | // 默认情况下的for-of循环 |
(5)Symbol.match
表示一个方法,用正则表达式去匹配字符串,由String.prototype.match()
方法使用。正则表达式的原型上默认有这个方法的定义。可以改写该方法以改变默认对正则表达式求值的行为。
(6)Symbol.replace
表示一个正则表达式方法,替换一个字符串中匹配的子串,由String.prototype.replace()
使用。正则表达式的原型上默认有该方法的定义。默认情况下,传入一个非正则表达式的值,会将该值转化为正则表达式。可以通过改写以Symbol.replace
为键的方法来改变默认行为,使该方法可以直接使用参数,而不必先将参数转化为正则表达式。
1 | class FooReplacer { |
(7)Symbol.search
表示一个正则表达式方法,返回字符串中匹配正则表达式的索引。由String.prototype.search()
方法使用。当然,也可以重写该方法,以改变默认行为。
(8)Symbol.species
表示 i 一个函数,作为创建派生对象构造函数。在内置类型中最为常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方法。用Symbol.species
定义静态的获取器方法(getter),可覆盖创建实例的原型定义。
1 | // 继承Array |
(9)Symbol.split
表示一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串,由String.prototype.split()
方法使用。正则表达式的原型上默认有这个方法的定义。给这个方法传入的非正则表达式的值,会先被转换为正则表达式。通过重新定义Symbol.split()
方法,可以改变该行为。
(10)Symbol.toPromitive
该符号属性表示一个方法,将对象转换为对应的原始值,由ToPrimitive
抽象操作使用。许多内置操作都会尝试将对象转换为原始值。可以通过提供给该函数的参数来控制返回的原始值。
1 | class Bar { |
(11)Symbol.toStringTag
根据ES
规范,该符号属性表示一个字符串,用于创建对象的默认字符串描述。由内置方法Object.prototype.toString()
使用。
通过toString()
方法获取对象标识时,会检索由Symbol.toStringTag
指定的实例标识符,默认为Object
,JS
内置类型都已经指定了该值,但自定义类实例还需要明确定义,否则该属性为undefined
,toString()
方法得到默认的Object
。
1 | // 内置类型已指定 |
(12)Symbol.unscopables
该符号作为一个属性,表示一个对象,对象所有的以及继承而来的属性,都会从关联对象的with
环境中排除。给具体的对象设置该符号属性,并将对应的键映射为true
,则会阻止对象的该属性出现在with
环境绑定中。
1 | let obj = { name: "cc", age: 18 }; |
实际上我们并不推荐使用with
,因此,该符号属性Symbol.unScopables
也不推荐使用。