- 原文地址:JavaScript Symbols: But Why?
- 原文做者:Thomas Hunter II
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:xionglong58
- 校对者:EdmondWang, Xuyuey
做为最新的基本类型,Symbol 为 JavaScript 语言带来了不少好处,特别是当其用在对象属性上时。可是,相比较于 String 类型,Symbol 有哪些 String 没有的功能呢?javascript
在深刻探讨 Symbol 以前,让咱们先看看一些许多开发人员可能都不知道的 JavaScript 特性。前端
JavaScript 中有两种数据类型:基本数据类型和对象(对象也包括函数),基本数据类型包括简单数据类型,好比 number(从整数到浮点数,从 Infinity 到 NaN 都属于 Number 类型)、boolean、string、undefined
、null
(注意尽管 typeof null === 'object'
,null
仍然是一个基本数据类型)。java
基本数据类型的值是不能够改变的,即不能更改变量的原始值。固然能够从新对变量进行赋值。例如,代码 let x = 1; x++;
,虽然你经过从新赋值改变了变量 x
的值,可是变量的原始值 1
仍没有被改变。node
一些语言,好比 C 语言,有按引用传递和按值传递的概念。JavaScript 也有相似的概念,它是根据传递数据的类型推断出来的。若是将值传入一个函数,则在函数中从新对它赋值不会修改它在调用位置的值。可是,若是你修改的是基本数据的值,那么修改后的值会在调用它的地方被修改。android
考虑下面的例子:ios
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
除外)老是与另外一个具备相同值的基本数据类型彻底相等。以下:git
const first = "abc" + "def";
const second = "ab" + "cd" + "ef";
console.log(first === second); // true
复制代码
然而,构造两个值相同的非基本数据类型则获得不相等的结果。咱们能够看到发生了什么:github
const obj1 = { name: "Intrinsic" };
const obj2 = { name: "Intrinsic" };
console.log(obj1 === obj2); // false
// 可是,当二者的 .name 属性为基本数据类型时 console.log(obj1.name === obj2.name); // true
复制代码
对象在 JavaScript 中扮演着重要的角色,几乎全部地方能够见到它们的身影。对象一般是键/值对的集合,然而这种形式的最大限制是:对象的键只能是字符串,直到 Symbol 出现这一限制才获得解决。若是咱们使用非字符串的值做为对象的键,该值会被强制转换成字符串。在下面的程序中能够看到这种强制转换:web
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
数据结构的部分缘由是为了在键不是字符串的状况下容许键/值方式存储。编程
如今既然咱们已经知道了基本数据类型是什么,也就终于能够定义 Symbol。Symbol 是不能被从新建立的基本数据类型。在这种状况下,Symbol 相似于对象,由于对象建立多个实例也将致使不彻底相等的值。可是,Symbol 也是基本数据类型,由于它不能被改变。下面是 Symbol 用法的一个例子:
const s1 = Symbol();
const s2 = Symbol();
console.log(s1 === s2); // false
复制代码
当实例化一个 symbol 值时,有一个可选的首选参数,你能够赋值一个字符串。此值用于调试代码,不会真正影响 symbol 自己。
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 还有另外一个重要的用法,它们能够被看成对象中的键!下面是一个在对象中使用 symbol 做为键的例子:
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']
复制代码
注意,symbols 键不会被在 Object.keys()
返回。这也是为了知足向后兼容性。旧版本的 JavaScript 没有 symbol 数据类型,所以不该该从旧的 Object.keys()
方法中被返回。
乍一看,这就像是能够用 symbols 在对象上建立私有属性!许多其余编程语言能够在其类中有私有属性,而 JavaScript 却遗漏了这种功能,长期以来被视为其语法的一种缺点。
不幸的是,与该对象交互的代码仍然能够访问对象那些键为 symbols 的属性。甚至是在调用代码本身没法访问 symbol 的状况下也有可能发生。 例如,Reflect.ownKeys()
方法可以获得一个对象的全部键的列表,包括字符串和 symbols:
function tryToAddPrivate(obj) {
obj[Symbol('Pseudo Private')] = 42;
}
const obj = { prop: 'hello' };
tryToAddPrivate(obj);
console.log(Reflect.ownKeys(obj));
console.log(obj[Reflect.ownKeys(obj)[1]]); // 42
复制代码
注意:目前有些工做旨在处理在 JavaScript 中向类添加私有属性的问题。这个特性就是 Private Fields 虽然这不会对全部对象都有好处,但会对类实例的对象有好处。Private Fields 从 Chrome 74 开始可用。
Symbol 类型可能会对获取 JavaScript 中对象的私有属性不利。它们之因此有用的另外一个理由是,当不一样的库但愿向对象添加属性时 symbols 能够避免命名冲突的风险。
若是有两个不一样的库但愿将某种元数据附加到一个对象上,二者可能都想在对象上设置某种标识符。仅仅使用两个字符串类型的 id
做为键来标识,多个库使用相同键的风险就会很高。
function lib1tag(obj) {
obj.id = 42;
}
function lib2tag(obj) {
obj.id = 369;
}
复制代码
应用 symbols,每一个库均可以经过实例化 Symbol 类生成所需的 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(); // 随机方法
function lib1tag(obj) {
obj[library1property] = 42;
}
const library2property = 'LIB2-NAMESPACE-id'; // namespaced approach
function lib2tag(obj) {
obj[library2property] = 369;
}
复制代码
你有多是正确的,上面的两种方法与使用 symbols 的方法很类似。除非两个库使用了相同的属性名,不然不会有冲突的风险。
在这一点上,机灵的读者会指出,这两种方法并不彻底相同。具备惟一名称的属性名仍然有一个缺点:它们的键很是容易找到,特别是当运行代码来迭代键或以其余方式序列化对象时。请考虑如下示例:
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}'
复制代码
若是咱们为对象的属性名使用了一个 symbol,那么 JSON 的输出将不包含 symbol 对应的值。为何会这样?由于仅仅是 JavaScript 支持了 symbols,并不意味着 JSON 规范也改变了!JSON 只容许字符串做为键,而 JavaScript 不会尝试在最终的 JSON 负载中呈现 symbol 属性。
咱们能够经过使用 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
复制代码
经过将字符串键的可枚举描述符设置为 false 来“隐藏”字符串键的行为很是相似于 symbol 键。它们经过 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()
方法提取,所以实际上并非私有的。假设咱们对字符串属性使用某种命名空间/随机值,那么咱们就消除了多个库意外发生命名冲突的风险。
可是,仍然有一个微小的差别。因为字符串是不可变的,Symbol 始终保证是惟一的,所以仍有可能生成相同的字符串并产生冲突。从数学角度来讲,意味着 symbols 确实提供了咱们没法从字符串中得到的好处。
在 Node.js 中,检查对象时(例如使用 console.log()
),若是遇到对象上名为 inspect
的方法,则调用该函数,并将输出表示成对象的日志。能够想象,这种行为并非每一个人都指望的,一般命名为 inspect
的方法常常与用户建立的对象发生冲突。如今有 symbol 可用来实现这个功能,而且能够在 require('util').inspection.custom 中使用。inspect
方法在 Node.js v10 中被废弃,在 v11 中彻底被忽略。如今没有人会由于意外改变 inspect 的行为!
这里有一个有趣的方法,咱们可使用它来模拟对象上的私有属性。这种方法将利用另外一个 JavaScript 的特性:proxy。proxy 本质上是封装了一个对象,并容许咱们与该对象进行不一样的交互。
proxy 提供了许多方法来拦截对对象执行的操做。咱们所感兴趣的是在尝试读取对象的键时,proxy 会有哪些动做。我不会去详细解释 proxy 是如何工做的,若是你想了解更多信息,请查看咱们的另外一篇文章:JavaScript Object Property Descriptors, Proxies, and Preventing Extension.
咱们可使用 proxy 来谎报对象上可用的属性。在本例中,咱们将建立一个 proxy,它用于隐藏咱们的两个已知隐藏属性,一个是字符串 _favColor
,另外一个是分配给 favBook
的 symbol:
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
字符串很简单:只需读取库的源代码便可。此外,动态键能够(例如以前讲的 uuid
示例)能够经过暴力找到。可是,若是不是直接引用 symbol,任何人都没法从 proxy
对象中访问到值 metro 2033
。
Node.js 声明:Node.js 中的一个特性破坏了 proxy 的隐私性。此功能不存在于 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。
这篇文章是我和 Thomas Hunter II 一块儿写的。我在一家名为 Intricsic 的公司工做(顺便说一下,咱们正在招聘!),专门编写用于保护 Node.js 应用程序的软件。咱们目前有一个产品应用 Least Privilege 模型来保护应用程序。咱们的产品主动保护 Node.js 应用程序不受攻击者的攻击,并且很是容易实现。若是你正在寻找保护 Node.js 应用程序的方法,请在 hello@inherin.com 上联系咱们。
横幅照片的做者 Chunlea Ju
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。