第 33 章 Symbol

第 33 章 Symbol

Symbol 是 ES6 引入的一种新数据类型,代表一个"独一无二"的值。就像每个人都有自己的身份证号一样,每个 Symbol 都是唯一的。

33.1 Symbol 基础

Symbol():创建独一无二的值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 创建 Symbol
const s1 = Symbol();
const s2 = Symbol();

console.log(s1 === s2); // 打印结果: false(每次 Symbol() 都是独一无二的)

// Symbol 可以有描述
const s3 = Symbol('mySymbol');
const s4 = Symbol('mySymbol');

console.log(s3 === s4); // 打印结果: false(描述相同,但值不同)
console.log(s3.description); // 打印结果: mySymbol

两个使用上的注意点:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// ❌ Symbol 不是构造函数,不能用 new
// new Symbol(); // TypeError: Symbol is not a constructor

// ✅ 需要包装对象时用 Object()
const boxed = Object(Symbol('s'));
console.log(typeof boxed);      // 打印结果: object
console.log(typeof Symbol('s')); // 打印结果: symbol(这才是常态)

// Symbol 不能参与算术运算和隐式字符串拼接,会直接抛类型错误
// Symbol('s') + '';  // TypeError: Cannot convert a Symbol value to a string
console.log(String(Symbol('s'))); // 打印结果: Symbol(s)(显式转换可以)

Symbol.for / Symbol.keyFor:全局注册表

Symbol.for 创建在全局注册表中的 Symbol,同一个 key 会返回同一个 Symbol。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Symbol.for:在全局注册表中查找或创建
const s1 = Symbol.for('globalSymbol');
const s2 = Symbol.for('globalSymbol');

console.log(s1 === s2); // 打印结果: true(同一个 key 返回同一个值)

// Symbol.keyFor:从全局注册表中获取 key
const key = Symbol.keyFor(s1);
console.log(key); // 打印结果: globalSymbol

// 普通 Symbol 不在全局注册表中
const s3 = Symbol('notGlobal');
console.log(Symbol.keyFor(s3)); // 打印结果: undefined

Symbol 属性不会被常规遍历看到

先澄清一个流传很广的错误说法:Symbol 属性默认是可枚举的,只是 Object.keys、for...in、JSON.stringify 这些「字符串键专用」的接口按规范跳过了它。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const obj = {
    name: '小明',
    age: 18
};

const secret = Symbol('secret');
obj[secret] = '隐藏的信息';

console.log(Object.keys(obj)); // 打印结果: ['name', 'age'](Symbol 键不在其中)
console.log(Object.getOwnPropertySymbols(obj)); // 打印结果: [Symbol(secret)]

// 它实际上是可枚举的
console.log(Object.getOwnPropertyDescriptor(obj, secret).enumerable); // 打印结果: true

// 想拿到全部键(字符串键 + Symbol 键),用 Reflect.ownKeys
console.log(Reflect.ownKeys(obj)); // 打印结果: ['name', 'age', Symbol(secret)]

这带来几个实用且常被忽略的行为差异:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const id = Symbol('id');
const source = { [id]: 1, name: 'A' };

// JSON.stringify 会丢掉 Symbol 键
console.log(JSON.stringify(source)); // 打印结果: {"name":"A"}

// 但 Object.assign 和展开运算符会复制 Symbol 键
console.log(Object.assign({}, source)[id]); // 打印结果: 1
console.log({ ...source }[id]);             // 打印结果: 1

// 深拷贝的经典坑:JSON.parse(JSON.stringify(x)) 会把 Symbol 键整个抹掉
console.log(JSON.parse(JSON.stringify(source))[id]); // 打印结果: undefined

另一个常见误解是把 Symbol 当成「真正的私有属性」。它只是不容易被误用:拿不到这个 Symbol 就无法访问,但只要能拿到对象,就能用 Object.getOwnPropertySymbols() 把键全列出来。想要外部彻底访问不到,用 #私有字段。

下一节,我们来学习内置 Symbol!

33.2 内置 Symbol

Symbol.iterator:可迭代协议

Symbol.iterator 让对象变成可迭代的,可以用 for…of 遍历。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
const myArray = {
    items: ['苹果', '香蕉', '橙子'],
    [Symbol.iterator]: function() {
        let index = 0;
        const self = this;
        return {
            next: function() {
                if (index < self.items.length) {
                    return {
                        value: self.items[index++],
                        done: false
                    };
                }
                return { done: true };
            }
        };
    }
};

for (const item of myArray) {
    console.log(item); // 打印结果: 苹果 香蕉 橙子
}

Symbol.toStringTag:自定义 toString 结果

1
2
3
4
5
const person = {
    [Symbol.toStringTag]: 'Person'
};

console.log(Object.prototype.toString.call(person)); // 打印结果: [object Person]

Symbol.hasInstance:自定义 instanceof 行为

1
2
3
4
5
6
7
8
class Even {
    static [Symbol.hasInstance](instance) {
        return typeof instance === 'number' && instance % 2 === 0;
    }
}

console.log(2 instanceof Even); // 打印结果: true
console.log(3 instanceof Even); // 打印结果: false

Symbol.toPrimitive:自定义类型转换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const obj = {
    value: 42,
    [Symbol.toPrimitive](hint) {
        if (hint === 'number') {
            return this.value;
        }
        if (hint === 'string') {
            return String(this.value);
        }
        return this.value;
    }
};

console.log(+obj); // 打印结果: 42(hint = 'number')
console.log(String(obj)); // 打印结果: 42(hint = 'string')
console.log(obj + ''); // 打印结果: 42(hint = 'default')

Symbol.replace / Symbol.split:自定义字符串方法

1
2
3
4
5
6
7
const replacer = {
    [Symbol.replace](target, replacement) {
        return target.toUpperCase().replace('HELLO', replacement);
    }
};

console.log('hello'.replace(replacer, 'Hi')); // 打印结果: Hi

准确地说,replace、replaceAll、match、matchAll、search、split 这些字符串方法都会先检查参数上有没有对应的 Symbol 方法,有就交给它执行。这就是「实现协议来接管内置行为」。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 自定义替换:把每个 'HELLO' 换掉
const asterisk = {
    [Symbol.replace](target, replacement) {
        return target.toUpperCase().split('HELLO').join(replacement);
    }
};
console.log('hello hello'.replace(asterisk, 'Hi')); // 打印结果: Hi Hi

// 自定义 split:逗号和分号都当作分隔符
const splitWay = {
    [Symbol.split](target) {
        return target.split(/[,;]/);
    }
};
console.log('a,b;c'.split(splitWay)); // 打印结果: ['a', 'b', 'c']

// 自定义 search
const onlyDigits = {
    [Symbol.search](target) {
        return target.search(/\d+/);
    }
};
console.log('abc123'.search(onlyDigits)); // 打印结果: 3

Symbol.isConcatSpreadable:控制 concat 展开

1
2
3
4
5
6
7
8
9
const array = [1, 2];
const likeArray = {
    0: 'a',
    1: 'b',
    length: 2,
    [Symbol.isConcatSpreadable]: true
};

console.log([].concat(array, likeArray)); // 打印结果: [1, 2, 'a', 'b']

其他值得知道的内置 Symbol

内置 Symbol作用常见出现场景
Symbol.asyncIterator异步迭代协议for await...of、异步生成器
Symbol.species指定派生对象的构造函数子类化 Array / Promise 时控制 map、slice 返回什么类型
Symbol.unscopables把某些属性从 with 作用域里排除历史遗留,现代代码不要依赖
Symbol.matchAll自定义 String.prototype.matchAll 的行为自定义匹配器
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 异步迭代器:让对象可以用 for await...of 遍历
const asyncNumbers = {
    [Symbol.asyncIterator]() {
        let i = 0;
        return {
            async next() {
                if (i < 3) {
                    await new Promise((r) => setTimeout(r, 10));
                    return { value: i++, done: false };
                }
                return { done: true };
            },
        };
    },
};

(async () => {
    for await (const n of asyncNumbers) {
        console.log(n); // 依次打印 0 1 2
    }
})();

跨环境的坑:Symbol.for 的「全局」是按环境算的

Symbol.for 的注册表只在一个运行环境内共享:

  • 同一个页面的不同 iframe 之间不共享,各自的 Symbol.for('x') 互不相等;
  • 主线程与 Web Worker 不共享;
  • Node.js 里同一进程内的模块共享,不同进程(如 cluster 起的多个进程)之间不共享;
  • Symbol 本身不能跨环境传递,postMessage 传 Symbol 会抛错。

所以 Symbol.for 适合「同一环境内多个库想共享同一个键」的场景,比如给 DOM 节点挂内部状态。跨进程通信要用字符串、数组等结构化克隆支持的类型。


本章小结

本章我们学习了 Symbol:

  1. Symbol 基础:Symbol() 创建独一无二的值,Symbol.for / Symbol.keyFor 使用全局注册表,Symbol 不能 new、不能隐式转字符串。
  2. 遍历差异:Symbol 属性不会被 Object.keys / for...in / JSON.stringify 看到,但默认是可枚举的,Object.assign、展开运算符、Reflect.ownKeys 都会带上它。
  3. 内置 Symbol:iterator / asyncIterator / toStringTag / hasInstance / toPrimitive / replace / split / search / isConcatSpreadable 等,通过实现协议改变对象与内置 API 的交互方式。
  4. 使用边界:Symbol 不是真私有(可被反射),Symbol.for 的全局范围只限于当前运行环境。

Symbol 是 JavaScript 的"秘密武器",可以创建独一无二的属性名,避免属性名冲突,是实现协议和元编程的重要工具。

下一章,我们要学习正则表达式——字符串匹配的"神器"!

最后修改 September 19, 2026: 更新 (3489033b1)