不久我们就能终于将 JavaScript 放逐至 ShadowRealm

Lobsters Hottest 新闻

摘要

本文探讨了 TC39 提出的 ShadowRealm 提案,该提案旨在允许在不使用 iframe 或 Web Workers 的情况下,在隔离环境(Realm)中执行 JavaScript,从而改善代码沙盒机制并提升性能。

<p><a href="https://lobste.rs/s/txgfvc/soon_we_can_finally_banish_javascript">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/05/13 00:23

# 很快我们就能终于把 JavaScript 放逐到 ShadowRealm | CSS-Tricks 来源: https://css-tricks.com/soon-we-can-finally-banish-javascript-to-the-shadowrealm/ 这一篇我得努力保持冷静。好吧。我能行。我是个*专业的技术写作者*。板着脸;公事公办。咳:如果你一直关注 TC39(负责维护和制定 JavaScript 标准的标准化组织)的近期工作,你可能遇到过他们关于 ShadowRealms 的一些最近的工作(https://github.com/tc39/proposal-shadowrealm)——*噗嗤*。抱歉!抱歉,我没事!只是,呼——“ShadowRealms”这个名字真够劲。好了,稍等,让我从头说起。也许这样会有帮助。 你很可能在某些地方见过将 JavaScript 描述为“单线程”——这通常是 JavaScript 基础知识列表中排名靠前的内容,与“区分大小写”、“对空白字符不敏感”和“数学很差”并列。从严格的“计算机科学”意义上说,这是*正确*的,但每次看到这种说法,我总会有一点抵触情绪。我的意思是,准确地说 JavaScript 确实不是多线程的,这没错。脚本总是以非常线性的方式执行——从上到下,从左到右,一个执行上下文接着另一个执行上下文,调用栈先压栈再出栈。但久而久之,你会了解到像 Web Workers(https://css-tricks.com/off-the-main-thread/)这样的东西,它们——说得直白点——允许你在*另一个线程*中执行 JavaScript 代码。这就是我认为“JavaScript 是单线程的”这一说法变得不太有用的地方,因为虽然 JavaScript 本身不是一种多线程*语言*,但 JavaScript*应用程序*确实可以利用多个线程。 更准确的说法——而且在技术上同样严谨——是 JavaScript **领域(realm)**是单线程的。领域指的是代码执行的环境:浏览器标签页是一个领域,在该领域内有一个单线程用于执行 JavaScript——即**主线程**。Web Worker 是一个拥有**工作线程**的领域。在跨域 `iframe` 中运行的 JavaScript 是在*那个* `iframe` 领域的主线程上运行的。例如,我们无法将单个函数的执行卸载到另一个线程——JavaScript *本身*作为一门语言是单线程的。但一个 JavaScript 应用程序可以跨越多个领域并利用多个执行线程,并且这些领域之间可以通过特定方式进行通信。 每个 JavaScript 领域都有自己独立的全局环境。在浏览器标签页中,全局对象是 `Window` 接口。在该浏览器标签页内的非同源 `iframe` 中也是如此——全局对象是由该 `iframe` “拥有”的 `Window`: ```js ( () => { console.log( window.globalThis ); // 结果: Window {} console.log( theIframe.contentWindow.globalThis ); // 结果: Window {} })(); ``` 它们不是*同一个*全局对象: ```js ( () => { console.log( window.globalThis === theIframe.contentWindow.globalThis ); // 结果: false })(); ``` 外部页面和内部 `iframe` 是两个独立的领域,每个领域都是单线程的,各自拥有自己的全局对象和固有对象(intrinsic objects): ```js (() => { console.log( window.Array ); /* 结果(展开): function Array() from: function from() fromAsync: function fromAsync() isArray: function isArray() length: 1 name: "Array" of: function of() prototype: Array [] Symbol(Symbol.species): undefined <prototype>: function () */ console.log( theIframe.contentWindow.Array ); /* 结果(展开): function Array() from: function from() fromAsync: function fromAsync() isArray: function isArray() length: 1 name: "Array" of: function of() prototype: Array [] Symbol(Symbol.species): undefined <prototype>: function () */ console.log( window.Array === theIframe.contentWindow.Array ); // 结果: false })(); ``` 所以,正如你预期的那样,在一个领域的上下文中定义的任何全局属性在另一个领域中都不可用: ```js function globalFunction() {}; console.log( window.globalFunction ); // 结果: function globalFunction() console.log( theIframe.contentWindow.globalFunction ); // 结果: undefined ``` “不可用”——或者,取决于你如何看待这个问题,是无法*干扰*另一个领域的全局对象。如果你使用 JavaScript 有一段时间了,你就会知道,无论我们在管理作用域时多么仔细,尽管我们尽了最大努力,全局环境仍然可能变得非常混乱。这部分确实是我们自己的原因——最优秀的人也会不小心留下变量绑定——但很多混乱是语言本身早期设计决策的结果,比如前例中的函数声明。考虑到我们无法控制的、可能会堆积到平均项目中的大量 JavaScript——从框架到第三方辅助库,再到 Polyfill、用户分析和广告——至少可以说,存在冲突的潜力。 鉴于自远古时代(90 年代)以来一直困扰着这门语言的全局作用域污染问题,不难想象将代码卸载到一个领域以作为沙盒的用例,用于执行那些我们不希望影响或受当前全局作用域中混乱内容影响的 JavaScript。我们可能希望在一个“无菌室”中运行部分测试套件,其中*执行*测试的行为不会潜在地干扰测试结果,模拟数据也不会与真实数据冲突;或者提供一个地方来运行我们希望与包含我们自身 JavaScript 应用程序的领域隔离开来的代码,以防止不需要访问全局环境的第三方库将其弄乱,却没有任何好处。 我们无法用现有的领域做到这一点——记住,JavaScript 是单线程的,因为每个*领域*都是单线程的,并且这些线程之间的通信是受限的。尽管用例不可否认,但我们不能利用一个替代领域在其单线程上执行代码,然后将执行结果编织回我们主领域的主线程。这本质上就是多线程执行,这不仅违背了 JavaScript 的基本性质,而且,嗯,让我这么说吧:JavaScript 允许同时执行多个线程*会带来新的问题给我们*(意译:这会给我们的世界带来全新的麻烦)。 要以这种方式卸载代码,需要一种*新类型*的领域——一种拥有自己的全局和固有对象,但*没有*自己线程的领域——一种代码卸载到其中后,仍将在“拥有”该脚本的领域的主线程上执行的领域。这是我们自身领域的黑暗镜像;一个光线永远无法触及的领域,只有我们放逐代码的短暂、虚幻的影子才能居住!想象这里有一声遥远的雷鸣;也许还可以想象我穿着一件斗篷,也许我把一个酒杯摔向地板。你知道,尽情娱乐吧。你怎么能忍住不笑呢?毕竟,它们被称为: ### ShadowRealms 提议中的 ShadowRealm API(https://github.com/tc39/proposal-shadowrealm)引入了一种专为*隔离*而设计的新领域,仅此而已。ShadowRealm *没有*自己的执行上下文——卸载到 ShadowRealm 的代码将存在于一个伪领域中,该伪领域拥有自己的全局对象和内置对象。该代码继续在创建 ShadowRealm 的代码所在的同一线程上运行;我们不需要在两个独立线程之间以受限的方式来回通信和共享资源。简而言之,脚本的执行方式就像局限于单个领域一样,但与外部领域的固有对象、API、全局对象以及我们的脚本对该全局对象所做的任何操作隔离开来。 这听起来很复杂,但提议中的 API 在实践中将异常简单: ```js // 创建 ShadowRealm: const shadow = new ShadowRealm(); function globalFunction() {}; console.log( globalthis.globalFunction ); // 结果: function globalFunction() // 在 ShadowRealm 内部评估 `globalThis.globalFunction`: console.log( shadow.evaluate( 'globalThis.globalFunction' ) ); // 结果: undefined ``` **注意:**请记住,此代码仍然是理论上的——它尚未存在于 ES-262 标准或浏览器中。`globalFunction` 就像我们之前看到的那样定义在外部领域的全局对象上,但它没有定义在我们新创建的 ShadowRealm 内部的全局对象上——无论我们在*外部*做什么,该 ShadowRealm 的全局对象都保持纯净。 反过来也是如此: ```js // 创建 ShadowRealm: const shadow = new ShadowRealm(); // 在 ShadowRealm 内部声明一个全局函数: shadow.evaluate( 'function globalFunction() {};' ); // 它不存在于外部领域的全局对象中: console.log( globalthis.globalFunction ); // 结果: undefined // 但当我们评估 ShadowRealm 内部的 `globalThis.globalFunction` 时: console.log( shadow.evaluate( 'globalThis.globalFunction' ) ); // 结果: function globalFunction() ``` 我们在 ShadowRealm 内部声明了该函数,我们可以通过引用该 ShadowRealm 对象的变量来调用它。该函数与外部全局对象以及任何其他 ShadowRealm 的全局对象隔离: ```js // 创建 ShadowRealm: const firstShadow = new ShadowRealm(); const secondShadow = new ShadowRealm(); // 在由 `secondShadow` 引用的 ShadowRealm 内部声明一个全局函数: secondShadow.evaluate( 'function globalFunction() {};' ); // 它不存在于外部领域的全局对象中: console.log( globalthis.globalFunction ); // 结果: undefined // 它不存在于由 `firstShadow` 引用的 ShadowRealm 的全局对象中: console.log( firstShadow.evaluate( 'globalThis.globalFunction' ) ); // 结果: undefined // 它仅存在于由 `secondShadow` 引用的 ShadowRealm 中: console.log( secondShadow.evaluate( 'globalThis.globalFunction' ) ); // 结果: function globalFunction() ``` 这就是所谓的“隔离”。ShadowRealms 不提供真正的安全边界,因为在 ShadowRealm 内部运行的代码仍然可以对其他领域中运行的代码进行推断。但它们*可以*被视为*完整性*边界,因为在 ShadowRealm 内部运行的代码不能直接干扰另一个领域——当然,除非我们允许它这样做。 尽管被分流到 ShadowRealm 的代码无法干扰外部的对象,但我们仍然可以自由地使用这些操作的结果,就像我们在宿主领域中使用相同操作的结果一样: ```js // 创建 ShadowRealm: const shadow = new ShadowRealm(); // 创建一个绑定来调用 ShadowRealm 内部的函数: const shadowFunction = shadow.evaluate( '( value ) => globalThis.someValue = value ); // ...并使用该绑定调用我们的包装函数: shadowFunction( "Hello from the ShadowRealm!" ); // 当然,在宿主领域中执行此函数不会*改变*这里的任何内容: console.log( globalThis.someValue ); // 结果: undefined // 但我们可以从 ShadowRealm 获取结果: const shadowValue = shadow.evaluate( 'globalThis.someValue' ); // 并在宿主领域中使用它: console.log( shadowValue ); // 结果: Hello from the ShadowRealm! ``` 无限的 disposable 无菌室!口袋维度,我们可以在其中执行任何我们想要的代码,而不必担心该代码会干扰任何其他 ShadowRealm *或* 外部领域(如果你愿意的话,可以称之为“光明领域”)的作用域。 现在,你们中的一些人——尤其是那些从 JavaScript 早期阶段就开始从事这项工作的人——可能会对示例感到退缩。你们可能会认为 ShadowRealm API 只是 goth 风格的 `eval`,而且你们并没有完全错:除了在 ShadowRealm 的上下文中运行之外,到目前为止你看到的本质上都是间接调用 `eval`——甚至受到相同的 `unsafe-eval` 内容安全策略(https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy)规则的限制。 然而,不要担心你的工作流程:虽然这些是*说明性*示例,但这并不是使用 ShadowRealms 的唯一方法。该提案在 ShadowRealm 对象的原型中包含了一个 `importValue` 方法,它允许你动态导入模块,然后捕获并处理导出的值和函数: ```js // spookycode.js export function greeting() { return "Hello from the ShadowRealm!"; } ``` ```js async function shadowGreeter() { // 我召唤了 ShadowRealm 的黑暗力量——咳。抱歉。 const shadow = new ShadowRealm(); /* * `importValue` 返回一个 promise,该 promise 解析为第二个参数中指定的函数的值: */ const shadowGreet = await shadow.importValue( "./spookycode.js", "greeting" ); // 调用我们的包装函数,然后…… shadowGreet(); } shadowGreeter(); // 结果: Hello from the ShadowRealm! ``` ### 阴影尚未降临 我很高兴地说,到目前为止,你已经看到了提议的 ShadowRealms API 的*全部*内容。该提案仅包含你在上面看到的这两种方法——`evaluate` 和 `importValue`——两者都是在 ShadowRealm 实例的上下文中*评估*代码的同时,仍在宿主领域线程的上下文中*执行*该代码的手段。 不过,重申一遍:这些目前还无法投入使用。提议的规范目前处于**第 2.7 阶段**(https://tc39.es/process-document/)——“原则上批准并正在进行验证”,这意味着如果有的话,它只会因浏览器中的测试和试验性实施的反馈而发生更改。你阅读本文算是超前了一步。当该提案达到第 3 阶段且我们开始在浏览器中看到实现时,你将准备好亲自尝试。不,不仅仅是准备好——当 ShadowRealm 的强大力量释放到网络上时,你将随时准备指挥其黑暗而可怕的 majjycks!(注:此处原文为幽默语气,majjycks 为虚构词,模仿黑暗力量的威严)*我们代码所站立的那个领域将会震颤*——好吧,好吧,抱歉。看,我忍不住!我是说,“*ShadowRealm*”,天哪。

相似文章

告别 Asm.js

Hacker News Top

Mozilla 的 SpiderMonkey 引擎默认禁用 asm.js 优化,标志着这一为 WebAssembly 铺平道路的技术走向终结。建议用户重新编译到 WebAssembly 以获得更好的性能。

沙盒化令人抓狂

Lobsters Hottest

一篇技术博客,讨论实现安全沙盒技术的复杂性与挫败感。

代码调用就是一切

Reddit r/AI_Agents

认为使用LLM生成的代码调用外部工具(代码调用)比传统的基于JSON的函数调用更高效、功能更强,但需要安全的沙箱环境。作者正在为此方法构建一个框架。