关于 Symbol
- Symbol(符号)是 ES6 新增的数据类型。符号是原始值,且符号的实例时唯一的、不可变的。符号的用途时确保对象属性使用唯一标识符,不会发生属性冲突危险。
- 符号就是用来创建唯一记号,进而用作非字符串形式的对象属性。
- 在我的理解里,Symbol 很像一个工厂函数。但实际上他只是一个基本数据类型。
符号的基本用法
- 符号需要使用
Symbol()
函数初始化。因为符号本身时原始类型,所以 typeof 返回的是 Symbol - 调用
Symbol()
函数时,也可以传入一个字符串参数作为对符号的描述,将来可以通过这个字符串来调试代码。但是,这个字符串参数与符号定义或标识完全物馆。这也是它们发挥作用的关键。
let sym = Symbol();
console.log(typeof sym); // symbol
let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');
console.log(genericSymbol == otherGenericSymbol); // false
console.log(fooSymbol == otherFooSymbol); // false
console.log(genericSymbol); // Symbol()
console.log(fooSymbol); // Symbol(foo)
Symbol()
不能与 new 关键字一起作为构造函数使用。这样做是为了避免创建符号包装对象- 如果确实像使用符号包装对象,可以借用
Object()
函数
let myBoolean = new Boolean();
console.log(typeof myBoolean); // object
let myString = new String();
console.log(typeof myString); // object
let myNumber = new Number();
console.log(typeof myNumber); // object
let mySymbol = Symbol();
let myWrappedSymbol = Object(mySymbol);
console.log(typeof myWrappedSymbol); // object
使用全局符号注册表
Symbol.for()
- 如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为键,再全局符号注册表中创建并重用符号。
Symbol.for()
对每个字符串键都执行幂等操作。第一次使用某个字符串作为键调用时,他会检查全局运行时注册表,发现不存在对应的符号,鱼事就会生成一个新符号实例并添加到注册表中。后续使用相同字符串的调用同样回检查注册表,发现存在与该字符串对应的符号,就会返回该符号实例
let fooGlobalSymbol = Symbol.for('foo'); // -> 创建新符号
console.log(typeof fooGlobalSymbol); // Symbol
let otherFooGlobalSymbol = Symbol.for('foo'); // 重用已有符号
console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true
// 即使采用相同的符号描述,在全局注册表中定义的符号跟使用 Symbol() 定义的符号也并不相同
let localSymbol = Symbol('foo');
console.log(fooGlobalSymbol == localSymbol); // false
// 全局注册表中的符号必须使用字符串键来创建,因此作为参数传给 Symbol.for() 的任何值都会被转换为字符串。同时,键月ibei用作符号描述
let emptyGlobalSymbol = Symbol.for();
console.log(emptyGlobalSymbol); // Symbol(undefined)
- 我们可以使用
Symbol.keyFor()
来查询全局注册表,这个方法接收符号,返回该全局符号对应的字符串键。如果查询的不是全局符号,则会返回 undefied。如果查询的类型不是 Symbol 则会抛出错误。
let s = Symbol.for('foo');
console.log(Symbol.keyFor(s)); // foo
let s2 = Symbol('bar');
console.log(Symbol.keyFor(s2)); // undefined
console.log(Symbol.keyFor('symbol')); // TypeError: symbol is not a symbol
使用符号作为属性
- 凡是可以使用字符串或者数值作为属性的地方,都可以使用符号。包括了对象字面量属性和
Object.defineProperty()
/Object.defineProperties()
定义的属性。对象字面量只能在计算属性语法中使用符号作为属性。
let s1 = Symbol('foo'),
s2 = Symbol('bar'),
s3 = Symbol('baz'),
s4 = Symbol('qux');
let o = {
[s1]: 'foo val'
}
console.log(o); // {Symbol(foo): "foo val"}
Object.defineProperty(o, s2, { value: 'bar val' });
console.log(o); // {Symbol(foo): "foo val", Symbol(bar): "bar val"}
Object.defineProperties(o, {
[s3]: { value: 'baz val' },
[s4]: { value: 'qux val' }
});
console.log(o); // {Symbol(foo): "foo val", Symbol(bar): "bar val", Symbol(baz): "baz val", Symbol(qux): "qux val"}
- 类似于
Object.getOwnPropertyNames()
返回对象实例的常规属性数组,Object.getOwnPropertySymbols
返回对象实例的符号属性数组。这两个方法的返回值彼此互斥。 Object.getOwnPropertyDescriptors
会返回同时包含常规和符号属性描述符的对象。Reflect.ownKeys()
回返回两种类型的键。
let s1 = Symbol('foo'),
s2 = Symbol('bar');
let o = {
[s1]: 'foo val',
[s2]: 'bar val',
baz: 'baz val',
qux: 'qux val'
}
console.log(Object.getOwnPropertySymbols(o)); // [Symbol(foo), Symbol(bar)]
console.log(Object.getOwnPropertyNames(o)); // ["baz", "qux"]
console.log(Object.getOwnPropertyDescriptors(o)); // {baz: {configurable: true, enumerable: true, value: "baz val", writable: true}, qux: {…}, Symbol(foo): {…}, Symbol(bar): {…}}
console.log(Reflect.ownKeys(o)); // ["baz", "qux", Symbol(foo), Symbol(bar)]
- 因为符号的属性是堆内存中符号的一个引用,所以直接创建并用作属性的符号不会丢失。但是,如果没有显式的保存对这些属性的引用,那么必须遍历对象的所有符号属性才能找到响应的符号属性。
let o = {
[Symbol('foo')]: 'foo val',
[Symbol('bar')]: 'bar val'
}
console.log(o); // {Symbol(foo): "foo val", Symbol(bar): "bar val"}
let barSymbol = Object.getOwnPropertySymbols(o).find(symbol => symbol.toString().match(/bar/));
console.log(barSymbol); // Symbol(bar)
常用内置符号
- ES6 也引入了一批常用内置符号(well-known symbol),用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。这些内置符号都以 Symbol 工厂函数字符串属性的形式存在。
- 这些内置符号最重要的用途直以是重新定义它们,从而改变原生结构的行为
- 比如,
for-of
循环会再相关对象上使用Symbol.iterator
属性,那么就可以通过再自定义对象上重新定义Symbol.iterator
的值,来改变for-of
在迭代该对象时的行为。 - 这些内置符号也没有什么特别之处,它们就时全局函数 Symbol 的普通字符串属性,只想一个符号的实例。所以内置符号属性都是不可写、不可枚举、不可配置的。
Symbol.asyncIterator
...省略(47.5)
Symobl.hasInstance
- 根据 ECMA 规范,这个符号作为一个属性表示“一个方法,该方法决定一个构造器对象是否认可一个对象是他的实例。由
instanceof
操作符使用”。instanceof
操作符可以用来确定一个对象实例的原型连上是否有原型。 - 在 ES6 中,
instanceof
操作符会使用Symbol.hasInstance
函数来确定关系。以Symbol.hasInstance
为键的函数会执行同样的操作,只是操作数对调了一下
function Foo() { }
class Bar { }
let foo = new Foo();
let bar = new Bar();
console.log(foo instanceof Foo); // true
console.log(bar instanceof Bar); // true
console.log(Foo[Symbol.hasInstance](foo)); // true
console.log(Bar[Symbol.hasInstance](bar)); // true
- 这个属性定义在
Function
的原型上,因此默认在所有函数和类上都可以调用。由于instanceof
操作符会在原型链上寻找这个属性定义,就跟在原型链上寻找其他属性一样,因此可以在继承的类上通过静态方法重新定义这个函数。
class Bar { }
class Baz extends Bar {
static [Symbol.hasInstance]() {
return false;
}
}
let b = new Baz();
console.log(b instanceof Bar); // true
console.log(Bar[Symbol.hasInstance](b)); // true
console.log(b instanceof Baz); // false
console.log(Baz[Symbol.hasInstance](b)); // false
To be continued...