1. JavaScript 的 Symbols 有什么用
Symbols 与其他 JavaScript 原语不同,其保证唯一性。
当开发者使用 Symbol('description')
创建 Symbols 时,其值永远不会与任何其他 Symbols 相同,即使是使用相同描述创建的 Symbols,这种独特性使其在特定用例中非常强大。
const symbol1 = Symbol("description");
const symbol2 = Symbol("description");
console.log(symbol1 === symbol2);
// 输出 false
Symbols 的真正魅力在于对象处理,与字符串或数字不同,Symbols 可以用作属性键,而不会与现有属性发生冲突。这使其在向对象添加功能而不影响现有代码方面非常有用。
const metadata = Symbol("elementMetadata");
function attachMetadata(element, data) {
element[metadata] = data;
return element;
}
const div = document.createElement("div");
const divWithMetadata = attachMetadata(div, { lastUpdated: Date.now() });
console.log(divWithMetadata[metadata]);
// {lastUpdated: 1684244400000}
同时,当使用 Symbol 作为属性键时,其不会显示在 Object.keys()
或普通 for...in 循环中。但是,开发者仍可以通过 Object.getOwnPropertySymbols()
访问这些属性。
const nameKey = Symbol("name");
const person = {
[nameKey]: "Alex",
city: "London",
};
console.log(Object.getOwnPropertySymbols(person));
// [Symbol(name)]
console.log(person[nameKey]);
// 输出'Alex'
2. Symbol.for 创建全局 Symbol 注册表
全局 Symbol 注册表为 Symbol 的使用增加了另一个维度。虽然普通的 Symbol 始终是唯一的,但有时开发者确实需要在代码的不同部分之间共享 Symbol,此时就是 Symbol.for()
的用武之地。
// 使用 Symbol.for() 在不同模块之间共享 Symbol
const PRIORITY_LEVEL = Symbol.for("priority");
const PROCESS_MESSAGE = Symbol.for("processMessage");
function createMessage(content, priority = 1) {
const message = {
content,
[PRIORITY_LEVEL]: priority,
[PROCESS_MESSAGE]() {
return `Processing: ${this.content} (Priority: ${this[PRIORITY_LEVEL]})`;
},
};
return message;
}
function processMessage(message) {
if (message[PROCESS_MESSAGE]) {
return message[PROCESS_MESSAGE]();
}
throw new Error("Invalid message format");
}
const msg = createMessage("Hello World", 2);
console.log(processMessage(msg));
// 输出 "Processing: Hello World (Priority: 2)"
console.log(Symbol.for("processMessage") === PROCESS_MESSAGE);
// 输出 true
// 常规 Symbols 永远不相等
console.log(Symbol("processMessage") === Symbol("processMessage")); // false
Symbol.for
可以保证多次调用返回的值完全相同,因此也经常用于多个模块之间的内容共享。
// 模块 A 的内容
const SHARED_KEY = Symbol.for("app.sharedKey");
const moduleA = {
[SHARED_KEY]: "secret value",
};
// 模块 B 的内容,且在不同的文件中
const sameKey = Symbol.for("app.sharedKey");
console.log(SHARED_KEY === sameKey);
// 输出 true
console.log(moduleA[sameKey]);
// 输出'secret value'
// 常规 Symbols 多次调用永远不同‘
const regularSymbol = Symbol("regular");
const anotherRegular = Symbol("regular");
console.log(regularSymbol === anotherRegular); // false
Symbol.for()
创建的 Symbol 的作用类似于共享密钥,应用程序可以通过相同的名称共享。而常规 Symbol 始终唯一,即使具有相同的名称。
3. 使用 Symbols 修改 JavaScript 内置行为
JavaScript 提供了众多内置 Symbol 让开发者修改对象在不同情况下的行为方式,相当于语言功能的各种钩子。
一个常见的用例是使用 Symbol.iterator
使对象可迭代,从而可以对对象使用 for...of
循环:
// 添加 Symbol.iterator 让对象可迭代
const tasks = {
items: ["write code", "review PR", "fix bugs"],
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.items.length) {
return { value: this.items[index++], done: false };
}
return { value: undefined, done: true };
},
};
},
};
for (let task of tasks) {
console.log(task);
// 输出值'write code', 'review PR', 'fix bugs'
}
另一个强大的功能是 Symbol.toPrimitive
,其用于控制对象如何转换为数字或字符串等原始值。
const user = {
name: "Alex",
score: 42,
[Symbol.toPrimitive](hint) {
// JavaScript 引擎使用 hint 参数表示类型
// hint 可以是'number', 'string', or 'default'
switch (hint) {
case "number":
return this.score;
case "string":
return this.name;
default:
return `${this.name} (${this.score})`;
// 其他例如 user + ''
}
},
};
console.log(+user);
// + 操作符表示想要数字,输出 42
console.log(`${user}`);
// 模板字符串表示需要字符串, 输出 "Alex"
console.log(user + "");
// `+ 字符串 ` 表示 "Alex (42)"
当然,开发者还可以通过 Symbol.hasInstance
修改 instanceof 的默认行为,比如下面的 JSONArray 对象:
class JSONArray {
constructor() {
this.items = [];
}
// 自定义 instanceof 行为
static [Symbol.hasInstance](instance) {
return instance && typeof instance === "object" && "items" in instance;
}
}
此时,下面代码的 instanceof 将直接输出 true:
const a = { items: [] };
a instanceof JSONArray; // 输出 true
4. 使用 Symbol.species 进行继承控制
在 JavaScript 中使用数组时有时需要限制可以保存的值类型,这时就需要使用专用数组,不过值得注意的是其可能导致 map()
和 filter()
等方法出现意外行为。
const createNumberArray = (...numbers) => {
const array = [...numbers];
array.push = function (item) {
if (typeof item !== "number") {
throw new Error("Only numbers allowed");
}
return Array.prototype.push.call(this, item);
};
// 告诉 JavaScript 引擎使用常规数组方法,例如:map
// 此时 map 派生数组不受影响
Object.defineProperty(array.constructor, Symbol.species, {
get: function () {
return Array;
},
});
return array;
};
const nums = createNumberArray(1, 2, 3);
nums.push(4);
// Works ✅
nums.push("5");
// Error! ❌ (as expected for nums)
const doubled = nums.map((x) => x * 2);
doubled.push("6");
// Works! ✅ (doubled is a regular array)
5. Symbol 限制和陷阱
在 JSON 中使用 Symbol 需要特别注意,例如:Symbol 属性在 JSON 序列化过程中将完全消失,这一点与 React 利用 Symbol 防止服务器端 JSON 漏洞非常类似。
const API_KEY = Symbol("apiKey");
// 将 Symbol 用于属性 Key
const userData = {
[API_KEY]: "abc123xyz",
// Symbol 用于隐藏的 API key
username: "alex",
// 常规属性
};
console.log(userData[API_KEY]);
// 输出值: 'abc123xyz'
// 序列化后 Symbol 完全丢失
const savedData = JSON.stringify(userData);
console.log(savedData);
// 打印: {"username":"alex"}
同时,Symbols 的字符串强制转换会导致另一个常见的陷阱。虽然开发者可能期望 Symbols 像其他基本类型一样工作,但它们对类型转换有严格的规则:
const label = Symbol("myLabel");
// 抛出类型错误
console.log(label + "is my label");
// 开发者必须显式转化为 String
console.log(String(label) + "is my label");
// 输出值 "Symbol(myLabel) is my label"
使用 Symbol 进行内存管理比较棘手,尤其是在使用全局 Symbol 注册表时。当没有引用时,常规 Symbol 可以被垃圾收集,但注册表 Symbol 会保留下来:
// 常规 Symbol 可以垃圾回收
let regularSymbol = Symbol("temp");
regularSymbol = null;
// 注册表 Registry Symbol 保留
Symbol.for("permanent");
// 即使没用引用也会保留
console.log(Symbol.for("permanent") === Symbol.for("permanent"));
// 输出 true