Symbol 类型
简单(原始)数据类型。符号是原始值,且符号的实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。符号就是用来创建唯一记号,进而用作非字符串形式 的对象属性。
一、符号的基本用法
符号需要使用 Symbol() 函数初始化。因为符号是原始类型,所以 typeof 操作符对符号返回symbol。
let sym = Symbol();
console.log(typeof sym); // symbol
调用 Symbol() 函数时,可传入一个字符串参数来对符号的描述,这个字符串与符号定义或标识完全无关,可用来调试代码,再次说明,符号都是唯一的。符号没有字面量语法。你只要创建 Symbol() 实例并将其用作对象的新属性,就可以保证它不会覆盖已有的对象属性,无论是符号属性还是字符串属性。
let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
console.log(genericSymbol); // Symbol()
console.log(otherGenericSymbol); // Symbol()
console.log(genericSymbol == otherGenericSymbol); // false
let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');
console.log(fooSymbol); // Symbol(foo)
console.log(otherFooSymbol); // Symbol(foo)
console.log(fooSymbol == otherFooSymbol); // false
不能用作构造函数,与 new 关键字一起使用。是为了避免创建符号包装对象,像使用 Boolean、String 或 Number 那样,它们都支持构造函数且可用于初始化包含原始值的包装对象:
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 = new Symbol(); // TypeError: Symbol is not a constructor
二、使用全局符号注册表
字符串作为键,在全局符号注册表中创建一个共享和重用的符号,使用 Symbol.for() 方法。
Symbol.for()
let fooGlobalSymbol = Symbol.for('foo'); // 创建全局符号
console.log(fooGlobalSymbol); // Symbol(foo)
console.log(typeof fooGlobalSymbol); // symbol
Symbol.for() 对每个字符串键都执行幂等操作。(幂等注解)第一次使用某个字符串调用时,它会检查全局运行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。
let fooGlobalSymbol = Symbol.for('foo'); // 创建新符号
let otherFooGlobalSymbol = Symbol.for('foo'); // 重用已有符号
console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true
console.log(fooGlobalSymbol); // Symbol(foo)
即使采用相同的符号描述,在全局注册表中定义的符号跟使用 Symbol() 定义的符号也并不等同:
let localSymbol = Symbol('foo');
let globalSymbol = Symbol.for('foo');
console.log(localSymbol === globalSymbol); // false
全局注册表中的符号必须使用字符串键来创建,因此作为参数传给 Symbol.for() 的任何值都会被转换为字符串。没传参数就是 undefined。此外,注册表中使用的键同时也会被用作符号描述。
let emptyGlobalSymbol = Symbol.for();
console.log(emptyGlobalSymbol); // Symbol(undefined)
Symbol.for()
我们可以使用 Symbol.keyFor() 来查询全局注册表,这个方法接收符号,返回该全局符号对应的字符串键。如果查询的不是全局符号, 则返回 undefined 。如果传入的参数不是符号,则会报错。
let s = Symbol.for('foo'); // 创建全局符号
let s1 = Symbol.for();
console.log(Symbol.keyFor(s)); // foo
console.log(Symbol.keyFor(s1)); // undefined 这个是字符串
let s2 = Symbol('bar'); // 创建普通符号
console.log(Symbol.keyFor(s2)); // undefined 这个是 Undefined 类型值
Symbol.keyFor(123); // TypeError: 123 is not a symbol
注意上述打印的两个 undefined 是完全不同的含义,s1 是没传参数创建的以 ”undefined“ 字符串作为描述符的符号。s2 是在全局注册表中没查找到描述符为 “bar” 的符号,返回 undefined 值。他们在控制台打印的颜色不一致。
三、使用符号作为属性
凡是可以使用字符串或数值作为属性的地方,都可以使用符号。这就包括了对象字面量属性和 Object.defineProperty() 或 Object.defineProperties() 定义的属性。对象字面量只能在计算属性语法中使用符号作为属性。Object.defineProperty() Object.defineProperties()注解
let s1 = Symbol('foo'),
s2 = Symbol('bar'),
s3 = Symbol('baz'),
s4 = Symbol('qux');
let o = { // 字面量形式定义属性
[s1]: 'foo val'
};
// 这样也可以: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: {...}, 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"}
// 下行代码用到了 数组的 find 方法和 字符串的 match 方法,还有正则表达式。
let barSymbol = Object.getOwnPropertySymbols(o)
.find((symbol) => symbol.toString().match(/bar/));
console.log(barSymbol); // Symbol(bar)
ECMAScript 6也引入了一批常用内置符号(well-known symbol) ,他们都涉及到很多 js 中的高级用法,本文只对 Symbol 类型做基本的知识点总结。以后会陆续整理更多高级的用法。