Back to list
技术
37 分钟
Symbol 与内置行为定制
Site Owner
Published on 2026-05-21
详解Symbol唯一性与全局注册表、Symbol.iterator/asyncIterator/hasInstance/species等内置Symbol的定制能力

Symbol 与内置行为定制
Symbol 是 ES6 引入的第七种原始类型,提供了唯一性标识符。更重要的是,Well-Known Symbols(众所周知的 Symbol)允许开发者自定义对象在语言内置操作中的行为——如迭代、类型转换、正则匹配等。Symbol 是 JavaScript 元编程的轻量级工具。
目录
- Symbol 基础与唯一性
- Symbol.iterator 与迭代协议
- Symbol.toPrimitive 与类型转换
- Symbol.hasInstance 与 instanceof
- Symbol.species 与派生类
- 其他 Well-Known Symbols
- Symbol.for 全局注册表
- 实战案例
- 深度追问
- 总结表格
1. Symbol 基础与唯一性
// 代码示例 1:Symbol 的唯一性
const s1 = Symbol('description');
const s2 = Symbol('description');
console.log(s1 === s2); // false(每次创建都是唯一的)
console.log(s1.description); // 'description'
// 作为属性键
const SECRET = Symbol('secret');
const obj = {
[SECRET]: 'hidden value',
public: 'visible'
};
console.log(Object.keys(obj)); // ['public']
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(secret)]
console.log(JSON.stringify(obj)); // '{"public":"visible"}'
console.log(obj[SECRET]); // 'hidden value'
2. Symbol.iterator 与迭代协议
// 代码示例 2:自定义迭代器
class Range {
constructor(start, end, step = 1) {
this.start = start;
this.end = end;
this.step = step;
}
[Symbol.iterator]() {
let current = this.start;
const { end, step } = this;
return {
next() {
if (current <= end) {
const value = current;
current += step;
return { value, done: false };
}
return { done: true };
},
// 支持提前退出
return(value) {
console.log('Iterator closed early');
return { value, done: true };
}
};
}
}
const range = new Range(1, 10, 2);
for (const n of range) {
console.log(n); // 1, 3, 5, 7, 9
}
console.log([...range]); // [1, 3, 5, 7, 9]
3. Symbol.toPrimitive 与类型转换
// 代码示例 3:完全控制类型转换
class Temperature {
#celsius;
constructor(celsius) {
this.#celsius = celsius;
}
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return this.#celsius;
case 'string':
return `${this.#celsius}°C`;
default:
return this.#celsius;
}
}
}
const temp = new Temperature(36.6);
console.log(+temp); // 36.6
console.log(`${temp}`); // '36.6°C'
console.log(temp + 0); // 36.6
console.log(temp > 30); // true
4. Symbol.hasInstance 与 instanceof
// 代码示例 4:自定义 instanceof 行为
class Validator {
static [Symbol.hasInstance](instance) {
return instance !== null &&
typeof instance === 'object' &&
typeof instance.validate === 'function';
}
}
const form = {
validate() { return true; }
};
console.log(form instanceof Validator); // true
console.log({} instanceof Validator); // false
5. Symbol.species 与派生类
// 代码示例 5:控制派生方法的返回类型
class MyArray extends Array {
// map/filter 等方法返回 MyArray 而非 Array
static get [Symbol.species]() {
return Array; // 改为返回普通 Array
}
}
const myArr = new MyArray(1, 2, 3);
const mapped = myArr.map(x => x * 2);
console.log(mapped instanceof MyArray); // false(如果 species 返回 Array)
console.log(mapped instanceof Array); // true
6. 其他 Well-Known Symbols
// 代码示例 6:Symbol.toStringTag
class Database {
get [Symbol.toStringTag]() {
return 'Database';
}
}
console.log(Object.prototype.toString.call(new Database()));
// '[object Database]'
// Symbol.isConcatSpreadable
const arrayLike = {
0: 'a', 1: 'b', length: 2,
[Symbol.isConcatSpreadable]: true
};
console.log(['x'].concat(arrayLike)); // ['x', 'a', 'b']
// Symbol.match/replace/search/split — 自定义正则行为
class CaseInsensitiveMatcher {
constructor(pattern) { this.pattern = pattern.toLowerCase(); }
[Symbol.match](str) {
return str.toLowerCase().includes(this.pattern) ? [this.pattern] : null;
}
}
console.log('Hello World'.match(new CaseInsensitiveMatcher('hello'))); // ['hello']
7. Symbol.for 全局注册表
// 代码示例 7:跨 realm 共享 Symbol
const s1 = Symbol.for('shared.key');
const s2 = Symbol.for('shared.key');
console.log(s1 === s2); // true(全局注册表)
console.log(Symbol.keyFor(s1)); // 'shared.key'
console.log(Symbol.keyFor(Symbol('local'))); // undefined
// 跨 iframe / worker 共享
// iframe 中:Symbol.for('app.event') === 主页面中的 Symbol.for('app.event')
8. 实战案例
实战案例 1:类型安全的枚举
const Direction = Object.freeze({
UP: Symbol('UP'),
DOWN: Symbol('DOWN'),
LEFT: Symbol('LEFT'),
RIGHT: Symbol('RIGHT'),
[Symbol.iterator]() {
return [this.UP, this.DOWN, this.LEFT, this.RIGHT][Symbol.iterator]();
}
});
function move(direction) {
if (![...Direction].includes(direction)) {
throw new TypeError('Invalid direction');
}
// ...
}
move(Direction.UP); // ✅
move('UP'); // TypeError(字符串不等于 Symbol)
实战案例 2:可释放资源协议 (Symbol.dispose)
// ES2024 Explicit Resource Management
class FileHandle {
#handle;
constructor(path) {
this.#handle = openFile(path);
}
[Symbol.dispose]() {
this.#handle.close();
console.log('File closed');
}
read() { return this.#handle.readAll(); }
}
// using 声明自动调用 Symbol.dispose
{
using file = new FileHandle('/tmp/data.txt');
const content = file.read();
} // 自动调用 file[Symbol.dispose]()
实战案例 3:私有协议标记
const INTERNAL = Symbol('internal');
class Plugin {
[INTERNAL] = { initialized: false };
init() {
this[INTERNAL].initialized = true;
}
// 框架内部可以访问
static getInternals(plugin) {
return plugin[INTERNAL];
}
}
const plugin = new Plugin();
// 外部代码无法访问 INTERNAL(除非获得该 Symbol 的引用)
console.log(Object.keys(plugin)); // []
9. 深度追问
Q1:Symbol 能被 GC 吗?
Symbol() 创建的 Symbol 在无引用时可被 GC。但 Symbol.for() 创建的 Symbol 存在于全局注册表中,永远不会被 GC,类似于字符串常量池。
Q2:为什么 Symbol 不能用 new?
因为 Symbol 是原始类型,不是对象。如果允许 new Symbol(),返回的将是 Symbol 包装对象,这与设计意图矛盾。需要包装对象时使用 Object(Symbol())。
Q3:WeakMap 为什么不能用 Symbol 作为键?
ES2023 已经允许了!Symbol 可以作为 WeakMap 的键,但仅限于非注册的 Symbol(不是 Symbol.for() 创建的),因为注册 Symbol 永远不会被 GC。
10. 总结表格
| Well-Known Symbol | 功能 | 触发操作 |
|---|---|---|
Symbol.iterator | 定义迭代行为 | for...of, ...spread |
Symbol.asyncIterator | 异步迭代 | for await...of |
Symbol.toPrimitive | 类型转换 | +obj, \${obj}`` |
Symbol.hasInstance | instanceof 检测 | x instanceof Class |
Symbol.species | 派生构造器 | arr.map() 返回类型 |
Symbol.toStringTag | 对象标签 | Object.prototype.toString |
Symbol.isConcatSpreadable | 数组展开 | [].concat(obj) |
Symbol.match/replace/search/split | 正则协议 | str.match(obj) |
Symbol.dispose | 资源释放 | using 声明 |
下一篇:13. 高阶操作与管道组合