Symbol
Symbol 数据类型是一种原始数据类型,该类型的性质在于这个类型的值可以用来创建 <strong style="color: red">匿名</strong> 的对象属性。
类型声明
interface SymbolConstructor {
readonly prototype: Symbol;
(descriptioon?: string | number): symbol;
for(key: string): symbol;
keyFor(sym: symbol): string | undefined;
}
declare var Symbol: SymbolConstructor;
类型特性
类型检测
Symbol 值只能通过 Symbol 函数生成。 Symbol 类型的值作为对象属性名可以保证不会与其他属性名不产生冲突。
const symbol = Symbol();
console.log(typeof symbol);
// Output: 'symbol'
无法实例化
注意,Symbol 函数前不能使用 new
命令实例化,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。
new Symbol('symbol');
// TypeError: Symbol is not a constructor
原型检测
使用 instanceof
检测实例与 Symbol 之间的关系。
const symbol = Symbol('foo');
console.log(symbol instanceof Symbol);
// false
实例描述
Symbol 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
如果 Symbol 的参数是一个对象,就会调用该对象的 toString
方法,将其转为字符串,然后才生成一个 Symbol 值。
const obj = {
toString() {
return 'abc';
},
};
const sym = Symbol(obj);
console.log(sym);
// Symbol(abc)
相同描述
注意,Symbol
函数的参数只是表示对当前 Symbol 值的描述,因此 相同参数 的 Symbol
函数的返回值是不相等的。
// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();
s1 === s2;
// false 表示两者并非严格双等
// 有参数的情况
let s3 = Symbol('foo');
let s4 = Symbol('foo');
s3 === s4;
// false
类型运算与转换
Symbol 值不能与其他类型的值进行运算,会报错
let sym = Symbol('symbol');
'HELLO' +
sym`HELLO ${
sym
// TypeError: Cannot convert a Symbol value to a string
}`;
// TypeError: Cannot convert a Symbol value to a string
但是,Symbol 值可以显式转为字符串。
let sym = Symbol('symbol');
String(sym);
// 'Symbol(symbol)'
sym.toString();
// 'Symbol(symbol)'
另外,Symbol 值也可以转为布尔值,但是不能转为数值。
let sym = Symbol();
Boolean(sym);
// true
console.log(!sym);
// false
if (sym) {
// ...
}
Number(sym);
// TypeError: Cannot convert a Symbol value to a number
console.log(sym + 2);
// TypeError: Cannot convert a Symbol value to a number
对象属性名
由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为 标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。
在实际工作中,肯定会经常使用到别人的类库,然后因为不知道某个对象的属性名,就不小心重写了那个对象的某个属性,导致一些不必要的错误出现。但是有了 Symbol 类型后,我们可以很容易的避免掉这样的失误。
let mySymbol = Symbol();
// 第一种写法 属性访问器
let a = {};
a[mySymbol] = 'Hello!';
// 第二种写法 字面量
let b = {
[mySymbol]: 'Hello!',
};
// 第三种写法 对象原型方法
let c = {};
Object.defineProperty(c, mySymbol, { value: 'Hello!' });
// 以上写法都得到同样结果
a[mySymbol];
// 'Hello!'
⚠️ 注意,Symbol 值作为对象属性名时,不能用 点运算符。
const sym = Symbol();
const a = {};
a.sym = 'Hello!';
console.log(a[sym]);
// undefined
console.log(a['sym']);
// 'Hello!'
上面代码中,因为点运算符后面总是字符串,所以不会读取 sym
作为标识名所指代的那个值,导致 a
的属性名实际上是一个字符串,而不是一个 Symbol 值。
同理,在对象内部,使用 Symbol 值定义属性时,Symbol 值必须放在 方括号 内。
let sym = Symbol()
let foo = {
[sym]: function (arg) {...}
}
foo[sym](123)
采用增强的对象写法。
let foo = {
[sym](arg) {...}
}
Symbol 类型还可以用于定义一组常量,保证这组常量的值都是不相等的。
const foo = {};
foo.constants = {
a: Symbol('a'),
b: Symbol('b'),
c: Symbol('c'),
};
console.log(foo.constants.a);
// Symbol(a)
还有一点需要注意,Symbol 值作为属性名时,该属性还是公开属性,不是私有属性。
不可枚举
Symbol 作为属性名,该属性不会出现在 for...in
、for...of
循环中,也不会被 Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回。
但是,它也不是私有属性,有一个 Object.getOwnPropertySymbols
方法,可以获取指定对象的所有 Symbol 属性名。
Object.getOwnPropertySymbols
方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
const foo = {};
let a = Symbol('a');
let b = Symbol('b');
foo[a] = 'Hello';
foo[b] = 'world';
const sym = Object.getOwnPropertySymbols(foo);
console.log(sym);
// [Symbol(a), Symbol(b)]
静态方法
Symbol.for()
Symbol.for()
方法会根据给定的键,来从运行时的 Symbol 注册表中找到对应的 Symbol,如果找到了,则返回它,否则,新建一个与该键关联的 Symbol,并放入全局 Symbol 注册表中。
Symbol.for(key);
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
s1 === s2;
// true
上面两个变量均为同样参数的 Symbol.for
方法生成的,所以实际上是同一个值。
Symbol.for
与 Symbol
这两种写法,都会生成新的 Symbol。它们的区别是:前者会被登记在全局环境中供搜索,后者不会。
Symbol.for()
不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的 key
是否已存在,如果不存在才会新建一个值。否则,返回的是同一个 Symbol 类型的值。
let s1 = Symbol('foo');
let s2 = Symbol('foo');
let s3 = Symbol.for('foo');
let s4 = Symbol.for('foo');
s1 === s2;
// false
s3 === s4;
// true
s1 === s3;
// false
Symbol.keyFor()
Symbol.keyFor
方法用于获取 Symbol 注册表中指定的 Symbol 值关联的键。
Symbol.keyFor(sym);
参数 sym
为存储在 Symbol 注册表中的某个 Symbol 实例。
const s1 = Symbol.for('s1');
Symbol.keyFor(globalSym);
// 's1'
const s2 = Symbol();
Symbol.keyFor(s2);
// undefined
⚠️ 注意,这个函数用于查找一个 Symbol 值的注册信息的,如果你使用 Symbol 函数创建的 Symbol 值,不论你传不传递参数,通过 Symbol.keyFor()
函数是查不到它的注册信息的。也就是说,通过 Symbol()
函数产生的 Symbol 值是没户口的孩子。但是通过 Symbol.for()
函数产生的 Symbol 值都是可以查到注册信息的。
内置值
除了自定义的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。
- Symbol.hasInstance
- Symbol.isConcatSpreadable
- Symbol.iterator
- Symbol.match
- Symbol.replace
- Symbol.search
- Symbol.species
- Symbol.split
- Symbol.toPrimitive
- Symbol.toStringTag
- Symbol.unscopables
手写实现
手动实现 Symbol:
(function () {
var root = this;
var generateName = (function () {
var postfix = 0;
return function (descString) {
postfix++;
return '@@' + descString + '_' + postfix;
};
})();
var SymbolPolyfill = function Symbol(description) {
if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor');
var descString = description === undefined ? undefined : String(description);
var symbol = Object.create({
toString: function () {
return this.__Name__;
},
valueOf: function () {
return this;
},
});
Object.defineProperties(symbol, {
__Description__: {
value: descString,
writable: false,
enumerable: false,
configurable: false,
},
__Name__: {
value: generateName(descString),
writable: false,
enumerable: false,
configurable: false,
},
});
return symbol;
};
var forMap = {};
Object.defineProperties(SymbolPolyfill, {
for: {
value: function (description) {
var descString = description === undefined ? undefined : String(description);
return forMap[descString]
? forMap[descString]
: (forMap[descString] = SymbolPolyfill(descString));
},
writable: true,
enumerable: false,
configurable: true,
},
keyFor: {
value: function (symbol) {
for (var key in forMap) {
if (forMap[key] === symbol) return key;
}
},
writable: true,
enumerable: false,
configurable: true,
},
});
root.SymbolPolyfill = SymbolPolyfill;
})();
无法实现特性:
- 使用
typeof
,结果为"symbol"
。利用 ES5,我们并不能修改typeof
操作符的结果,所以无法实现 - Symbol 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分
- Symbol 值不能与其他类型的值进行运算,会报错
- Symbol 值可以显式转为字符串
- Symbol 作为属性名,该属性不会出现在
for...in
、for...of
循环中,也不会被Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回
总结
- Symbol 值只能通过
Symbol()
函数生成 Symbol()
的参数只是表示对当前 Symbol 值的描述,相同参数调用返回值不相等Symbol
函数前不能使用new
命令- 对象的属性名可以为 Symbol 类型,能避免对象属性重名
Symbol
值作为对象属性名不能用点运算符
应用场景:
- 利用 Symbol 值的唯一特性,作为类库某些对象的属性名,这样可以避免使用者命名冲突导致的覆盖问题