本文深入解析 JavaScript ES6 引入的 Symbol 类型。通过对比值类型与引用类型,阐述 Symbol 作为唯一标识符在对象属性键中的核心作用。文章详细说明了如何利用 Symbol 避免多库开发时的命名冲突,并探讨其作为非真正私有属性的局限性,包括 JSON 序列化限制及 Reflect.ownKeys 的访问方式。最后结合 Proxy 对象演示模拟私有属性的技巧及其在 Node.js 中的潜在风险。
JavaScript Symbol 类型概述
Symbol 是 ES6 引入的一种全新数据类型,它为 JavaScript 带来了显著优势,特别是在处理对象属性时。那么,Symbol 究竟能实现字符串无法完成的任务吗?
在深入探讨 Symbol 之前,我们需要先了解一些 JavaScript 的基础特性,这些特性常被许多开发者忽略。
JavaScript 数据类型背景
背景知识
JavaScript 中的数据类型主要分为两类:值类型和引用类型。
值类型(基本类型)包括:数值型(Number)、字符串型(String)、布尔型(Boolean)、null 以及 undefined。
引用类型(类)包括:函数、对象、数组等。
理解值类型:变量间的赋值意味着开辟新的内存空间,将原变量的值复制一份存入新内存。此后,两个变量的值互不干扰。例如:
var a = 10; //开辟一块内存空间保存变量a的值“10”;
var b = a; //给变量 b 开辟一块新的内存空间,将 a 的值 “10” 赋值一份保存到新的内存里;
//a 和 b 的值以后无论如何变化,都不会影响到对方的值;
部分语言(如 C)区分引用传递和值传递,JavaScript 也遵循类似逻辑,具体取决于传递的数据类型。若传递的是值类型,函数内部重新赋值不会影响调用处的变量;但若修改的是引用类型,调用处的对象也会随之改变。
理解引用类型:变量间的赋值仅是指针(引用地址)的交换,而非复制对象本身。对象在内存中依然只有一个,只是增加了多个指向它的引用。例如:
var a = { x: 1, y: 2 }; //需要开辟内存空间保存对象,变量 a 的值是一个地址,这个地址指向保存对象的空间;
var b = a; // 将a 的指引地址赋值给 b,而并非复制一给对象且新开一块内存空间来保存;
// 这个时候通过 a 来修改对象的属性,则通过 b 来查看属性时对象属性已经发生改变;
除神秘的 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
// 但是,它们的 .name 属性是基本类型:
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' }
Symbol 的定义与特性
Symbol 的定义
Symbol() 函数返回 symbol 类型的值,该类型包含静态属性和静态方法。其静态属性暴露若干内置成员,静态方法暴露全局 symbol 注册表。虽然类似内建对象类,但它不是完整的构造函数,不支持使用 new 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']
乍看之下,这似乎允许在对象上创建私有属性。其他编程语言在类中支持私有属性,而 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 版本开始可用,但并非适用于所有对象。
代码部署后可能存在的 BUG 无法实时获知,事后为解决这些 BUG 需花费大量时间进行日志调试。此处推荐一款好用的 BUG 监控工具 Fundebug。
防止属性名称冲突
防止属性名称冲突
Symbol 虽不能直接为 JavaScript 对象提供真正的私有属性,但它在另一场景下极具价值:当不同库希望向对象添加属性而不引发名称冲突时,Symbol 非常有用。
虽然 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 属性。
const library2property = "f468c902-26ed-4b2e-81d6-5775ae7eec5d"; // namespaced approach
function lib2tag(obj) {
Object.defineProperty(obj, library2property, {
enumerable: false,
value: 369
});
}
const user = {
name: "Thomas Hunter II",
age: 32
};
lib2tag(user);
console.log(user); // {name: "Thomas Hunter II", age: 32, f468c902-26ed-4b2e-81d6-5775ae7eec5d: 369}
console.log(JSON.stringify(user)); // {"name":"Thomas Hunter II","age":32}
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 中的 inspect 与模拟私有属性
在 Node.js 中,检查对象时(例如使用 console.log() ),如果遇到名为 inspect 的对象上的方法,将调用该函数,并将打印内容。可以想象,这种行为并不是每个人都期望的,通常命名为 inspect 的方法经常与用户创建的对象发生冲突。 现在 Symbol 可用来实现这个功能,并且可以在 equire(‘util').inspect.custom 中使用。inspect 方法在 Node.js v10 中被废弃,在 v1 1 中完全被忽略, 现在没有人会偶然改变检查的行为。 模拟私有属性
这里有一种有趣的方法,可用于模拟对象上的私有属性。这种方法将利用另一个 JavaScript 特性: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 中有一个功能会破坏代理的隐私。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 实例中使用。
以上所述是小编给大家介绍的 JavaScript 为什么要有 Symbol 类型详解整合,希望对大家有所帮助。如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对本站网站的支持!