为什么 Symbol(42) !== Symbol(42)?深入理解 JS Symbol 的唯一性与全局注册表

78 阅读2分钟

定义

Symbol是ES6引入的一种原始数据类型(基本类型),是六大数据类型(string,boolean,numeric(number,bigint),null,undefined,symbol)之一,表示唯一的值

意思是每次调用Symbol(),无论参数是什么,都会返回一个全新的唯一Symbol值

如:

const sym1 = Symbol(42)
conosle.log(typeof sym1) // symbol

console.log(Symbol(42)) // Symbol(42)

console.log(Symbol(42) === Symbol(42)) // false

不是?为什么Symbol(42) === Symbol(42)false?????

自增计数器

因为JS引擎内部维护一个自增计数器(或全局唯一ID生成器),会在调用Symbol()时为每个Symbol生成一个新的唯一ID,这个ID与Symbol绑定。伪代码实现:

let symbolId = 0;
function Symbol(description) {
  // 每次都生成一个新的唯一ID
  const uid = ++symbolId;
  // 返回一个带有唯一ID的 Symbol 对象
  return { __desc: description, __uid: uid };
}

以V8引擎为例,V8(Chorme/Node.js的JS引擎)内部有一个SymbolTable,每次创建Symbol都会分配一个新的唯一标识(类似指针或ID),这个唯一标识在整个运行时内都不会重复。

属性

Symbol.for(key)

这个属性会在全局注册表中查找/创建Symbol,返回一个全局唯一、可复用的Symbol。

const a = Symbol.for('bar)
const b = Symbol.for('bar')
console.log(a === b) // true

Symbol.keyFor()

用于反查,给定一个通过Symbol.for创建的Symbol,返回它在全局注册中的key(字符串),如果不是全局注册表里的Symbol,返回undefined

const s1 = Symbol.for('hello')
const s2 = Symbol('world')
const s3 = Symbol.for('hello')

console.log(Symbol.keyFor(s1)) // 'hello'
console.log(Symbol.keyFor(s2)) // undefined
console.log(Symbol.keyFor(s3)) // 'hello'

全局注册表中不存在s2这个共享的symbol,所以取不出来对应的key,可见Symbol.keyFor是获取对应的key的。

全局注册表

全局注册表是JavaScript引擎内部维护的一个全局唯一的对象(类似全局字典),专门用来存储通过Symbol.for(key)创建的Symbol,让不同作用域、不同文件、不同模块之间可以通过同一个key访问到同一个Symbol值。

为什么需要全局注册表?

普通的Symbol()每次都是新值,无法共享,而且在有些场景下(比如跨文件、跨库、跨模块),我们希望通过同一个“名字”拿到同一个Symbol,这就需要全局注册表。

全局注册表的内部原理

其实本质上是一个全局对象,以字符串为key。以Symbol为value,只要key一样,Symbol.for返回的就是同一个Symbol,这个注册表是全局唯一的,所有代码全局共享。

伪代码实现

// 引擎内部
const globalSymbolRegistry = {};

function Symbol_for(key) {
  if (globalSymbolRegistry[key]) {
    return globalSymbolRegistry[key];
  } else {
    const newSymbol = Symbol(key);
    globalSymbolRegistry[key] = newSymbol;
    return newSymbol;
  }
}