## Symbols 的出现是为了什么呢?
* 翻译自 [medium](https://medium.com/intrinsic/javascript-symbols-but-why-6b02768f4a5c)
* Symbols 是 JavaScript 最新推出的一种基本类型,它被当作对象属性时特别有用,可是有什么是它能作而 String 不能作的呢?
* 在咱们开始探索 Symbols 功能以前,咱们先来看一下被不少开发者忽略 JavaScript 的特性。
### 背景:
* JavaScript 有两种值类型,一种是 基本类型 (primitives),一种是 对象类型 (objects,包含 function 类型),基本类型包括数字 number (包含 integer,float,Infinity,NaN),布尔值 boolean,字符串 string,undefined,null,尽管 `typeof null === 'object'`,null 仍然是一个基本类型。
* 基本类型的值是不可变的,固然了,存放基本类型值得变量是能够被从新分配的,例如当你写 `let x = 1; x++`,变量 x 就被从新分配值了,可是你并无改变原来的1.
* 一些语言,例如 c 语言有引用传递和值传递的概念,JavaScript 也有相似的概念,尽管它传递的数据类型须要推断。当你给一个 function 传值的时候,从新分配值并不会修改该方法调用时的参数值。然而,假如你修改一个非基本类型的值,修改值也会影响原来的值。
* 考虑下下面的例子:
```
function primitiveMutator(val) {
val = val + 1;
}
let x = 1;
primitiveMutator(x);
console.log(x); // 1
function objectMutator(val) {
val.prop = val.prop + 1;
}
let obj = { prop: 1 };
objectMutator(obj);
console.log(obj.prop); // 2
```
* 基本类型同样的值永远相等(除了奇怪的 NaN ),看看这里:
```
const first = "abc" + "def";
const second = "ab" + "cd" + "ef";
console.log(first === second); // true
```
* 然而,非基本类型的值即便内容同样,但也不相等,看看这里:
```
const obj1 = { name: "Intrinsic" };
const obj2 = { name: "Intrinsic" };
console.log(obj1 === obj2); // false
// Though, their .name properties ARE primitives:
console.log(obj1.name === obj2.name); // true
```
* 对象扮演了一个 JavaScript 语言的基本角色,它们被处处使用,它们常被用在键值对的存储。然而这样使用有一个很大的限制:在 symbols 诞生以前,对象的键只能是字符串。假如咱们试着使用一个非字符串当作对象的键,就会被转换为字符串,以下所示:
```
const obj = {};
obj.foo = 'foo';
obj['bar'] = 'bar';
obj[2] = 2;
obj[{}] = 'someobj';
console.log(obj);
// { '2': 2, foo: 'foo', bar: 'bar',
'[object Object]': 'someobj' }
```
> 注意:稍微离一下题,Map 数据结构被建立的目的就是为了应对存储键值对中,键不是字符串的状况。
### symbols 是什么?
* 如今咱们知道了什么是基本类型,终于准备好如何定义什么是 symbols 了。symbols 是一种没法被重建的基本类型。这时 symbols 有点相似与对象建立的实例互相不相等的状况,但同时 symbols 又是一种没法被改变的基本类型数据。这里有一个例子:
```
const s1 = Symbol();
const s2 = Symbol();
console.log(s1 === s2); // false
```
* 当你初始化一个带有一个接收可选字符串参数的 symbols 时,咱们能够来 debug 看下,除此以外看看它会否影响自身。
```
const s1 = Symbol('debug');
const str = 'debug';
const s2 = Symbol('xxyy');
console.log(s1 === str); // false
console.log(s1 === s2); // false
console.log(s1); // Symbol(debug)
```
### symbols 做为对象的属性
* symbols 有另外一个很重要的用途,就是用做对象的 key。这儿有一个 symbols 做为对象 key 使用的例子:
```
const obj = {};
const sym = Symbol();
obj[sym] = 'foo';
obj.bar = 'bar';
console.log(obj); // { bar: 'bar' }
console.log(sym in obj); // true
console.log(obj[sym]); // foo
console.log(Object.keys(obj)); // ['bar']
```
* 咱们注意到使用 Object.keys() 并无返回 symbols,这是为了向后兼容性的考虑。老代码不兼容 symbols,所以古老的 Object.keys() 不该该返回 symbols。
* 看第一眼,咱们可能会以为 symbols 这个特性很适合做为对象的私有属性,许多其余语言都要相似的类的隐藏属性,这一直被认为是 JavaScript 的一大短板。不幸的是,仍是有可能经过 symbols 来取到对象的值,甚至都不用试着获取对象属性就能够获得对象 key,例如,经过 Reflect.ownKeys() 方法就能够获取全部的 key,包括 字符串和 symbols,以下所示:
```
function tryToAddPrivate(o) {
o[Symbol('Pseudo Private')] = 42;
}
const obj = { prop: 'hello' };
tryToAddPrivate(obj);
console.log(Reflect.ownKeys(obj));
// [ 'prop', Symbol(Pseudo Private) ]
console.log(obj[Reflect.ownKeys(obj)[1]]); // 42
```
> 注意:如今已经有一个旨在解决 JavaScript 私有属性的提案,叫作 [Private Fields](https://github.com/tc39/proposal-class-fields#private-fields),尽管这并不会使全部的对象受益,它仍然对对象的实例有用,Private Fields 在 Chrome 74版本可用。
### 阻止对象属性名冲突
* symbols 可能对对象的私有属性没有直接好处,可是它有另一个用途,它在不知道对象原有属性名的状况下,扩展对象属性颇有用。
* 考虑一下当两个不一样的库要读取对象的一些原始属性时,或许它们都想要相似的标识符。若是只是简单的使用字符串 id 做为 key,这将会有很大的风险,由于它们的 key 彻底有可能相同。
```
function lib1tag(obj) {
obj.id = 42;
}
function lib2tag(obj) {
obj.id = 369;
}
```
* 经过使用 symbols,不一样的库在初始化的时候生成其所需的 symbols,而后就能够在对象上任意赋值。
```
const library1property = Symbol('lib1');
function lib1tag(obj) {
obj[library1property] = 42;
}
const library2property = Symbol('lib2');
function lib2tag(obj) {
obj[library2property] = 369;
}
```
* 这方面 symbols 的确对 JavaScript 有用。而后你或许会奇怪,不一样的库进行初始化的时候为何不使用随机字符串,或者使用命名空间呢?
```
const library1property = uuid(); // random approach
function lib1tag(obj) {
obj[library1property] = 42;
}
const library2property = 'LIB2-NAMESPACE-id'; // namespaced approach
function lib2tag(obj) {
obj[library2property] = 369;
}
```
* 你是对的,这种方法确实相似于 symbols 的这一做用,除非两个库使用相同的属性名,那就会有被覆写的风险。
* 机敏的读者已经发现这两种方案的效果并不彻底相同。咱们独有的属性名仍然有一个缺点:它们的 key 很容易被找到,尤为是当代码进行递归或者系列化对象,考虑以下的例子:
```
const library2property = 'LIB2-NAMESPACE-id'; // namespaced
function lib2tag(obj) {
obj[library2property] = 369;
}
const user = {
name: 'Thomas Hunter II',
age: 32
};
lib2tag(user);
JSON.stringify(user);
// '{"name":"Thomas Hunter II","age":32,"LIB2-NAMESPACE-id":369}'
```
* 假如咱们使用 symbols 做为属性名,json 的输出将不会包含 symbols,这是为何呢?由于 JavaScript 支持 symbols,并不意味着 json 规范也会跟着修改。json 只容许字符串做为 key,JavaScript 并无试图让 json 输出 symbols。
* 咱们能够简单的经过 Object.defineProperty() 来调整对象字符串输出的 json。
```
const library2property = uuid(); // namespaced approach
function lib2tag(obj) {
Object.defineProperty(obj, library2property, {
enumerable: false,
value: 369
});
}
const user = {
name: 'Thomas Hunter II',
age: 32
};
lib2tag(user);
// '{"name":"Thomas Hunter II",
"age":32,"f468c902-26ed-4b2e-81d6-5775ae7eec5d":369}'
console.log(JSON.stringify(user));
console.log(user[library2property]); // 369
```
* 相似于 symbols,对象经过设置 enumerable 标识符来隐藏字符串 key,它们都会被 Object.keys() 隐藏掉,并且都会被 Reflect.ownKeys() 展现出来,以下所示:
```
const obj = {};
obj[Symbol()] = 1;
Object.defineProperty(obj, 'foo', {
enumberable: false,
value: 2
});
console.log(Object.keys(obj)); // []
console.log(Reflect.ownKeys(obj)); // [ 'foo', Symbol() ]
console.log(JSON.stringify(obj)); // {}
```
* 在这一点上,咱们至关于重建了 symbols,咱们的隐藏字符串和 symbols 都被序列化器隐藏了,属性也均可以经过 Reflect.ownKeys() 来获取,所以他们并不算私有属性。假设咱们使用命名空间、随机字符串等字符串做为对象的属性名,咱们就能够避免多个库重名的风险。
* 可是仍然有一点细微的不一样,字符串是不可变的,而 symbols 能够保证永远惟一,所以仍然有可能会有人生成重名的字符串。从数学意义上 symbols 提供了一个字符串没有的优势。
* 在 Node.js 里面,当检测一个对象(例如使用 console.log()),假如对象上的一个方法叫作 inspect,当记录对象时,该方法会被调用并输出。你能够想象,这种行为并非每一个人都会这样作,被用户建立的 inspect 方法常常会致使命名冲突,如今 require('util').inspect.custom 提供的 symbol 能够被用在函数上。inspect 方法在 Node.js v10 被放弃,在 v11 版直接被忽略。如今没人能够突然就改变 inspect 方法的行为了。
### 模拟私有属性
* 这里有一个在对象上模拟私有属性的有趣的尝试。使用了另外一个 JavaScript 的新特性:proxy。proxy 会包住一个对象,而后咱们就能够跟这个对象进行各类各样的交互。
* proxy 提供了不少种拦截对象行为的方式。这里咱们感兴趣的是读取对象属性的行为。我并不会完整的解释 proxy 是如何工做的,因此若是你想要了解的更多,能够查看咱们的另外一篇文章:[JavaScript Object Property Descriptors, Proxies, and Preventing Extension](https://medium.com/intrinsic/javascript-object-property-descriptors-proxies-and-preventing-extension-1e1907aa9d10)
* 咱们可使用代理来展现对象上可用的属性。这里咱们先建立一个 proxy 来隐藏两个属性,一个是字符串 _favColor,另外一个是 symbol 叫 favBook。
```
let proxy;
{
const favBook = Symbol('fav book');
const obj = {
name: 'Thomas Hunter II',
age: 32,
_favColor: 'blue',
[favBook]: 'Metro 2033',
[Symbol('visible')]: 'foo'
};
const handler = {
ownKeys: (target) => {
const reportedKeys = [];
const actualKeys = Reflect.ownKeys(target);
for (const key of actualKeys) {
if (key === favBook || key === '_favColor') {
continue;
}
reportedKeys.push(key);
}
return reportedKeys;
}
};
proxy = new Proxy(obj, handler);
}
console.log(Object.keys(proxy)); // [ 'name', 'age' ]
console.log(Reflect.ownKeys(proxy)); // [ 'name', 'age', Symbol(visible) ]
console.log(Object.getOwnPropertyNames(proxy)); // [ 'name', 'age' ]
console.log(Object.getOwnPropertySymbols(proxy)); // [Symbol(visible)]
console.log(proxy._favColor); // 'blue'
```
* 发现 _favColor 属性很简单,只须要阅读源码便可,另外,动态的 key 能够经过暴力破解方式得到(例如前面的 uuid 例子)。可是对 symbol 属性,若是你没有直接的引用,是没法访问到 `Metro 2033` 这个值的。
* Node.js 备注:有一个特性能够破解私有属性,这个特性不是 JavaScript 的语言特性,也不存在与其余场景,例如 web 浏览器。当使用 proxy 时,你能够获取到对象隐藏的属性。这里有一个破解上面私有属性的例子:
```
const [originalObject] = process
.binding('util')
.getProxyDetails(proxy);
const allKeys = Reflect.ownKeys(originalObject);
console.log(allKeys[3]); // Symbol(fav book)
```
* 咱们如今要么修改全局的 Reflect 对象,要么修改 util 的方法绑定,来组织他们被某个 Node.js 实例访问。但这是一个无底洞,若是你有兴趣深挖,能够看这篇文章:[Protecting your JavaScript APIs](https://medium.com/intrinsic/protecting-your-javascript-apis-9ce5b8a0e3b5)