7.4 Symbol 作为属性键
创建永远不会冲突的键,在以下两种场景中非常有用:
- 在继承树中定义非公开的属性。
- 保证子层属性不会和父层属性冲突。
7.4.1 Symbol 用作非公开属性键
无论何种形式的 JavaScript 继承(例如通过类,通过 mixin ,或者就是单纯的原型的方式),都会存在两种类型的属性:
- 公开属性 能被客户代码访问。
- 私有属性 在组成继承结构的代码片段(例如类、 mixin 或者对象)内部使用。(受保护的属性 被若干个代码片段共享,和私有属性面临同样的问题。)
为了可用性的缘故,公开属性通常用字符串作为键。但是,对于字符串作为键的私有属性,意外的命名冲突可能会导致一些问题。因此, symbol 是一个好的选择。例如,在下面的代码中, symbol 用于私有属性 _counter
和 _action
。
const _counter = Symbol('counter');
const _action = Symbol('action');
class Countdown {
constructor(counter, action) {
this[_counter] = counter;
this[_action] = action;
}
dec() {
let counter = this[_counter];
if (counter < 1) return;
counter--;
this[_counter] = counter;
if (counter === 0) {
this[_action]();
}
}
}
注意 Symbol 仅会使你免于命名冲突,并不会阻止未授权的属性访问,因为可以通过 Reflect.ownKeys()
找出对象所有自有属性键,包括 symbol 。如果你想对属性做保护,可以选择在这一节中讲述的方法之一:类的私有数据。
7.4.2 Symbol 用作元级属性键
相对于“普通的”属性键,Symbol 的唯一性使其在一个不同的纬度成为理想的公开属性,因为元级键和普通键不能冲突。举个元级键的例子,对象可以实现一些特定的方法,从而自定义某个库如何处理该对象。使用 Symbol 键可以避免第三方库错误地将普通方法当成自定义方法。
ECMAScript 6 中的可迭代性就属于这种自定义方法的场景。如果一个对象有一个键为 Symbol.iterator
的方法,那么它就是可迭代的。在下面的例子中,obj
是可迭代的。
const obj = {
data: [ 'hello', 'world' ],
[Symbol.iterator]() {
const self = this;
let index = 0;
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++]
};
} else {
return { done: true };
}
}
};
}
};
obj
的可迭代性使其可用于 for-of
循环,以及类似的 JavaScript 特性中:
for (const x of obj) {
console.log(x);
}
// Output:
// hello
// world
7.4.3 JavaScript 标准库命名冲突举例
或许你觉得命名冲突不要紧,下面有三个例子,展示了在 JavaScript 标准库演化过程中命名冲突带来的问题:
- 在引入新方法
Array.prototype.values()
的时候,破坏了已有的with
与数组结合使用的代码,它会在外面的作用域中隐藏一个values
变量(bug report 1,bug report 2)。因此,引入了一种隐藏属性的机制( System.unscopables )。 String.prototype.contains
和 MooTools 中添加的一个方法冲突了,不得不重命名成String.prototype.includes
( bug report )。- ES2016 中的
Array.prototype.contains
方法也和 MooTools 中添加的方法冲突了,不得不重命名成Array.prototype.includes
( bug report )。
相比之下,通过属性键 System.iterator
给一个对象增加可迭代性就不会引起问题,因为该键并不会和任何键冲突。
上述例子展示了成为一门 web 开发语言意味着什么:向后兼容是至关重要的,这就是为什么在改进语言的时候,偶尔妥协是必须的。这种向后兼容的好处就是,改进老的代码库很轻松,因为新的 ECMAScript 版本绝不会(好吧,是几乎不会)破坏这些老代码。