javascript Symbols
javascript Symbols
Symbols 是最新的js原始类型,它带来了一些好处,尤其上司当做对象属性时特别有用。但是它提供的什么是String做不到的呢?
在我们开始深入了解Symbols之前让我们先来看看有哪些细节是开发者们不太容易注意到的。
背景
在js中有两种数据类型,原始类型和引用数据类型(原文这里用的objects); 原始类型包括numbers(包括整数,浮点数,Infinity,NaN),布尔值,字符串,undefined
,还有null
(虽然 typeof null === 'object'
但null
仍然是一个原始类型)
解百纳类型的值是不可变的。他们不能被改变。一个变量被赋值了一个原始类型仍然可以被重新赋值。举例: let x = 1; x++;
.这时候就是把变量x重新赋值了,但是并没有改变1的原始数值。
有些其它语言,比如C,有按引用传递和按值传递的概念。js也有这个概念,虽然它根据要传递的数据类型来推断。当你给一个方法传递原始类型的值时,重新给参数赋值并不会改变传递的值。但当你修改一个不是原始类型的值,传递的值也会被修改.
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
console.log(obj1.name === obj2.name) //true
对象再js中的用途很广泛,它们无处不在。经常作为键值对组合。但是当以这种方式使用的时候会有很大的限制:直到Symbols出现,object的key只能是string字符串。如果我们试图用一个不是字符串的值去当key的话,这个值就会强制转换成string;
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就像是被许多实例化的对象被赋予了不同的值.
const s1 = Symbol();
const s2 = Symbol();
console.log(s1 === s2); //false
当初始化一个symbol的时候,可以传递一个字符串当做可选的参数。这个值将会用做debug的标识,并不是真实的影响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)
Symbol 当做对象属性
Symbol有另一个重要的用途.它能用来当做object的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()
并不返回Symbol,这也是处于向后兼容的目的.旧代码并不认识symbol,所以Objects.keys()
方法也不应该返回他们;
一眼看上去,symbol可以用来创建object的一个私有属性。许多其它编程语言在类中都有隐藏属性的概念,长期以来这也被视为js的一个缺点.
不幸的是,与对象交互的方法仍有可能访问其key为symbol的值。甚至在调用代码尚未访问符号本身的情况下也有可能。Reflect.ownKeys()
就是一个例子,这个方法可以列出object的所有属性,string的和symbol的。
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” ,尽管这不会使所有对象受益,但它将使作为类实例的对象受益。专用字段自Chrome 74起可用
防止命名冲突
Symbol可能无法为JavaScript提供对象私有属性的好处,但是它还有另一个好处,在不同的库希望向同一个object添加属性而又不希望引起命名冲突的问题的情况下很有用。
想象一下这样一个场景,两个不同的库希望将要某种元数据附加到同一个对象的情况。比如他们都想在对象上设置一个标识的key,有很大风险许多库都会用id
作为标识;
function lib1tag(obj){ obj.id = 42;}
function lib2tag(obj){ obj.id = 24;}
用symbol的话就解决了这个问题,每个库产生自己的symbol然后再添加到object上。
const lib1property = Symbol('lib1');
function lib1tag(obj){ obj[lib1property] = 42; }
const lib2property = Symbol('lib2');
function lib2tag(obj){ obj[lib2property] = 24; }
是不是感觉好多了。
你也许会想,为什么每个库不能生成一个随机的字符串或者使用命名空间呢?
const lib1property = uuid();
function lib1tag(obj){ obj[lib1property] = 42; }
const lib2property = 'LIB2-NAMESPACE-id';
function lib2tag(obj){ obj[lib2property] = 24; }
你是对的,这种方法实际上与用symbol的方法非常类似。除非两个库使用相同的属性名称,否则不会有冲突的风险。
在这一点上,精明的读者会指出,这两种方法并不完全相同。我们使用的唯一的属性名称仍然有个缺点:它很容易被发现,尤其是当代码在迭代keys或者序列号object的时候。例子:
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当做object的key,序列化的结果就不会出现我们设置的名称了。为什么呢?因为js支持symbol并不意味着JSON规范也改变了。JSON只允许用字符串当做key,JavaScript并不会尝试用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}'
console.log(JSON.stringify(user));
console.log(user[library2property]); // 369
通过设置 enumerable
属性,字符串key就被“隐藏”了,这样就和用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)); // {}
至此,我们几乎重新创建了Symbol。隐藏的字符串属性和符合都从序列化程序中隐藏。并且他们都可以用 Reflect.ownKeys()
方法来提取,这两个属性也不是私有的。假设我们用命名空间/随机的字符串当做属性名称的话,我们就避免了命名冲突的风险.
但还是有点小区别。由于字符串是不可变的,并且Symbol是唯一的,因此仍有可能有人生成相同的组合并产生冲突.从数学上讲,Symbol确实提供了字符串无法提供的好处。
在Nodejs中,当检测对象时(比如 console.log()
),如果遇到了对象上有个方法叫 inspect
的话,这个方法就会被调用,并且输出结果会表示为对象的结果.正如你所想的那样,这个特性不是每个人都期望的,并且这个通用的名称 inspect
经常会与开发人员的名称冲突。现在有一个可用的Symbol实现的方法。 require('util').inspect.custom
. inspect
方法以及在Nodejs V10中设为不推荐,并且在V11中被弃用了。现在也不会有这种情况产生了。
模拟私有属性
这有一种有趣的方法,我们可以在object上模拟一个私有的属性。这个方法会用到js的另一个特性: Proxy
; Proxy
实质上就是包了一层对象,并允许我们与这个对象进行交互。
proxy 提供了多种方法来拦截对object执行的操作。现在我们想用的就是在读取object的key的时候会产生效果。我并不打算介绍proxy是如何工作的,如果有兴趣的话可以去看我们的另一篇文章:JavaScript Object Property Descriptors, Proxies, and Preventing Extension
我们用代理来确定对象上哪些属性可用。在这个例子中我们将设计一个代理来隐藏我们的两个已知隐藏属性,一个是字符串 _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
字符串很简单:只需阅读库的源代码即可。另外,可以通过蛮力找到动态键(例如,之前的uuid示例)。但是没有直接引用该符号,没有人可以从proxy object访问“ Metro 2033”值
Nodejs 提示:Node.js中有一个功能可以打破代理的隐私,该功能在JavaScript语言中并不存在,并且在其它情况下(比如在浏览器中)也不适用。给定代理后,它就可以访问基础对象。
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/javascript-symbols-but-why-6b02768f4a5c