大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!
1. 什么是代码域
代码域(realm)是代码片段所在的上下文,其包含全局变量、已加载的模块等等。即使代码只存在于一个域中,其也可能访问其他域中的代码。
例如,浏览器中的每个框架 (frame) 都有自己独立的域,并且代码执行可以从一个框架跳转到另一个框架,如以下 HTML 所示。
<head>
<script>
function test(arr) {
var iframe = frames[0];
// 此代码与 iframe 的代码存在于
// 不同的领域。因此,全局变量
// 例如 Array 是不同的
console.log(Array === iframe.Array);
// false
console.log(arr instanceof Array);
// false
console.log(arr instanceof iframe.Array);
// true
// 但是: Symbols 是相同的
console.log(Symbol.iterator ===
iframe.Symbol.iterator); // true
}
</script>
</head>
<body>
<iframe srcdoc="<script>window.parent.test([])</script>">
</iframe>
</body>理解以上代码的核心在于,每个代码域都有独立的数组副本,而且由于对象具有独立的标识,本地副本即使本质上是同一对象也会被视为不同。同样,外部库和用户代码都会加载一次,并且每个代码域都有同一对象的不同副本。
相比之下,布尔值、数字和字符串等原始类型由于没有独立的标识,因此相同值的多个副本不会造成问题,因为副本会 “按值” 比较从而视为相等。而 Symbol 具有独立的标识,因此无法像原始值跨代码域传输,最终影响 Symbol.iterator 跨代码域工作。
同时,如果一个对象在一个代码域中可迭代,那么在其他代码域中也应该可迭代。如果 JavaScript 引擎提供了跨代码域 Symbol,引擎就可以确保在每个代码域中使用相同的值。而对于外部库来说则需要额外的支持,其以全局 Symbol 注册表 (Global Symbol Registry) 的形式出现,即该注册表对所有代码域都是全局的,并将字符串映射到 Symbol。
因此对于每个 Symbol,库需要提供一个尽可能唯一的字符串。为了创建 Symbol,其不使用 Symbol() 方法,而是向全局注册表请求获取该字符串映射到的 Symbol。如果注册表中已经有该字符串的条目,则返回关联的 Symbol,否则先创建再返回。
例如:开发者可以通过 Symbol.for() 向注册表请求一个 Symbol,并通过 Symbol.keyFor() 检索与该 Symbol 关联的字符串:
let sym = Symbol.for('Hello everybody!');
Symbol.keyFor(sym)
// 输出'Hello everybody!'2. 什么是全局 Symbol
const sym = Symbol("foo");
typeof sym; // "symbol"使用 Symbol() 语法将创建一个在程序的整个生命周期内保持唯一的值,而要创建跨文件甚至跨代码域可用的 Symbol,开发者需要使用 Symbol.for() 和 Symbol.keyFor() 从全局 Symbol 注册表中设置和检索 Symbol,此时就不得不提到全局 Symbol 注册表(Global Symbol Registry):
全局 Symbol 注册表可以通过字符串键访问,其 “全局” 的含义比全局作用域更全局,全局 Symbol 注册表涵盖了引擎的所有代码域。在浏览器中,网页、iframe 和 Web Worker 都有各自的代码域和全局对象,但都可以通过全局 Symbol 注册表共享 Symbol。
不过实际上,全局 Symbol 注册表只是一个虚构的概念,可能与 JavaScript 引擎中任何内部数据结构都不对应,而且即使存在这样的注册表,其内容也无法供 JavaScript 代码使用,除非通过 for() 和 keyFor() 方法。
Symbol.keyFor(Symbol.for("tokenString")) === "tokenString";
// 输出 true由于全局 Symbol (Registered Symbols) 可以在任何地方任意创建,行为几乎与包装的字符串基础类型完全相同。因此,不能保证唯一且不可被垃圾回收(非常容易伪造)。因此,WeakMap、WeakSet、WeakRef 和 FinalizationRegistry 对象中不允许使用全局 Symbol。
// 通过 Symbol.for("key") 创建的全局 Symbol 容易伪造,无法被垃圾回收
// 需要注意的是 Symbol 本身是基础类型,Symbol.for 更像基础类型
// 而 Symbol() 更像引用类型,不容易伪造
const sym1 = Symbol.for("myKey");
let sym1Ref = sym1;
sym1Ref = null;
// 注意:原始值 (Symbol) 是直接拷贝,而非引用赋值
// 再次通过相同的 key 获取这个 Symbol
const sym2 = Symbol.for("myKey");
console.log(sym1 === sym2);
// 输出 true,证明 sym1 仍然存在,未被垃圾回收值得一提的是,除了开发者通过 Symbol.for() 手动创建的全局 Symbol,JavaScript 标准库中也定义了一些内置的全局 Symbol,例如:
- Symbol.iterator
- Symbol.asyncIterator
- Symbol.hasInstance
- Symbol.toPrimitive
- Symbol.toStringTag
- Symbol.replace 等等
不过,这些内置 Symbol 是由 JavaScript 引擎预定义,开发者可以在不同代码域间共享,但由于不是通过 Symbol.for() 创建,所以不会出现在全局 Symbol 注册表中,用 Symbol.keyFor() 也无法查到。
const sym = Symbol.iterator;
typeof sym;
// 输出'symbol',表明是 Symbol 基础类型
console.log(Symbol.keyFor(sym));
console.log(Symbol.keyFor(Symbol.hasInstance));
// 都输出 undefined,即内置全局 Symbol 不在全局 Symbol 注册表中开发者可以把内置 Symbol 看作是 JavaScript 的 “系统级常量”,而 Symbol.for() 创建的是开发者级别的 “自定义全局 Symbol”。
3. 全局 Symbol 和全局对象有什么区别
从前文大致可以得出以下结论:
关于代码域的概念再做一个说明:
一个应用程序可能由多个 JavaScript 环境组成,每个环境都有各自的全局作用域和全局对象,而环境被称为一个代码域 (Realm)。从代码内部打开的窗口、网页中的 \<iframe> 以及 Web Worker 都是代码域。一个代码域中的代码可以访问其他关联代码域中的代码,但并不共享同一个全局作用域。
参考资料
https://2ality.com/2014/12/es6-symbols.html#crossing-realms-with-symbols
https://stackoverflow.com/questions/31897015/what-is-global-symbol-registry
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol
https://shoes-web.ru/has/js/kartinka_77/
