大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!
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
5. 使用 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)
6.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
参考资料
https://www.trevorlasn.com/blog/symbols-in-javascript
https://www.youtube.com/watch?v=ZfXAwr0Idl8
https://www.youtube.com/watch?v=KTls9254tuU
https://javascript.plainenglish.io/exploring-the-magic-of-javascript-mastering-symbol-manipulation-e4645738e8d0