JavaScript 在 ES6 中引入了 Symbols 作为防止属性名称冲突的方法。并且 Symbols,还提供了一种在2015-2019 JavaScript中模拟私有属性的方法。
简介
创建一个 symbol 非常的简单,只需要调用一下 Symbol() 函数就可以。symbol 有两个比较特殊的特性
Symbols可以作为 Object 的 keys,并且有且只有strings和symbols才能作为 Object 的 keys。- 两个
symbols是不相等的
const symbol1 = Symbol();
const symbol2 = Symbol();
symbol1 === symbol2; // false
const obj = {};
obj[symbol1] = 'Hello';
obj[symbol2] = 'World';
obj[symbol1]; // 'Hello'
obj[symbol2]; // 'World'
尽管使用 Symbol() 创建 symbols 让 symbols 看起来就像是一个 object, 但是 symbols 是一个基础数据类型,如果使用 Symbol 实例化一个构建函数将会抛出异常
const symbol1 = Symbol();
typeof symbol1; // 'symbol'
symbol1 instanceof Object; // false
// Throws "TypeError: Symbol is not a constructor"
new Symbol();
说明
Symbol() 接受一个字符串参数,这个字符串主要起到描述说明的作用,通常都是为了 debugging - 字符串会在调用 symbols 的 toString() 方法时展示。然而,即使两个symbol的描述字符串相等,那两个 symbols 也是不相等的。
const symbol1 = Symbol('my symbol');
const symbol2 = Symbol('my symbol');
symbol1 === symbol2; // false
console.log(symbol1); // 'Symbol(my symbol)'
有一种全局注册 symbol 的方法,在创建 symbol 的时候使用 Symbol.for(String) 创建,这个 symbol 会被注册到全局,并且按传入的字符串做区分。换句话说就是,如果你通过 Symbol.for(String) 创建了两个 symbol,并且传入的字符串是相等的,那么这两个 symbol 就是相等的。
const symbol1 = Symbol.for('test');
const symbol2 = Symbol.for('test');
symbol1 === symbol2; // true
console.log(symbol1); // 'Symbol(test)'
一般来说,除非有特殊情况才会使用这种方式去创建 symbol,否则不要使用,因为有可能会遇到命名冲突。
命名冲突
Symbol.iterator 是第一个使用 symbol 的,具有 Symbol.iterator 函数作为 key 的对象被认为是可迭代的。这意味着该对象可以使用for / of循环。
(关于Symbol.iterator 与循环可以查看,iterator 和 for...of 循环)
const fibonacci = {
[Symbol.iterator]: function*() {
let a = 1;
let b = 1;
let temp;
yield b;
while (true) {
temp = a;
a = a + b;
b = temp;
yield b;
}
}
};
// Prints every Fibonacci number less than 100
for (const x of fibonacci) {
if (x >= 100) {
break;
}
console.log(x);
}
为什么 Symbol.iterator 是一个 symbol 而不是一个 string ?
现在我们来做个假设,用字符串 iterator替换掉 Symbol.iterator 来检查一个对象是否可以被迭代。并且,你又下面这样一个类,可以被迭代
class MyClass {
constructor(obj) {
Object.assign(this, obj);
}
iterator() {
const keys = Object.keys(this);
let i = 0;
return (function*() {
if (i >= keys.length) {
return;
}
yield keys[i++];
})();
}
}
实例化一个 MyClass 将会迭代对象上的所有的key,这就会有一个潜在的问题,假设故意传递了一个 iterator 属性
const obj = new MyClass({ iterator: 'not a function' });
如果这个时候你使用 for/of 去循环 obj,就会抛出一个异常 TypeError: obj is not iterable。因为用户自定义的 iterator 会覆盖原本类的 iterator 属性(在调用 iterator 方法时,会优先找到用户自定义额那个,这样就会报错)。这是很典型的原型污染的问题,随便复制用户数据可能导致一些像__proto__和 constructor 特殊属性出现问题。
这种模式很重要的一点就是 symbols 能够很好的分割一个对象中的用户数据跟程序数据。由于symbol无法用JSON表示,因此不存在将数据传递到具有错误 Symbol.iterator 属性的 Express API的风险。在将用户数据与内置函数和方法(如Mongoose的models)混合的对象中,可以使用符号来确保用户数据不会与内置功能冲突。
私有属性
由于两个 symbols 是不相等的,所以使用 symbols 来定义一些私有属性是非常方便的。(话说我以前定义一些内部变量或者方法都会加上_fn,来表示私有)。Symbols 属性无法呗 Object,keys() 遍历出来,因此,除非是显式导出了一个 symbols,或者使用Object.getOwnPropertySymbols()函数,否则其他任何代码都无法访问该属性。
function getObj() {
const symbol = Symbol('test');
const obj = {};
obj[symbol] = 'test';
return obj;
}
const obj = getObj();
Object.keys(obj); // []
// Unless you explicitly have a reference to the symbol, you can't access the
// symbol property.
obj[Symbol('test')]; // undefined
// You can still get a reference to the symbol using `getOwnPropertySymbols()`
const [symbol] = Object.getOwnPropertySymbols(obj);
obj[symbol]; // 'test'
symbols 对于私有属性也很方便,因为它们不会显示在JSON.stringify()的输出中。准确来说,JSON.stringify() 以静默方式忽略符号键和值。
Symbols are also convenient for private properties because they do not show up in JSON.stringify() output. Specifically, JSON.stringify() silently ignores symbol keys and values.
const symbol = Symbol('test');
const obj = { [symbol]: 'test', test: symbol };
JSON.stringify(obj); // "{}"
###总结
Symbols 是表示对象内部状态的绝佳工具,同时确保用户数据与程序状态保持独立。使用Symbols,不再需要使用'$ __ internalFoo时,请考虑使用
Symbols。