Symbol
# 13.Symbol与新特性
Symbol 是 ES6 引入的第七种原始数据类型,主要用于创建对象的唯一属性键,避免命名冲突。除了自定义 Symbol,JavaScript 还定义了一系列内置 Symbol 来控制对象的底层行为。本章同时涵盖 ES2020-ES2024 的重要新特性。
# 13.1 Symbol 基础
# 13.1.1 创建 Symbol
// 基本创建
const s1 = Symbol();
const s2 = Symbol();
console.log(s1 === s2); // false(每个 Symbol 都是唯一的)
// 带描述(仅用于调试,不影响唯一性)
const s3 = Symbol('description');
const s4 = Symbol('description');
console.log(s3 === s4); // false
console.log(s3.toString()); // 'Symbol(description)'
console.log(s3.description); // 'description'(ES2019)
// Symbol 不能用 new 调用
// new Symbol(); // TypeError
2
3
4
5
6
7
8
9
10
11
12
13
14
# 13.1.2 Symbol 作为属性键
const id = Symbol('id');
const name = Symbol('name');
const user = {
[id]: 12345,
[name]: 'Alice',
age: 25
};
// 访问
console.log(user[id]); // 12345
console.log(user[name]); // 'Alice'
// Symbol 属性不出现在常规遍历中
console.log(Object.keys(user)); // ['age']
console.log(JSON.stringify(user)); // '{"age":25}'
console.log(Object.getOwnPropertyNames(user)); // ['age']
// 专门获取 Symbol 属性
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id), Symbol(name)]
console.log(Reflect.ownKeys(user)); // ['age', Symbol(id), Symbol(name)]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这使得 Symbol 适合定义库内部使用的属性,不会与用户定义的属性冲突。
# 13.1.3 Symbol.for() 全局注册表
// Symbol.for() 在全局注册表中查找或创建 Symbol
const s1 = Symbol.for('shared');
const s2 = Symbol.for('shared');
console.log(s1 === s2); // true(同一个 Symbol)
// Symbol.keyFor() 查找全局 Symbol 的键
console.log(Symbol.keyFor(s1)); // 'shared'
// 普通 Symbol 不在注册表中
const local = Symbol('local');
console.log(Symbol.keyFor(local)); // undefined
// 全局注册表跨 iframe 和 Service Worker 共享
2
3
4
5
6
7
8
9
10
11
12
13
使用场景对比:
| 方式 | 唯一性 | 跨作用域共享 | 适用场景 |
|---|---|---|---|
Symbol() | 总是唯一 | 不可共享 | 私有属性、防冲突 |
Symbol.for() | 按键名共享 | 全局共享 | 跨模块通信、插件系统 |
# 13.2 内置 Symbol
JavaScript 定义了 13 个内置 Symbol,用于自定义对象的底层行为。
# 13.2.1 Symbol.iterator
定义对象的默认迭代器,使其可以被 for...of 遍历:
class Pagination {
constructor(items, pageSize) {
this.items = items;
this.pageSize = pageSize;
}
*[Symbol.iterator]() {
for (let i = 0; i < this.items.length; i += this.pageSize) {
yield this.items.slice(i, i + this.pageSize);
}
}
}
const pages = new Pagination([1,2,3,4,5,6,7], 3);
for (const page of pages) {
console.log(page); // [1,2,3], [4,5,6], [7]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 13.2.2 Symbol.asyncIterator
定义异步迭代器(详见第12章):
const asyncIterable = {
async *[Symbol.asyncIterator]() {
yield await Promise.resolve(1);
yield await Promise.resolve(2);
yield await Promise.resolve(3);
}
};
(async () => {
for await (const val of asyncIterable) {
console.log(val); // 1, 2, 3
}
})();
2
3
4
5
6
7
8
9
10
11
12
13
# 13.2.3 Symbol.toPrimitive
控制对象到原始值的转换(在第2章和第3章有基础介绍):
class Temperature {
constructor(celsius) {
this.celsius = celsius;
}
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return this.celsius;
case 'string':
return `${this.celsius}°C`;
case 'default':
return this.celsius;
}
}
}
const t = new Temperature(36.6);
console.log(+t); // 36.6(hint: 'number')
console.log(`${t}`); // '36.6°C'(hint: 'string')
console.log(t + 0); // 36.6(hint: 'default')
console.log(t > 35); // true(hint: 'number')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 13.2.4 Symbol.hasInstance
自定义 instanceof 的行为:
class EvenNumber {
static [Symbol.hasInstance](num) {
return typeof num === 'number' && num % 2 === 0;
}
}
console.log(2 instanceof EvenNumber); // true
console.log(3 instanceof EvenNumber); // false
console.log(4 instanceof EvenNumber); // true
// 实际应用:接口检查
class Serializable {
static [Symbol.hasInstance](instance) {
return typeof instance.toJSON === 'function';
}
}
const obj = { toJSON() { return {}; } };
console.log(obj instanceof Serializable); // true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 13.2.5 Symbol.species
控制派生对象的构造函数。当内置方法(如 map、filter)需要创建新实例时,使用 Symbol.species 指定的构造函数:
class MyArray extends Array {
static get [Symbol.species]() {
return Array; // 派生方法返回普通 Array
}
}
const myArr = new MyArray(1, 2, 3);
const mapped = myArr.map(x => x * 2);
console.log(myArr instanceof MyArray); // true
console.log(mapped instanceof MyArray); // false(因为 species 指向 Array)
console.log(mapped instanceof Array); // true
2
3
4
5
6
7
8
9
10
11
12
# 13.2.6 Symbol.toStringTag
自定义 Object.prototype.toString() 的标签:
class MyClass {
get [Symbol.toStringTag]() {
return 'MyClass';
}
}
const obj = new MyClass();
console.log(Object.prototype.toString.call(obj)); // '[object MyClass]'
// 内置对象的 toStringTag
Object.prototype.toString.call(new Map()); // '[object Map]'
Object.prototype.toString.call(new Set()); // '[object Set]'
Object.prototype.toString.call(new Promise(() => {})); // '[object Promise]'
2
3
4
5
6
7
8
9
10
11
12
13
# 13.2.7 Symbol.isConcatSpreadable
控制 Array.prototype.concat() 是否展开:
// 默认数组会展开
const arr = [1, 2];
console.log([0].concat(arr)); // [0, 1, 2]
// 阻止展开
arr[Symbol.isConcatSpreadable] = false;
console.log([0].concat(arr)); // [0, [1, 2]]
// 让类数组对象可展开
const arrayLike = {
0: 'a',
1: 'b',
length: 2,
[Symbol.isConcatSpreadable]: true
};
console.log([].concat(arrayLike)); // ['a', 'b']
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 13.2.8 其他内置 Symbol
// Symbol.match / Symbol.replace / Symbol.search / Symbol.split
// 自定义字符串方法行为
class Validator {
constructor(pattern) {
this.pattern = pattern;
}
[Symbol.match](str) {
return str.includes(this.pattern);
}
}
console.log('hello world'.match(new Validator('world'))); // true
// Symbol.unscopables
// 控制 with 语句中哪些属性被排除(主要用于兼容性)
Array.prototype[Symbol.unscopables];
// { at: true, copyWithin: true, entries: true, ... }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 13.2.9 内置 Symbol 总览
| Symbol | 用途 | 触发场景 |
|---|---|---|
Symbol.iterator | 默认迭代器 | for...of、展开、解构 |
Symbol.asyncIterator | 异步迭代器 | for await...of |
Symbol.toPrimitive | 类型转换 | 算术/字符串/比较运算 |
Symbol.hasInstance | 实例检查 | instanceof |
Symbol.species | 派生构造函数 | map/filter/slice 等 |
Symbol.toStringTag | 类型标签 | Object.prototype.toString |
Symbol.isConcatSpreadable | concat 展开 | Array.prototype.concat |
Symbol.match | 字符串匹配 | String.prototype.match |
Symbol.replace | 字符串替换 | String.prototype.replace |
Symbol.search | 字符串搜索 | String.prototype.search |
Symbol.split | 字符串分割 | String.prototype.split |
Symbol.unscopables | with 排除 | with 语句 |
Symbol.dispose | 资源清理 | using 声明(ES2024) |
# 13.3 WeakRef 与 FinalizationRegistry
# 13.3.1 WeakRef(弱引用)
ES2021 引入 WeakRef,创建对对象的弱引用,不阻止垃圾回收:
let target = { data: 'important' };
const weakRef = new WeakRef(target);
// 通过 deref() 访问目标
console.log(weakRef.deref()); // { data: 'important' }
// 移除强引用
target = null;
// 某个时刻 GC 后
// weakRef.deref() 可能返回 undefined
2
3
4
5
6
7
8
9
10
11
实际应用:缓存
class WeakCache {
#cache = new Map();
get(key) {
const ref = this.#cache.get(key);
if (ref) {
const value = ref.deref();
if (value !== undefined) return value;
this.#cache.delete(key); // 已被 GC
}
return undefined;
}
set(key, value) {
this.#cache.set(key, new WeakRef(value));
}
has(key) {
return this.get(key) !== undefined;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 13.3.2 FinalizationRegistry(回调清理)
当注册的对象被垃圾回收时触发回调:
const registry = new FinalizationRegistry((heldValue) => {
console.log(`对象被回收,附带信息: ${heldValue}`);
});
let obj = { name: 'test' };
registry.register(obj, 'obj-metadata'); // 注册对象
obj = null; // 移除强引用
// 当 GC 发生时,回调被触发
// 取消注册
let obj2 = { name: 'test2' };
const token = {};
registry.register(obj2, 'obj2-info', token);
registry.unregister(token); // 取消监听
2
3
4
5
6
7
8
9
10
11
12
13
14
15
实际应用:自动清理外部资源
class FileHandle {
static #registry = new FinalizationRegistry(handle => {
console.log(`Auto-closing file handle: ${handle}`);
// nativeClose(handle);
});
#handle;
constructor(path) {
this.#handle = path; // 模拟打开文件
FileHandle.#registry.register(this, this.#handle);
}
close() {
FileHandle.#registry.unregister(this);
// nativeClose(this.#handle);
this.#handle = null;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
注意:WeakRef 和 FinalizationRegistry 的回调时机不确定,不应依赖它们执行关键逻辑。
# 13.4 装饰器(Decorators)
ES2024(Stage 3)引入的装饰器语法,用于声明式地修改类及其成员。
# 13.4.1 类装饰器
function logged(target, context) {
// context.kind === 'class'
// context.name === 类名
return class extends target {
constructor(...args) {
console.log(`Creating ${context.name} with args:`, args);
super(...args);
}
};
}
@logged
class User {
constructor(name) {
this.name = name;
}
}
new User('Alice');
// Creating User with args: ['Alice']
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 13.4.2 方法装饰器
function bound(target, context) {
// context.kind === 'method'
const methodName = context.name;
context.addInitializer(function() {
this[methodName] = this[methodName].bind(this);
});
}
function deprecated(target, context) {
return function(...args) {
console.warn(`${context.name} is deprecated`);
return target.apply(this, args);
};
}
class API {
@bound
handleClick() {
console.log(this); // 始终指向实例
}
@deprecated
oldMethod() {
return 'old result';
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 13.4.3 属性装饰器
function validate(min, max) {
return function(target, context) {
// context.kind === 'field' 或 'accessor'
return function(initialValue) {
if (initialValue < min || initialValue > max) {
throw new RangeError(`${context.name} must be between ${min} and ${max}`);
}
return initialValue;
};
};
}
class Config {
@validate(1, 100)
retryCount = 3;
@validate(0, 1)
opacity = 0.8;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 13.4.4 auto-accessor 装饰器
function observable(target, context) {
// context.kind === 'accessor'
const { get, set } = target;
return {
get() {
console.log(`Getting ${context.name}`);
return get.call(this);
},
set(value) {
console.log(`Setting ${context.name} to ${value}`);
set.call(this, value);
},
init(initialValue) {
console.log(`Initializing ${context.name} to ${initialValue}`);
return initialValue;
}
};
}
class Store {
@observable
accessor count = 0;
}
const store = new Store();
store.count; // Getting count
store.count = 5; // Setting count to 5
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 13.5 using 声明与显式资源管理
ES2024 引入 using 声明(基于 Symbol.dispose),实现确定性资源清理。
# 13.5.1 同步资源管理
class FileHandle {
constructor(path) {
this.path = path;
this.closed = false;
console.log(`Opened: ${path}`);
}
read() {
if (this.closed) throw new Error('Already closed');
return 'file content';
}
[Symbol.dispose]() {
if (!this.closed) {
this.closed = true;
console.log(`Closed: ${this.path}`);
}
}
}
// using 声明 - 离开作用域时自动调用 [Symbol.dispose]()
{
using file = new FileHandle('/tmp/test.txt');
console.log(file.read());
} // 自动调用 file[Symbol.dispose]()
// 输出: Opened → file content → Closed
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 13.5.2 异步资源管理
class DatabaseConnection {
constructor(url) {
this.url = url;
}
async query(sql) {
return `Result of: ${sql}`;
}
async [Symbol.asyncDispose]() {
console.log('Closing database connection...');
// await this.client.end();
}
}
async function main() {
await using db = new DatabaseConnection('postgres://...');
const result = await db.query('SELECT * FROM users');
console.log(result);
} // 自动 await db[Symbol.asyncDispose]()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 13.5.3 DisposableStack
// 管理多个资源
{
using stack = new DisposableStack();
const file1 = stack.use(new FileHandle('/tmp/a.txt'));
const file2 = stack.use(new FileHandle('/tmp/b.txt'));
// ... 使用 file1 和 file2
} // 按注册的逆序清理:先 file2,后 file1
2
3
4
5
6
7
8
9
# 13.6 其他 ES2020-2024 重要特性
# 13.6.1 structuredClone(ES2022)
全局深拷贝函数,支持循环引用和复杂类型:
const original = {
name: 'Alice',
date: new Date(),
nested: { arr: [1, 2, 3] },
regex: /hello/gi,
map: new Map([['a', 1]]),
set: new Set([1, 2, 3]),
};
// 添加循环引用
original.self = original;
const clone = structuredClone(original);
clone.nested.arr.push(4);
console.log(original.nested.arr); // [1, 2, 3](未被影响)
console.log(clone.self === clone); // true(循环引用正确处理)
// 不支持的类型:Function、DOM节点、Symbol属性
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
对比其他拷贝方式:
| 方式 | 深拷贝 | 循环引用 | 特殊类型 | 性能 |
|---|---|---|---|---|
{...obj} | 浅拷贝 | 不支持 | 不处理 | 快 |
JSON.parse(JSON.stringify()) | 深拷贝 | 报错 | 丢失 | 中等 |
structuredClone() | 深拷贝 | 支持 | 保留 | 中等 |
# 13.6.2 Error cause(ES2022)
错误链,保留原始错误信息:
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
throw new Error(`Failed to fetch user ${id}`, { cause: error });
}
}
try {
await fetchUser(123);
} catch (error) {
console.log(error.message); // 'Failed to fetch user 123'
console.log(error.cause.message); // 'HTTP 404'(原始错误)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 13.6.3 Array 新方法(ES2023)
不修改原数组的变更方法:
const arr = [3, 1, 4, 1, 5];
// toSorted() - 排序(不修改原数组)
const sorted = arr.toSorted();
console.log(arr); // [3, 1, 4, 1, 5](原数组不变)
console.log(sorted); // [1, 1, 3, 4, 5]
// toReversed() - 反转
const reversed = arr.toReversed();
console.log(reversed); // [5, 1, 4, 1, 3]
// toSpliced() - splice 的不可变版
const spliced = arr.toSpliced(1, 2, 'a', 'b');
console.log(spliced); // [3, 'a', 'b', 1, 5]
// with() - 修改指定索引
const changed = arr.with(0, 99);
console.log(changed); // [99, 1, 4, 1, 5]
// findLast / findLastIndex
[1, 2, 3, 4].findLast(x => x % 2 === 0); // 4
[1, 2, 3, 4].findLastIndex(x => x % 2 === 0); // 3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 13.6.4 Hashbang 语法(ES2023)
#!/usr/bin/env node
// 允许在文件第一行使用 hashbang 注释
// 用于直接在命令行执行 JS 文件
console.log('Hello from CLI!');
2
3
4
# 13.6.5 Promise.withResolvers(ES2024)
// 传统方式
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// ES2024 简化写法
const { promise: p, resolve: res, reject: rej } = Promise.withResolvers();
// resolve 和 reject 直接可用
// 实际应用
function createDeferred() {
return Promise.withResolvers();
}
const deferred = createDeferred();
setTimeout(() => deferred.resolve('done'), 1000);
await deferred.promise; // 'done'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 13.6.6 Object.groupBy / Map.groupBy(ES2024)
const people = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
{ name: 'Charlie', age: 25 },
{ name: 'Diana', age: 30 },
];
// Object.groupBy
const grouped = Object.groupBy(people, p => p.age);
console.log(grouped);
// {
// 25: [{ name: 'Alice', age: 25 }, { name: 'Charlie', age: 25 }],
// 30: [{ name: 'Bob', age: 30 }, { name: 'Diana', age: 30 }]
// }
// Map.groupBy(键可以是对象)
const mapGrouped = Map.groupBy(people, p => p.age);
// Map { 25 => [...], 30 => [...] }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 13.6.7 Top-level await(ES2022)
模块顶层可以直接使用 await,无需包裹在 async 函数中:
// config.mjs
const response = await fetch('/api/config');
export const config = await response.json();
// app.mjs
import { config } from './config.mjs';
// 导入时会等待 config.mjs 的异步操作完成
console.log(config);
2
3
4
5
6
7
8
注意:Top-level await 会阻塞该模块和依赖该模块的所有模块的执行,应谨慎使用。
# 13.6.8 正则表达式 v 标志(ES2024)
v 标志替代 u 标志,支持 Unicode 属性集合操作:
// 集合交集(&&)
/[\p{Script=Greek}&&\p{Letter}]/v
// 集合差集(--)
/[\p{Decimal_Number}--[0-9]]/v // 非 ASCII 数字
// 字符串字面量匹配
/[\q{abc|def}]/v // 匹配 'abc' 或 'def'
2
3
4
5
6
7
8