ECMAScript 双月报告:Pipeline Operator 进入 Stage 2(2021/8)

简介: 这次的 TC39 会议只有两天的时间,如 JavaScript 运算符重载与十进制计算、Wasm-JS 互操作性等议题都延迟到了 10 月份讨论。即使如此,这次会议还是有许多对于开发者来说非常有帮助的议题如 Pipeline 运算符、JavaScript 固定布局对象等有了阶段性进展。

作者|吴成忠(昭朗) Alibaba F2E


这次的 TC39 会议只有两天的时间,如 JavaScript 运算符重载与十进制计算、Wasm-JS 互操作性等议题都延迟到了 10 月份讨论。即使如此,这次会议还是有许多对于开发者来说非常有帮助的议题如 Pipeline 运算符、JavaScript 固定布局对象等有了阶段性进展。



Stage 3 → Stage 4



从 Stage 3 进入到 Stage 4 有以下几个门槛:

  1. 必须编写与所有提案内容对应的 tc39/test262 测试,用于给各大 JavaScript 引擎和 transpiler 等实现检查与标准的兼容程度,并且 test262 已经合入了提案所需要的测试用例;
  2. 至少要有两个实现能够兼容上述 Test 262 测试,并发布到正式版本中;
  3. 发起了将提案内容合入正式标准文本 tc39/ecma262 的 Pull Request,并被 ECMAScript 编辑签署同意意见。


Relative indexing .at() method


提案链接:https://github.com/tc39/proposal-relative-indexing-method


很多时候,类似于 Python 中的数组负值索引可以非常实用。比如在 Python 中我们可以通过 arr[-1] 来访问数组中的最后一个元素,而不用通过目前 JavaScript 中的方式来访问 arr[arr.length-1]。这里的负数是作为从起始元素(即arr[0])开始的反向索引。


但是现在 JavaScript 中的问题是,[] 这个语法不仅仅只是在数组中使用(当然在 Python 中也不是),而在数组中也不仅仅只可以作为索引使用。像arr[1]一样通过索引引用一个值,事实上引用的是这个对象的 "1" 这个属性。所以 arr[-1] 已经完全可以在现在的 JavaScript 引擎中使用,只是它可能不是代表的我们想要表达的意思而已:它引用的是目标对象的 "-1" 这个属性,而不是一个反向索引。


这个提案提供了一个通用的方案,我们可以通过任意可索引的类型(Array,String,和 TypedArray)上的 .at 方法,来访问任意一个反向索引、或者是正向索引的元素。



// 数组
[0, 1, 2, 3].at(-1); // => 3
// 字符串
'0123'.at(-1); // => '3'


这个特性已经在 Chrome 92、Firefox 90 中默认启用。


Object.hasOwn


提案链接: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


这个特性将在 Chrome 93、Firefox 91 中默认启用。



Class static initialization blocks


提案链接:https://github.com/tc39/proposal-class-static-block


自从有了 Class Private Fields,对于类的语法是不断地有新的实践与需求。这个提案提议的 Class Static 初始化块会在类被执行、初始化时被执行。Java 等语言中也有类似的静态初始化代码块的能力,Static Initialization Blocks (https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html


提案中定义的初始化代码块可以获得 class 内的作用域,如同 class 的方法一样,也意味着可以访问类的 #字段。通过这个定义,我们就可以实现 JavaScript 中的 Friend 类了。



let getX;
export class C {
  #x
  constructor(x) {
    this.#x = { data: x };
  }
  static {
    // getX has privileged access to #x
    getX = (obj) => obj.#x;
  }
}
export function readXData(obj) {
  return getX(obj).data;
}


这个特性将在 Firefox 93、Chrome 94 中默认启用。



Stage 1 → Stage 2



从 Stage 1 进入到 Stage 2 需要完成撰写包含提案所有内容的标准文本的初稿。


Change Array by Copy


提案链接:https://github.com/tc39/proposal-change-array-by-copy


Array.prototype 上有非常多十分实用的方法,如 Array.prototype.popArray.prototype.sortArray.prototype.reverse 等等,这些方法通常都是直接就地修改当前的数组对象与其中的元素内容。如果我们需要避免修改原有的数组对象的话,通常我们可以通过 [...arr] 来快速浅拷贝一个数组对象,然后再对这个数组对象调用刚才所说的方法。


这在 Tuple 与 Record 类型正式引入 ECMAScript 之前确实没什么问题。但是如果我们需要引入 Tuple (一种内容不可变的数组),同时 Tuple 想要同样具备 Array.prototype 上的这些便捷方法的话,先有的 Array.prototype 上的就地修改的方法就不再兼容 Tuple 了,而 Tuple 也不能重用这些方法名来使用非就地修改的语义。所以这个提案就准备先为 JavaScript 引入多个就地修改方法的拷贝版,后续 Tuple 也就可以只支持这些拷贝版本的方法。



let arr = [ 3, 2, 1 ];
arr.sort();
arr; // => [ 1, 2, 3 ] 被修改了
arr = [ 3, 2, 1 ];
let sorted = arr.withSorted();
sorted; // => [ 1, 2, 3 ]
arr; // => [ 3, 2, 1 ] 没有被修改


Pipeline operator


提案链接:https://github.com/tc39/proposal-pipeline-operator


如果我们用了比如 lodash 等常见的函数库,我们会发现如果需要连续调用多次 lodash 函数,我们需要不断地包裹调用:



foo(bar(1, baz(x)[0]).method())


又或者是连续调用异步函数:



let text = await (await fetch(...)).text();


我们可以发现这些写法都不易读。实际使用中,读的顺序与执行的顺序也可能是相反的,造成理解上的困难。


而 Pipeline 运算符引入了调用返回值传播语法,让我们可以非常方便地书写类似的链式调用:



// 嵌套式调用
foo(bar(1, baz(x)[0]).method())
// Pipeline 运算符
x |> baz(%)[0] |> bar(1,%).method() |> foo(%)
// 嵌套 await 调用
let text = await (await fetch(...)).text();
// Pipeline 运算符
let text = await fetch() |> await %.text();


目前的 Pipeline 运算符右手操作符可以是任意表达式加上一个 % 占位符代表传播的值,我们可以通过下表来看目前 Pipeline 运算符支持的表达形式:


传统写法

Pipeline 运算符

o.m(x)

x |> o.m(%)

o.m(0, x)

x |> o.m(0, %)

new o.m(x)

x |> new o.m(%)

o[x]

x |> o[%]

x[i]

x |> %[i]

x + 1

x |> % + 1

[0, x]

x |> [0, %]

{ key: x }

x |> { key: % }

await o.m(x)

x |> await o.m(%)

yield o.m(x)

x |> (yield o.m(%))



Stage 0 → Stage 1



从 Stage 0 进入到 Stage 1 有以下门槛:

  1. 找到一个 TC39 成员作为 champion 负责这个提案的演进;
  2. 明确提案需要解决的问题与需求和大致的解决方案;
  3. 有问题、解决方案的例子;
  4. 对 API 形式、关键算法、语义、实现风险等有讨论、分析。
    Stage 1 的提案会有可预见的比较大的改动,以下列出的例子并不代表提案最终会是例子中的语法、语义。


String.isUSVString


提案链接:https://github.com/guybedford/proposal-is-usv-string


JavaScript 字符串都是 UTF-16 编码的字符串。在 Web API 中,我们可以发现有些 API (如 URL、URLSearchParams 等等系列 API)都声明了需要 USVString 作为参数。什么是 USVString?USV 代表 Unicode Scalar Value,即 Unicode 标量值。根据 Unicode 定义,Unicode 的码位(Code Point)可以分成几个类别,分别是图形码(Graphic),格式码(Format),控制码(Control),私有码(Private-Use),代理码(Surrogate),非字符码(Noncharacter),与保留码(Reserved)。而其中的代理码又分成了高位代理码与低位代码码,只有当一个高位代码码与一个低位代理码组合成一个代理码对,才是一个合法的 Unicode 字符。


目前,JavaScript 字符串并不限制这个字符串的值是否是合法的 Unicode 值,比如我们可以编码一个字符串只有高位代理码,而没有低位代理码等等。而如严格的 Web URL API 定义必须要求参数字符串是合法的 Unicode 标量值,因此我们需要有方法能够去区分一个字符串是否是合法的 Unicode 标量值。


这个提案提出给 String 增加一个静态方法 String.isUSVString 来供开发者用来校验字符串是否是合法 Unicode 标量值。



String.isUSVString('\ud800');       // => false
String.isUSVString('\ud800\udc00'); // => true


Array.fromAsync


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


目前,我们可以通过 Array.from(iterator) 来方便地将一个迭代器转换成一个数组来做后续的操作,比如:



function* f () {
  for (let i = 0; i < 4; i++)
    yield i;
}
Array.from(f()); => [0, 1, 2, 3].


随着 Async Generator 在 Web API 与 Node.js API 中应用的范围越来越广,如 Node.js 的 Streams API 也已经实现了 Symbol.asyncIterator 协议,或者是 Web 的 ReadableStream 等等。现在我们可以通过下面的方式便捷地迭代这些 stream 对象,比如文件流:



const fs = require('fs');
async function print(readable) {
  // 设置编码格式为 UTF8
  readable.setEncoding('utf8');
  // 异步迭代 readable stream
  let data = '';
  for await (const chunk of readable) {
    data += chunk;
  }
  // 输出
  console.log(data);
}
print(fs.createReadStream('file')).catch(console.error);


提案期望提供一个如同 Array.from 的内置 Array.asyncFrom 方法,用于从一个异步迭代器从组装出完整的数组,可以便捷地用于开发、调试等等用途:



const fs = require('fs');
async function print(readable) {
  // 设置编码格式为 UTF8
  readable.setEncoding('utf8');
  // 异步迭代 readable stream
  const data = (await Array.asyncFrom(readable)).join('');
  // 输出
  console.log(data);
}
print(fs.createReadStream('file')).catch(console.error);


BigInt Math


提案链接:https://github.com/tc39/proposal-bigint-math


BigInt 在 JavaScript 中提供了任意精度的整型数值计算的能力。不过,目前 JavaScript 中对于 Number 类型的常见数学计算操作如 Math.maxMath.powMath 命名空间下的函数都没有对于 BigInt 的支持。目前如 Web Performance API、Node.js Performance API 都已经提供了基于 BigInt 的 API 实现,对于开发者来说这些常见的数学计算操作支持还是非常有必要性的。


提案提出了将 Math 命名空间下对于 BigInt 来说合理的函数增加 BigInt 支持,如 Math.maxMath.pow 等。而提案希望不包含对于 BigInt 可能造成精度丢失的函数如三角函数(Math.sin 等)、对数函数(Math.log 等)等等函数的支持,避免开发者误用。


Get Intrinsic


提案链接:https://github.com/ljharb/proposal-get-intrinsic


在 JavaScript 中,我们可以对任意对象做任意的操作,比如删除属性、替换方法、增加实现等等,比如我们常见的 Polyfill 或者 Monkey Patch 等方案的基础。而这同样也造成了一定问题,比如我们引入了一个第三方库,而这个第三方库对于固有对象如 Array.prototype 上的方法做了修改,如果这个修改的健壮性与兼容性有问题的话,对于其他的第三方库、应用代码都会造成不可预期的影响,如两个 Polyfill 库互相冲突等,甚至会破坏运行时的内置行为。比如 Node.js 的内置库如 fs 等等或者 Deno 的内置库都是使用 JavaScript 实现的,也就意味着他们都依赖于这些 ArrayMap 等等 JavaScript 固有对象实现,对这些对象的修改同样可能破坏 Node.js 与 Deno 的行为。因为,Node.js 等运行时或者 Polyfill 库为了避免被这些问题影响,都会在加载的第一时间就保存下这些 JavaScript 固有对象的方法引用,后续用户代码、第三方库对于这些内置对象的修改就不会再影响到他们。


而这也要求了固定的启动顺序,也就是说这些需要保存 JavaScript 固有方法的代码都需要第一时间加载,然后才能加载用户代码。比如社区包 get-intrinsic 提供了保存这些固有方法的能力,但是必须作为第一个包被引入。值得注意的是,如 get-intrinsic 等实现是通过遍历所有已知的 JavaScript 固有方法来实现的,也意味着他们的代码包大小是不可忽视的一个问题,如 get-intrinsic 包有9.7KB。


目前 JavaScript 有什么固有对象与方法?ECMA262: 已知的固有对象(https://tc39.es/ecma262/#sec-well-known-intrinsic-objects


另外,有些 JavaScript 的固有方法是必须通过特定语法来获取的,比如 GeneratorFunction 与 AsyncGeneratorFunction:



const GeneratorFunction = (function* (){}).constructor;
const AsyncGeneratorFunction = (async function* (){}).constructor;


get-intrinsic 如果需要能在低版本的 JavaScript 环境中也能正常运行,就必须通过 eval 等方式来间接执行 JavaScript 代码而避免整个文件因为不支持的语法而无法执行。而 eval 的问题也是显而易见的,它可能会与 CSP 冲突而无法使用。


所以提案期望引入一个内置的 JavaScript 函数来直接获取 JavaScript 的固有方法、对象,这样就不需要依赖于特定的语法就可以访问这些固有方法,给 Node.js 等运行时、各种 Polyfill 库都提供了便利性。



const GeneratorFunction = getIntrisic("%GeneratorFunction%");
const AsyncGeneratorFunction = getIntrinsic("%AsyncGeneratorFunction%");


Fixed layout objects


提案链接:https://github.com/syg/proposal-structs/


近期,关于 WebAssembly GC 提案的落地非常惹人注目。作为与 WebAssembly 交互的第一梯队语言,JavaScript 自然也需要一个良好的能力来与 Wasm GC 来交互。这里说的“交互”,目前提案将这个能力限制在了 JavaScript 能够与 Wasm 互相访问,并没有以两者底层实现共享为目标。


WasmGC 中的对象在 JavaScript 暴露的结构就是提案所描述的固定布局对象(或者说 struct)。不过在 Wasm 中这个对象的字段是有额外的类型检查的,而 JavaScript 中这个固定布局对象并没有类型检查、限制一说。JavaScript Struct 后续当然也可以反哺到 Wasm 中作为 WasmGC 的对象,不过这部分工作提案期望留给未来后续来设计,因为这可能需要引入 Struct 的字段类型检查与限制。


对象传递方向

Wasm -> JS

JS -> Wasm

结构体实例

这个提案的目标

未来提案的目标

类型与反射

不是 TC39 的目标

不是 TC39 的目标


另外,大型应用如 GSuite(Google 套件)、Microsoft Office、TypeScript Compiler 都或多或少地碰到了性能问题,他们希望能在 JavaScript 中有更方便的内存共享、多线程工作负载的方式来提升应用表现。在此之前,在 JavaScript 中我们已经可以使用 SharedArrayBuffer 来共享内存。不过,使用 SharedArrayBuffer 操作复杂对象数据时,通常我们还需要通过一层包装来序列化与反序列化,有额外的成本。如果我们能直接在不同的 JavaScript 执行上下文里直接共享一系列的对象、结构体的话,就能以更具有表达能力的方式来操作共享内存。


目前,提案所提出的语法与能力都还是处于草稿阶段,提案提出了两种类型的 struct,第一种既是普通、非共享内存型。与 class 实例、普通 JavaScript 对象的区别是他们默认就是已经 seal 的对象 Object.seal(obj)  (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal),不能动态添加新的字段:



struct class Box {
  x;
  getX() { return this.x; }
}
let box = new Box();
// x 已经声明过
box.x = 42;
// 返回 42
box.getX();
// Structs 相当于 Object.seal 过的对象,不能添加新字段
assert.throws(() => {
  box.y = 8.8;
});


另外一种,就是可以用于 Web API 中的 MessagePort 等 API 传递到其他 JavaScript 执行环境中的共享内存型 struct。相比于普通的 struct 来说,共享的 struct 不能有 constructor 以外的方法,不过可以有静态方法:



shared struct class SharedBox {
  constructor(x) { this.x = x; }
  x;
}
let sharedBox = new SharedBox();
// x 声明过
sharedBox.x = 42;
assertThrows(() => {
  // y 没有声明过
  sharedBox.y = 84;
});
// 将共享 struct 传递给 worker
let worker = new Worker('worker.js');
worker.postMessage({ sharedBox });
// 字段类型可以随意变更,就如同普通 JavaScript 对象一样
sharedBox.x = "main";



结语



由贺师俊牵头,阿里巴巴前端标准化小组等多方参与组建的 JavaScript 中文兴趣小组(JSCIG,JavaScript Chinese Interest Group)在 GitHub 上开放讨论各种 ECMAScript 的问题,非常欢迎有兴趣的同学参与讨论:esdiscuss

相关文章
|
JavaScript
JS设置日期为0时0分0秒
项目中经常要给设置默认值,搜索从哪天开始,这时候,如果直接通过new Date()来获取时间,会有时分秒,如果快速设置为0时0分0秒?
791 0
|
8月前
|
SQL 关系型数据库 数据库
Postgresql入门之psql用法详解(一)- 命令行参数详解
`psql` 是 PostgreSQL 的命令行客户端,支持交互式或批量执行 SQL 查询。它提供丰富的元命令、脚本自动化、格式化输出(如 CSV、HTML)、连接 URI/服务配置及 LDAP 集成,并可通过命令行选项控制连接、事务与错误处理,适用于日常操作与系统管理。
|
弹性计算 JavaScript Ubuntu
WebSocket协议相关的测试命令工具使用简介
本文介绍了针对WebSocket的测试工具wscat和websocat的基本使用方法,以及通过curl命令测试HTTP/HTTPS协议的方式。对于WebSocket,直接使用curl测试较为复杂,推荐使用wscat或websocat。文中详细说明了这两种工具的安装步骤、常用参数及连接示例,例如在ECS上开启8080端口监听并进行消息收发测试。此外,还提供了curl命令的手动设置头部信息以模拟WebSocket握手的示例,但指出curl仅能作为客户端测试工具,无法模拟服务器。
4172 5
|
机器学习/深度学习 存储 人工智能
数据结构在实际开发中的广泛应用
【10月更文挑战第20天】数据结构是软件开发的基础,它们贯穿于各种应用场景中,为解决实际问题提供了有力的支持。不同的数据结构具有不同的特点和优势,开发者需要根据具体需求选择合适的数据结构,以实现高效、可靠的程序设计。
1028 63
|
传感器 数据采集 监控
物联网 GE-PREDIX
GE-Predix 是一个由通用电气公司开发的工业互联网平台,旨在为工业设备提供连接、分析和管理服务。它支持设备数据的收集与分析,帮助企业优化运营效率,实现智能化转型。
1331 2
|
安全 数据处理 数据中心
不同类型的光纤电缆及其应用特点
【10月更文挑战第22天】
1027 6
|
计算机视觉 Python
10个使用NumPy就可以进行的图像处理步骤
这篇文章介绍了使用NumPy进行图像处理的10个基本步骤,包括读取图像、缩小图像、水平和垂直翻转、旋转、裁剪、分离RGB通道、应用滤镜(如棕褐色调)、灰度化、像素化、二值化以及图像融合。通过这些简单的操作,读者可以更好地掌握NumPy在图像处理中的应用。示例代码展示了如何实现这些效果,并配有图像结果。文章强调这些方法适合初学者,更复杂的图像处理可使用专门的库如OpenCV或Pillow。
635 5
|
Dubbo Java 应用服务中间件
微服务技术栈选型,看了这个别的可以不用看了
本文由PPmoney架构师敖小剑分享:微服务的核心技术,目前可选的开源微服务框架,以及为微服务提供支撑的基础设施。
18469 136