symbol是最新的JavaScript特性,它为语言带来了一些好处,并且在用作对象属性时特别有用。但是,它们可以为我们做些弦乐器不能做的事情?
在我们过多地探索symbol之前,让我们先来看许多开发人员可能不知道的一些JavaScript功能。
背景
JavaScript本质上有两种类型的值。第一种类型是原始值,第二种类型是对象(还包括函数)。原始值包括简单的值类型,如数字(其中包括从整数浮Infinity
到NaN
),布尔值,字符串,undefined
和null
(注:即使typeof null === 'object'
,null
是一个仍然是原始值)。
原始值也是不可变的。它们无法更改。当然,可以重新分配已分配基本类型的变量。例如,在编写代码时let x = 1; x++;
,您已经重新分配了变量x
。但是,您尚未更改的原始数字值1
。
某些语言(例如C)具有“按引用传递”和“按值传递”的概念。JavaScript有点也有这个概念,但是它是根据传递的数据类型来推断的。如果您曾经将值传递给函数,则重新分配该值不会修改调用位置中的值。但是,如果您修改非原始值,修改后的值将也被修改的地方已经从调用。
考虑以下示例:
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语言中起着基本作用。它们无处不在。它们通常用作键/值对的集合。但是,以这种方式使用它们是一个很大的限制:在存在symbol之前,对象键只能是字符串。如果我们尝试将非字符串值用作对象的键,则该值将被强制为字符串。我们可以在这里看到此功能:
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也是原始的,因为它不能被突变。这是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)
symbol作为对象属性
symbol还有另一个重要用途。它们可以用作对象中的键!这是在对象中使用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']
请注意,在的结果中如何不返回它们Object.keys()
。同样,这是为了向后兼容。旧代码不了解symbol,因此不应从旧Object.keys()
方法中返回此结果。
乍一看,这几乎就像symbol可用于在对象上创建私有属性一样!许多其他编程语言在其类中都具有隐藏的属性,这种遗漏早已被视为JavaScript的缺点。
不幸的是,与该对象交互的代码仍然有可能访问其键为symbol的属性。当调用代码确实这是在情况下甚至有可能不能访问symbol本身。例如,该Reflect.ownKeys()
方法能够获取对象上所有键的列表,包括字符串和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受益。但是,由于另一个原因,它们是有益的。在完全不同的库希望向对象添加属性而又不存在名称冲突风险的情况下,它们很有用。
考虑两个不同的库希望将某种元数据附加到对象的情况。也许他们俩都想在对象上设置某种标识符。通过简单地使用两个字符串id
作为密钥,存在很大的风险,多个库将使用相同的密钥。
function lib1tag(obj) {
obj.id = 42;
}
function lib2tag(obj) {
obj.id = 369;
}
通过使用symbol,每个库都可以在实例化时生成其所需的symbol。然后,可以在遇到对象时检查对象上的symbol并将其设置为对象。
const library1property = Symbol('lib1');
function lib1tag(obj) {
obj[library1property] = 42;
}
const library2property = Symbol('lib2');
function lib2tag(obj) {
obj[library2property] = 369;
}
因此,symbol似乎确实对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;
}
好吧,你会是对的。该方法实际上与使用symbol的方法非常相似。除非两个库选择使用相同的属性名称,否则不会有重叠的风险。
在这一点上,精明的读者会指出,这两种方法还不完全相同。我们具有唯一名称的属性名称仍然有一个缺点:它们的键很容易找到,特别是当代码运行以迭代键或以其他方式序列化对象时。考虑以下示例:
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输出将不包含其值。这是为什么?好吧,仅因为JavaScript获得了对symbol的支持并不意味着JSON规范已经改变!JSON仅允许将字符串作为键,而JavaScript不会尝试表示最终JSON有效负载中的symbol属性。
我们可以轻松地解决以下问题:我们的库对象字符串污染了JSON输出Object.defineProperty()
:
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
通过将enumerable
描述符的描述符设置为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)); // {}
至此,我们几乎重新创建了symbol。隐藏的字符串属性和symbol都从序列化程序中隐藏。可以使用该Reflect.ownKeys()
方法提取这两个属性,因此这两个属性实际上不是私有的。假设我们为属性名称的字符串版本使用某种名称空间/随机值,那么我们消除了多个库意外发生名称冲突的风险。
但是,仍然只有一个微小的差异。由于字符串是不可变的,并且symbol始终保证是唯一的,因此有人仍然有可能生成每种可能的字符串组合并产生冲突。从数学上讲,这意味着symbol确实提供了我们从字符串中无法获得的好处。
在Node.js中,检查一个对象(例如使用console.log()
)时,如果inspect
遇到名为对象的方法,则会调用该函数并将输出用作该对象的记录表示形式。您可以想象,并非所有人都期望这种行为,并且通用名称的inspect
方法通常会与用户创建的对象发生冲突。现在有一个可用于实现此功能的symbol,该symbol可在访问require('util').inspect.custom
。该inspect
方法在Node.js v10中已弃用,在v11中被完全忽略。现在,没有人会偶然改变检查的行为!
模拟私有属性
这是一种有趣的方法,可用于模拟对象的私有属性。这种方法将利用今天提供给我们的另一个JavaScript功能:代理。代理实质上是包装一个对象,并允许我们与该对象进行各种交互。
代理提供了许多方法来拦截对对象执行的操作。我们感兴趣的一种会在尝试读取对象键时发生影响。我不会完全解释代理的工作原理,因此,如果您想了解更多信息,请查看我们的其他文章:JavaScript对象属性描述符,代理和防止扩展。
然后,我们可以使用代理来确定对象上哪些属性可用。在这种情况下,我们将设计一个代理来隐藏我们的两个已知的隐藏属性,一个是字符串_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
可以通过蛮力找到动态键(例如,之前的示例)。但是,如果没有直接引用该symbol,则任何人都无法从该proxy
对象访问“ Metro 2033”值。
Node.js警告:Node.js中有一项功能可以打破代理的隐私。此功能在JavaScript语言本身中并不存在,并且不适用于其他情况,例如Web浏览器。给定代理后,它便可以访问基础对象。这是使用此功能破坏上面的私有属性示例的示例:
const [originalObject] = process
.binding('util')
.getProxyDetails(proxy);
const allKeys = Reflect.ownKeys(originalObject);
console.log(allKeys[3]); // Symbol(fav book)
现在,我们需要修改全局Reflect
对象或修改util
流程绑定,以防止它们在特定的Node.js实例中使用。
网友评论