一.Symbol是什么
typeof Symbol() === 'symbol'
,symbol是js中第7种基本类型(本来就有的6种是null, undefined, Number, Boolean, Object, String),不是字符串也不是对象
作用:symbol用来避免命名冲突,解决了篡改(添加属性)原生对象的后遗症,不用担心属性名以后和原生属性名或者其它类库操作冲突
二.语法
获取Symbol有3种方式,如下:
1.Symbol(desc)
返回symbol,desc可选,symbol.toString()返回`Symbol(${desc})`
,例如:
var obj = {
a: 1
};
// 不用new,Symbol不是构造器
var safeKey = Symbol();
obj[safeKey] = 'value';
console.log(obj[safeKey]); // value
var anotherSafeKey = Symbol('isAnimActive');
console.log(anotherSafeKey); // Symbol(isAnimActive)
上面的var safeKey = Symbol();
看起来比较奇怪,new
操作符呢?Symbol
不是构造器,不能通过new
操作符调用,非要new
的话,会得到这样一个错误:
Uncaught TypeError: Symbol is not a constructor
这确实比较奇怪,编码规范中一般首字母大写表示类型名,理应通过new
操作符调用,但API给出的就是一个不合编码规范的Symbol
函数
特点:
Symbol('key') !== Symbol('key')
,每次调用Symbol()返回的都是不一样的值Symbol可以用作属性名,而且和任何东西(字符串、数字、其它Symbol)都不相等,所以Symbol类型的属性名不会和任何已有属性名冲突,也不会和将来的任何新属性名冲突
for...in
、Object.keys(obj)
、Object.getOwnPropertyNames(obj)
会跳过symbol属性Object.getOwnPropertySymbols(obj)
返回对象所有的symbol属性名Reflect.ownKeys(obj)
返回对象的所有属性名(包括symbol属性名和字符串属性名)symbol只读,类似于字符串,严格模式下给symbol添加属性会报错TypeError
symbol不会自动转换为字符串,尝试拼接symbol会报错TypeError,可以手动调用toString()再拼接
示例(接着上一个示例)如下:
obj[Symbol('ready')] = true;
//!!! undefined
// 因为特点1
console.log(obj[Symbol('ready')]); // undefined
console.log(obj); // Object {Symbol(): "value", Symbol(ready): true}
// 跳过symbol属性
for (var key in obj) {
console.log(`obj[${key}] = ${obj[key]}`);
} // obj[a] = 1
// 获取所有obj上所有Symbol类型的属性名
console.log(Object.getOwnPropertySymbols(obj)); // Array [ Symbol(), Symbol(ready) ]
// 获取objs上的所有属性名
console.log(Reflect.ownKeys(obj)); // Array [ "a", Symbol(), Symbol(ready) ]
// 只读
var s = Symbol();
s.a = 1;
console.log(s.a); // undefined
// 不会自动转字符串
// console.log(Symbol('123') + '4'); // TypeError: can't convert symbol to string
console.log(Symbol('123').toString() + '4'); // Symbol(123)4
2.Symbol.for(str)
表示symbol注册表,用来创建共享symbol,特点如下:
Symbol.for('ready') === Symbol.for('ready')
Symbol('str') !== Symbol.for('str')
示例(接着前面的所有示例):
// Symbol.for()
obj[Symbol.for('ready')] = 'ready';
console.log(obj[Symbol.for('ready')]); // ready
console.log(Object.getOwnPropertySymbols(obj));
// log print: Array [ Symbol(), Symbol(ready), Symbol(ready) ]
P.S.最后输出的数组中有两个Symbol(ready),但它们对应的symbol不相等,只是toString返回的结果相同(特点2)
注意:共享Symbol的话,只能保证属性名不与将来的DOM API冲突,不能保证不与其它代码冲突。类库作者不应该使用它,建议只在业务代码中需要跨模块或者跨页面Symbol共享时使用
3.Symbol.xxx
用来获取原生Symbol,例如Symbol.iterator
,特点如下:
新API向后兼容,比如实现iterable接口:
obj[Symbol.iterator] = gen
,不会影响旧代码准备好了hook,借助symbol以后新特性API都不会影响旧代码
尚未实现的新特性:
Symbol.hasInstance
扩展instanceof
Symbol.unscopables
阻止方法加入动态作用域Symbol.match
扩展str.match
这些Symbol已经作为原生Symbol预留出来了,很快就会实现,以后的(增强现有API的)新特性都可以通过Symbol简单安全地添加
三.应用场景
1.给DOM元素添个标记属性
可能多数时候不需要给DOM元素添加自定义属性,因为这存在副作用(自定义属性名可能与其它代码冲突,或者与将来的DOM API冲突),于是一般都选择维护一个表结构来保存元素对应的状态等附加信息,每次查表获得目标元素的附加信息,如果表很大,查表会比较耗时,如果表结构比较复杂,查表操作本身也会变得很麻烦。。。等等,我们为什么要查表获取元素的附加信息?因为副作用,那如果这个副作用没了呢?
Symbol就是用来消除这个副作用的,元素相关的信息,直接以Symbol对象为key,添在DOM元素上就可以了,简单粗暴安全有效,例如:
var IS_MOVING = Symbol('isMoving');
var INFO = Symbol('info');
if (!elem[IS_MOVING]) {
anim(elem);
elem[IS_MOVING] = true;
elem[INFO] = {
step: 1;
};
}
需要持有自定义的Symbol类型的key,把该key放在自己的作用域里,就不会有任何副作用了
2.生成唯一的key
以前生成唯一的key可能需要一个稍复杂的小算法,而现在Symbol是最简单的唯一key生成方法,例如:
// 不关注属性名,只是想简单地存取obj
var data = {
dir: {},
save(val) {
var key = Symbol();
this.dir[key] = val;
return key;
},
get(key) {
return this.dir[key];
}
}
var key = data.save('data');
console.log(data.get(key)); // data
3.通过原生Symbol实现支持的API扩展
标准将提供越来越多的hook,让我们来实现API扩展方法,例如让自定义对象可迭代:
var obj = {
'a': 1,
'b': 2
}
// generator实现迭代器
obj[Symbol.iterator] = function*() {
for (var key in this) {
yield this[key];
}
}
for (var val of obj) {
console.log(val);
}
四.总结
Symbol算是一个精致的特性,解决了扩展现有API的大问题(给新特性提供了一套hook机制,以后可以安全地扩展)
参考资料
- 《ES6 in Depth》:InfoQ中文站提供的免费电子书