当前位置:主页 > 查看内容

ECMAScript 双月报告:Realms 提案进入 Stage 3(2021/07)

发布时间:2021-08-20 00:00| 位朋友查看

简介:C.isC(d);// = false; 另一个值得注意的事情是,对于库作者来说,用户在某些场景下是可以在同一个 JavaScript 程序中使用多个版本的库,比如 lodash 3.1.1 和 lodash 3.1.2,这取决于依赖管理的定义。而如果这些库中使用了私有字段,则他们的对象是不具有互操……
C.isC(d); // = false;

另一个值得注意的事情是,对于库作者来说,用户在某些场景下是可以在同一个 JavaScript 程序中使用多个版本的库,比如 lodash 3.1.1 和 lodash 3.1.2,这取决于依赖管理的定义。而如果这些库中使用了私有字段,则他们的对象是不具有互操作性的。这与上面这个同名字段例子的原因相同,这些不同版本的库虽然库名相同,但是实际上他们在程序中是具有独立的运行上下文的,class C 会被定义两次,并且这两次是毫无关系的,所以对于 3.1.1 版本的 C.isC 判断会对 3.1.2 版本的实例 c 会判断为 false。这对于库作者维护来说,是非常需要注意的一个事项。

最后需要注意的是,在下面这个场景中,Private-in 检查可以造成正确但是不符合直觉的结果:

class Foo extends function(o) { return o; } {
 #first = 1;
 #second = (() = { throw null })();
 static hasFirst = o = checkHas(() = o.#first);
 static hasSecond = o = checkHas(() = o.#second);
let checkHas = fn = { try { return fn(), true; } catch { return false; } };
let obj = {};
try { new Foo(obj); } catch {}
console.log('obj.#first exists', Foo.hasFirst(obj)); // true
console.log('obj.#second exists', Foo.hasSecond(obj)); // false

这其中的重点是 JavaScript 的 class constructor 和多个 #字段 是可以部分初始化的(虽然这里 extends 一个函数的写法可能也是让人无法挪开视线,直拍大腿:你咋这么能呢?但这不是问题重点 :D)。而如果使用 Private-in 来做 class brand check,即严格类型检查,则有可能因为部分初始化而只检查了成功初始化的 #first 但是没检查 #seond 是否存在,导致访问 #second 的时候可能抛出异常。实际场景中我们可能有非常多的 #字段,为了解决这个问题,目前同样有一个的提案 Class Brand Checks 可以帮助解决这种问题。

提案所提出的适用于 #字段 的 in 操作符已经可以在 Chrome 91、Firefox 90 中发行,可供使用。

Stage 2 → Stage 3

提案从 Stage 2 进入到 Stage 3 有以下几个门槛:

撰写了包含提案所有内容的标准文本,并有指定的 TC39 成员审阅并签署了同意意见;ECMAScript 编辑签署了同意意见。

Accessible Object.prototype.hasOwnProperty


提案链接:https://github.com/tc39/proposal-accessible-object-hasownproperty

现在我们就可以通过 Object.prototype.hasOwnProperty 来使用提案所包含的特性。但是直接通过对象自身的 hasOwnProperty 来使用 obj.hasOwnProperty('foo') 是不安全的,因为这个 obj 可能覆盖了 hasOwnProperty 的定义,MDN 上也对这种使用方式进行了警告。

JavaScript 并没有保护 hasOwnProperty 这个属性名,因此,当某个对象可能自有一个占用该属性名的属性时,就需要使用外部的 hasOwnProperty 获得正确的结果...

Object.create(null).hasOwnProperty("foo")
// Uncaught TypeError: Object.create(...).hasOwnProperty is not a function
let object = {
 hasOwnProperty() {
 throw new Error("gotcha!")
object.hasOwnProperty("foo")
// Uncaught Error: gotcha!

所以一个正确的方式就得写成这样繁琐的方式:

let hasOwnProperty = Object.prototype.hasOwnProperty
if (hasOwnProperty.call(object, "foo")) {
 console.log("has property foo")
}

而提案在 Object 上增加了一个 hasOwn 方法,便于大部分场景使用:

let object = { foo: false }
Object.hasOwn(object, "foo") // true
let object2 = Object.create({ foo: true })
Object.hasOwn(object2, "foo") // false
let object3 = Object.create(null)
Object.hasOwn(object3, "foo") // false

Array find from last


提案链接:https://github.com/tc39/proposal-array-find-from-last

这个提案引入了 Array.prototype.findLast 与 Array.prototype.findLastIndex(同样的还有 %TypedArray.prototype%.findLast 与 %TypedArray.prototype%.findLastIndex)。从 Array.prototype.find 和 Array.prototype.findIndex 可以衍生得出这两个新的 API 语义是类似的,不过新的 API 是从数组的尾部开始遍历寻找符合期望的元素。

const array = [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }];
// find/findLast
array.findLast(n = n.value % 2 === 1); // = { value: 3 }
array.findLast(n = n.value === 42); // = undefined
// findIndex/findLastIndex
array.findLastIndex(n = n.value % 2 === 1); // = 2
array.findLastIndex(n = n.value === 42); // = -1

Realms

提案链接:https://github.com/tc39/proposal-realms
标准定义:https://tc39.es/proposal-realms/

提案介绍

Realms 提案为在 JavaScript 程序中以独立的 Global 环境执行 JavaScript 代码的需求提供了一个新的方案

目前提案包含的 API 可以在 JavaScript 中以一定程度的虚拟化来执行不同的程序,并且期望后续能够在多种 JavaScript 环境中如浏览器、Node.js 中无缝兼容。而这些能力在目前的 Web 浏览器上是难以实现的。

提供所设计的 API 十分简单,仅仅只有 3 个函数:

class Realm {
 constructor();
 importValue(specifier: string, bindingName: string): Promise PrimitiveValueOrCallable 
 evaluate(sourceText: string): PrimitiveValueOrCallable;
}

除了 constructor 之外,对于用户来说,实际使用的函数只有 importValue 与 evaluate,我们下面将一个一个来分析。

我们可以看到 importValue 与 evaluate 的返回值都是一个 PrimitiveValueOrCallable(或者这个类型的 Promise)。这意味着什么呢?其实,在 Realms 提案之前,开发者们为了实现类似的全局对象沙盒机制,在浏览器上、Node.js 中都有类似的实现。我们发现这些允许对象跨“执行环境”的机制都存在着严重的对象身份不连续的问题,比如如果我们从 iframe 中获取到一些对象,这些对象在 iframe 的外部拥有着不一样的身份:这是因为每一个 iframe 中都有独立的 Global 与 Object、Array 等 ECMAScript 内置对象。我们看下面这个例子:

const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const iframeArray = iframe.contentWindow.Array;
console.assert(iframeArray !== Array);
const list = iframeArray('a', 'b', 'c');
list instanceof Array; // = false
[] instanceof iframeArray; // = false
Array.isArray(list); // = true

同样的,使用 Node.js 的 vm 模块也存在同样的问题:

const vm = require('vm');
const ctx = vm.createContext();
const vmArray = vm.runInContext('Array', ctx);
console.assert(vmArray !== Array);
const list = vmArray('a', 'b', 'c');
list instanceof Array; // false
[] instanceof vmArray; // false
Array.isArray(list); // true

因此,为了避免出现更多的 footgun,Realms 之间无法直接交换除了原始 JavaScript 值(number, string, bigint, symbol 等,他们的身份是以值确定的)和 Callable (即函数,但是与函数对象有所不同)以外的 JavaScript 值:

Realms 中透出的函数会以一个 Callable 的包装对象返回,这个包装对象上不能访问到原函数对象上的属性,并且只能用来调用(参数也只能是原始 JavaScript 值或者 Callable);可以通过目前的 API 建立对象桥接(如共享 JSON 对象数据),可以参阅 POC https://github.com/caridy/irealm 获取更多信息;目前不支持交互 ArrayBuffer/TypedArray 对象;

除此之外,我们看到 Realm 提供的 importValue 方法即是原生支持 ECMAScript Module 的方案。每一个 Realm 都有自己的模块图,也就是说同一个模块在不同的 Realm 中都需要重新以这个 Realm 的 Global 环境执行并被使用,避免造成数据冲突或者泄露。

不过,常用浏览器 iframe 与 Node.js 的 vm 模块的同学可能注意到了,iframe 中的全局对象中是存在各种常见的浏览器 API 如 setTimeout、URL、TextEncoder 等等。而 Node.js 中的 vm 模块默认初始情况下只有 ECMAScript 原生的内置对象,并不存在如 setTimeout、URL、TextEncoder 等常用 API。那么 Realm 提供的全局对象是否是纯净的 ECMAScript 环境呢?目前提案的建议方案是由平台自行决定是否需要注入平台定义的 API,如浏览器可以将其认为非常重要而通用的 API 如 URL、TextEncoder 注入 Realm 环境,这取决于后续 Web 标准化组织如何定义 Web 标准。

说了这么多,我们来看一看 Realm API 的大致使用例子:

const red = new Realm();
// realm 可以导入模块,并且这些模块会在 realm 自己的环境中执行,不会影响到 realm 外部。
// 通过这个 API,我们也可以获取到模块导出值。当然,这些导出值也会被以
// 类似 `PrimitiveValueOrCallable` 的方式转化成安全的跨 Realm 值。
const redAdd = await red.importValue('./sandboxed-code.js', 'add');
// redAdd 是一个被包装的函数对象,可以通过调用它来间接调用对应 red 中绑定的真实函数。
let result = redAdd(2, 3);
console.log(result === 5); // = true
// 修改外部的 globalThis 不会影响到 realm 中的 globalThis
globalThis.someValue = 1;
// 修改 realm 中的 globalThis 不会影响到外部的 globalThis
red.evaluate('globalThis.someValue = 2');
console.log(globalThis.someValue === 1); // = true
// 调用包装的函数时可以传递一个函数作为参数(或者原始值)
const setUniqueValue =
 await red.importValue('./sandboxed-code.js', 'setUniqueValue');
result = setUniqueValue((x) = x ** 3);
console.log(result === 16); // = true

本文转自网络,原文链接:https://developer.aliyun.com/article/787584
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!
上一篇:在AWS上的架构部署与设计 下一篇:没有了

推荐图文

  • 周排行
  • 月排行
  • 总排行

随机推荐