汇总近期 TC39 比较关键的提案,是一篇非常通俗易懂又有料的文章,建议 PC 端观看~
最近翻看 TC39,一些期待已久的提案居然已经落地了,一些阻塞了很久的提案也有了新的进展,这次分享我们就来汇总介绍,包含近半年全部已经落地的提案,以及近期有进展、比较有趣或者与开发相关的尚未落地的提案。
TC39:Ecma International's TC39 is a group of JavaScript developers, implementers, academics, and more, collaborating with the community to maintain and evolve the definition of JavaScript.
一、TC39 提案过程
回顾一下,TC39 提案过程分为 Stage 0 ~ 4,共 5 个阶段,分别代表 起草、提案、草案、候选、完成。
Stage 0:
所有人都可以向 ECMA 262 提出提案,但是必须满足:
- 有 champion (目前中文叫 责编) 支持
- 或者由 来自 TC39 会员或已注册为 TC39 撰稿人的非会员提交给 TC39 大会评审,且没有被明确拒绝
才能进入 Stage 0,并收录到 proposals/stage-0-proposals.md[1] 。
Stage 1
要想进入 Stage 1 必须要满足 6 个条件:
- 一个 TC39 成员作为 champion
- 有一个完整的当前遇到什么问题、如何解决的文章
- 有使用例
- 有 API 设计
- 有潜在的与其他提案冲突、或是实现复杂性、可能遇到的问题的考虑
- 有一个 repository 来存放上述这些内容 (如果 stage 0 的时候这个 repo 不在 tc39 组织下,需要 transfer 给 tc39)
同时在这个阶段也应当有 polyfill 或者 demo 用于演示
Stage 2
Stage 1 这些复杂要求顺利通过,且满足
- 有符合 ECMAScript 的语法、API (此时可以有 TODO)
- 描述尽可能完整 且可供实现后,经过 TC39 会议评审,就可以推动到 Stage 2 了。进入这个阶段,意味着这个提案非常有可能被最终通过。提案的内容也不能大改,只能增量修改。
Stage 3
当 Stage 2 留下的 TODO 已经补完,所有的语法、API 都完整实现、reviewer (TC39 指定) 以及全部 ECMAScript 编辑都签署完成后,TC39 会议评审通过就可以推动进入 Stage 3。此时浏览器会开始实现这个标准,并接收用户反馈。这个阶段的提案文本只能针对用户反馈发现的关键问题进行修改。
Stage 4
当满足:
- Test262 测试已经准备就绪并合入测试集
- 至少有 2 个实现通过了 Test262 测试
- 该实现已经有了显著的实践应用,比如有 2 个 VM(比如 Node、浏览器)已经不需要设置 flag 就能执行了
- 包含文本的 Merge Request 已经提给了 ecma262 仓库
- 所有的 ECMAScript 编辑都批准了这个 Merge Request
进入 Stage 4 的提案基本已经认为完成了。会在下一次发布时成为 JavaScript 标准的一部分。
二、近期关键提案汇总
下面我们就来介绍近期的提案。
2.1 Stage 4 (已落地)
Stage 4 的提案已经确认写入 ECMAScript 标准中,且至少有 2 个浏览器已经实现并通过测试。
2.1.1 .at() (proposal-relative-indexing-method)
取出数组中的倒数某个值,或者字符串中的后数几个字符,在 JavaScript 中一直是比较麻烦的事情,比如 arr[arr.length - 1] 或者 arr.slice(-1)[0]?
现在,不用这么麻烦了,我们只要用 arr.at(-1) 就好了。
同时,这个也给 string、TypedArray 添加了相同的功能。
Array.prototype.at
String.prototype.at
同类提案:
- proposal-array-last`[2]:Stage 1,为 Array 添加了 lastItem 和 lastIndex
2.1.2 Error Cause (proposal-error-cause)
我们正在实现一个方法。在这个方法中,调用的某个外部方法抛出了异常,而且:
- 我们的方法无法处理它;
- 我们和调用方约定了会抛出的异常,不能抛出未约定的异常;
- 我们也不能直接 new 一个新的 Error,因为会丢失原始 Error 的上下文。
如果 Error 能串起来,那将极大地方便我们去调试并定位问题。而 Error Cause 的出现,在语言层面上解决了这个进退两难的问题。在这之前,我们的应用可能是这样解决的:
- class BusinessError extends Error {
- constructor(code, message, { cause }) {
- super(message);
- this.code = code;
- this.cause = cause;
- }
- }
- async function addName() {
- const { ctx } = this;
- try {
- const res = await ctx.fetch('http://examples.com/getxxx');
- return res.json();
- } catch (e) {
- throw new BusinessError(500, 'request failed', { cause: e });
- }
- }
- // production response: { code: 500, message: 'request failed' }
- // boe response: { code, message, error: { stack, cause: { ... } } }
那么,为什么不用这种继承方法创建自定义的 Error Class,而还需要在语言层面做修改呢?
提案者本身也回答了这个问题,他说:确实有很多办法能够实现这一提案想要的行为,但是,如果在语言层面上定义了 cause,调试工具就可以更可靠地用这个信息,而不是去要求开发者以某种方式抛出 Error 了。
While there are lots of ways to achieve the behavior of the proposal, if the cause property is explicitly defined by the language, debug tooling can reliably use this info rather than contracting with developers to construct an error properly.
如提案者所说,FireFox 的调试工具实现了这个 cause 将 Error 串起来的功能,而 chrome 目前似乎还没有跟上。
2.1.3 Accessible Object.prototype.hasOwnProperty
判断某个字段是否存在于对象中,该怎么做?xxx in obj ?不对,如果对象的原型上有这个字段,会错误判断。
当有这种需求时,就必须用到 Object.prototype.hasOwnProperty 了。但这一方法使用起来,还是非常的迷惑:
- let hasOwnProperty = Object.prototype.hasOwnProperty
- if (hasOwnProperty.call(obj, "foo")) {
- console.log("has property foo")
- }
为什么不是 obj.hasOwnProperty() ?因为对于 Object.create(null) 等魔法产生的对象,并不含 Object 原型链上的方法。
这个提案为 Object 添加了新的静态方法 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
2.1.4 Class Fields
这 3 个提案 TypeScript 早已实现,且已经在上半年正式落地了,而且我们已经在项目中用了很久,本不该在本文的讨论范围,但因为下面 2 个提案都与 class 有关,我们回顾一下。
- Field declarations - 字段可以直接定义在 class 类中
- Private fields/methods - 可以定义私有字段、方法。私有成员只能在类中定义、只有当前类可以访问、没有后门
- Private getter/setters - 可以定义私有 getter / setter
- Static public fields/methods - 公共方法、字段可以定义为静态
- Static private fields/methods - 私有方法、字段可以定义为静态
比如提案中提到,自定义一个点击就可以 +1 的 Element:
- class Counter extends HTMLElement {
- #count = 0;
- get #x() { return #count; }
- set #x(value) {
- this.#count = value;
- window.requestAnimationFrame(this.#render.bind(this));
- }
- #clicked() {
- this.#x++;
- }
- constructor() {
- super();
- thisthis.onclick = this.#clicked.bind(this);
- }
- connectedCallback() { this.#render(); }
- #render() {
- thisthis.textContent = this.#x.toString();
- }
- }
- window.customElements.define('num-counter', Counter);
这个内部的 count 值是没有任何办法获取的(devtools 协议的某些手段除外)。
2.1.5 Ergonomic brand checks for Private Fields
这一提案是私有字段/方法的扩展。
假如,我们需要在公有方法中设置某个私有字段,但不知道传参是不是我们的对象,该怎么办呢?
如果没有 in,以前,我们可能需要用到 try 和 catch 这么做:
- class C {
- #a;
- static isC(obj) {
- try {
- obj.#a;
- return true;
- } catch {
- return false;
- }
- }
- }
但这一操作有个问题,如果这个 #a 是个可能会 throw Error 的 getter,怎么办呢?
- class C {
- get #a() {
- throw new Error('you cannot get me!');
- }
- }
好像没办法,所以,我们需要用这一提案的 #a in obj (注意,与公有方法不同,这个判断左侧不是字符串,公有方法的判断是 'a' in obj)
这是一个应用的例子:
代码
- class C {
- #a;
- // 如果我们认可这个对象
- // 就将它设成 verified
- static verifyAndSet(obj) {
- if (#a in obj) {
- obj.#a = 'verified!'
- }
- }
- }
应用
对于 C 对象,我们能正确设置 #a
- class D {
- #a;
- readA() {
- return this.#a;
- }
- }
D 不是我们的对象,因此不会设置 #a (也没办法设置)
- class E extends C {
- #a;
- readA() {
- return this.#a;
- }
E 虽然 extends C,也会设置 #a,但这个 #a 不是 E 的 #a,私有成员只能自己访问,E 不能访问 C 的成员。
那么,为什么不是 instanceof ?提案者:安全,安全,还是安全。instanceof 是可以被欺骗的。而 private fields是没有任何办法从外部访问的,可以确保这个对象就是自己类的实例,就是我们想要的对象。这也是 brand check 的核心。
2.1.6 Class Static Block (proposal-class-static-block)
如果 class 的一些 static fields 需要运算一个表达式才能设置,之前的 class fields 提案似乎没办法实现,因此,这个提案可以作为补充。
例如,同时设置 y 和 z 的值...
- class C {
- static x = ...;
- static y;
- static z;
- static {
- try {
- const obj = doSomethingWith(this.x);
- this.y = obj.y;
- this.z = obj.z;
- }
- catch {
- this.y = ...;
- this.z = ...;
- }
- }
- }
static 代码块是在 ClassDefinitionEvaluation 阶段执行的,也就是说,是在创建 class 的时候执行的,这也让它有能力访问并设置 private fields。它还有能力暴露特权函数给外部,理论上,外部函数没办法访问类的私有成员,但如果是 static 代码块赋给外部的,就可以突破这个限制。
- let getX;
- class C {
- #x;
- constructor(x) {
- this.#x = { data: x };
- }
- static {
- getX = (obj) => obj.#x;
- }
- }
- getX(new C('private data'));
- // { data: 'private data' }
2.2 Stage 3 (已确定,实现中)
Stage 3 的提案基本上已经确定并交由内核实现,甚至部分浏览器已经实现。这一阶段的提案只能针对关键问题进行修改。
2.2.1 Array find from last
有 indexOf 和 lastIndexOf,有 find 和 findIndex,那为什么没有 findLast 和 findLastIndex 呢?现在它终于快来了...
2.2.2 Import Assertions
我们经常会用到类似
- import Component from './component';
- import data from './data.json';
- import styles from './index.module.css';
这样的引入,但是它们都是 webpack 等打包工具帮我们处理的。随着 json modules 和 css modules 加入 Web 标准,原生 JavaScript 也要考虑引入对它们的支持。
但不能就这样引入!因为...假如,我们在浏览器中执行
- import sheet from './styles.css';
而后端给我们返回了
- Content-Type: application/javascript; charset=utf8;
- alert('you are rickrolled!');
emmm... 这可不好。
为什么不用扩展名来区分呢?因为扩展名不是资源的一切,我们有太多资源没有扩展名了。Content-Type 由后端掌控,不够安全,因此,提案中设计了 import assertion 的方式。
- // 同步的
- import json from "./foo.json" assert { type: "json" };
- // 异步的
- const cssModule = await import('./style.css', { assert: { type: 'css' }});
2.2.3 proposal-temporal
JavaScript 的 Date 有多难用,不支持 format、很难用的 setDate、... 就不用说了,更不用说 month 还是从 0 开始的。
大概 tc39 的成员们也觉得 Date 没有改造价值了,因此干脆写个新的 Temporal 来代替它。整个 Temporal 对象重新实现了 Date,可以参考以下文档(中文):
https://tc39.es/proposal-temporal/docs/zh\_CN/index.html[3]
简单说来,它将时间分为 壁钟时间(本地时间) 和 确切时间(UTC时间) 两种。壁钟时间受当地的时区、历法或是夏令时之类的影响,而确切时间则表示的就是对应的 UTC 时间点。Temporal 有以下几种核心方法:
- Temporal.Instant:确切时间,无时区、历法等信息
- Temporal.PlainDate
- Temporal.PlainTime
- Temporal.PlainDateTime
- Temporal.PlainYearMonth
- Temporal.PlainMonthDay:壁钟时间,只表示某一时刻,不包含时区历法
- Temporal.ZonedDateTime:表示包含当前的时区历法的某一时刻
- Temporal.Now:当前时刻,包含当前的时区、历法信息
- Temporal.TimeZone:只表示时区
- Temporal.Calendar: 只表示历法
- Temporal.Duration:只表示时间间隔,不包含其他任何信息
小测验,下面这两条语句的输出是什么?
- const timeZone = Temporal.TimeZone.from('Asia/Shanghai');
- timeZone.getInstantFor('2000-01-01T00:00');
- timeZone.getPlainDateTimeFor('2000-01-01T00:00Z');
- 上海时区和北京时区一样,都是 UTC+8,无夏令时
- getInstantFor 想要得到的是确切时间,需要用 壁钟时间 与上海时区 换算。那么,在上海看到钟上显示的时间是 2000-01-01 00:00,确切时间应该就是 1999-12-31T16:00:00Z。
- getPlainDateTimeFor 想要得到的是壁钟时间,我们知道确切时间是 2000-01-01T00:00Z,那么上海的钟上应该应该是 2000-01-01T08:00:00。
Temporal 有对应的字符串表示,由这几部分组成Not Final:
一些例子🌰:
构造、修改、比较、运算:
- dt = Temporal.PlainDateTime.from({
- year: 1995,
- month: 12,
- day: 7,
- hour: 3,
- minute: 24,
- second: 300,
- millisecond: 0,
- microsecond: 3,
- nanosecond: 500
- });
- // Temporal.PlainDateTime <1995-12-07T03:24:59.0000035>
- dt.with({ year: 2000, day: 30 })
- // Temporal.PlainDateTime <2000-12-30T03:24:59.0000035>
- dt.add / substract(Temporal.Duration) = Temporal.PlainDateTime
- dt.until / since(Temporal.PlainDateTime) => Temporal.Duration
duration.round:比如,我们有时候会需要把 134 秒 转换为 x 分 x 秒的形式,Duration 就非常适合我们
- a = Temporal.Duration.from({ seconds: 134 });
- b = a.round({ largestUnit: 'day' });
- b.toLocaleString();
- // 我们期待的是 2 分 14 秒,但实际上暂时不能这么做,因为
- // 提供国际化格式化 Duration 的 Intl.DurationFormat 还在实现中
- // largestUnit 是必须的,不然它不会自动进位
- // 如果 largestUnit 是月或者年,就必须提供起点,比如某月的天数不同,某年的天数不同
- // 考虑到夏令时等因素,甚至还需要提供时区和历法
2.2.4 ShadowRealm
(这个提案以前叫 Realm)
设想一些场景:
1. 我们需要执行一些代码,但这个代码需要我们刚才讲的 Temporal 的 Polyfill,于是它直接 window.Temporal = TemporalPolyfill。执行它污染了我的作用域。仅仅一个变量可能还可控一些,假如它引入了个 core-js,那就不得了了。我们想保护好自己
2. 我想让我的代码执行在原生且安全的环境中,不希望受到别人影响,比如,外面的脚本可能劫持了我的一些方法,比如 garfish 就劫持了我的 localStorage、insertElement 等方法,我想要原生对象做一些操作。
3. 我是个子应用,我想让我的应用不受其他应用干扰,也不要干扰到其他应用(garfish 通过给子应用的 context 塞假的 globalThis 对象来实现)。
ShadowRealm 就是来解决这些问题的!它让每一段可信的 JavaScript 都有能力执行在隔离的领域中,获得干净的原生环境,类似于新的 context,不受任何影响,也不影响其他人。
- declare class ShadowRealm {
- constructor();
- importValue(specifier: string, bindingName: string): Promise<PrimitiveValueOrCallable>;
- evaluate(sourceText: string): PrimitiveValueOrCallable;
- }
这也相当于给 JavaScript 添加了新的隔离环境的方式。以前我们只有 context,现在还有 realm 了。
详细了解,可以参考以下文档:
https://github.com/tc39/proposal-shadowrealm/blob/main/explainer.md
2.3 Stage 2 (待确定,很可能实现)
这一部分的提案不管讲得多详细,看起来多么 promising,也不得不贴上 Not Final 的标签。反复横跳的事情时有发生,对于它们,可以先了解一下大致思路。
2.3.1 Decorators
曾经非常有潜力的装饰器提案,花了几年的时间大改才进展到 stage 2,被放弃后又被重新提出,目前又进展到 stage 2 了。
这一版本的 Decorators 目标有 3 个主要能力:
- 将被装饰的值替换为同类值,比如,字段替换为另一个字段,方法替换为另一个方法,类替换为另一个类...等等
- 为被装饰的值添加 元数据,支持 元编程
- 通过元数据访问被装饰的值,不管是公有还是私有
目前它的定义大概是这样的,还在不断变化中:
- type Decorator = (value: Input, context: {
- // 要装饰的是什么
- kind: 'class' | 'method' | 'getter' | 'setter' | 'field' | 'auto-accessor';
- // 目标的名称,或者私有元素的可读名称
- name: string | symbol;
- // 访问方法,仅私有元素存在,提供访问和修改私有元素的方式
- access?: {
- get?(): unknown;
- set?(value: unknown): void;
- };
- // 是否为私有
- isPrivate?: boolean;
- // 是否为静态
- isStatic?: boolean;
- // 如果是 @init: 的装饰器,提供初始化方法
- addInitializer?(initializer: () => void): void;
- // 获取和设置元数据
- getMetadata(key: symbol);
- setMetadata(key: symbol, value: unknown);
- }) => Output | void;
2.3.1.1 简单示例
以简单的 @logged 为例:
- function logged(value, { kind, name }) {
- if (kind === "method") {
- return function (...args) {
- console.log(`starting ${name} with arguments ${args.join(", ")}`);
- const ret = value.call(this, ...args);
- console.log(`ending ${name}`);
- return ret;
- };
- }
- }
- class C {
- @logged
- m(arg) {}
- }
- new C().m(1);
- // starting m with arguments 1
- // ending m
- value: 什么也不做的 m 函数
- kind: 'method'
- name: 'm'
同理 'class' | 'method' | 'getter' | 'setter' | 'field' 都是这样的。
2.3.1.2 Auto accessor
再来说说这个 auto accessor,这个提案给类字段加了个 accessor 关键字,大概是自动生成一个私有字段的 getter 和 setter:
- class C {
- accessor x = 1;
- }
- // 约等于这样:
- class C {
- #x = 1;
- get x() {
- return this.#x;
- }
- set x(val) {
- this.#x = val;
- }
- }
- // 当然这个东西也能生成私有的或者静态的
- class C {
- static accessor x = 1;
- accessor #y = 2;
- }
这个有什么用?@decorated 就有用了:
- function logged(value, { kind, name }) {
- if (kind === "auto-accessor") {
- let { get, set } = value;
- return {
- get() {
- console.log(`getting ${name}`);
- return get.call(this);
- },
- set(val) {
- console.log(`setting ${name} to ${val}`);
- return set.call(this, val);
- },
- initialize(initialValue) {
- console.log(`initializing ${name} with value ${initialValue}`);
- return initialValue;
- }
- };
- }
- // ...
- }
- class C {
- @logged accessor x = 1;
- }
- let c = new C();
- // initializing x with value 1
- c.x;
- // getting x
- c.x = 123;
- // setting x to 123
2.3.1.3 @init:
如果 decorator 是 @init: 开头,那么它会额外获得一个 addInitializer 方法,会在被装饰的目标已经初始化完毕后执行。
- 对于类,会在类定义完成后执行。
- 对于类字段和类方法,会在类实例初始化时,对应字段和方法初始化完成后执行。
- 对于静态类字段和静态类方法,会在类定义时,对应字段和方法初始化完成后执行。
如 @init 一个自定义元素
- function customElement(name) {
- (value, { addInitializer }) => {
- addInitializer?.(function() {
- customElements.define(name, this);
- });
- }
- }
- @init:customElement('my-element')
- class MyElement extends HTMLElement {
- static get observedAttributes() {
- return ['some', 'attrs'];
- }
- }
或者生成一个不管怎样都 bind this 的方法
- function bound(value, { name, addInitializer }) {
- addInitializer(function () {
- this[name] = this[name].bind(this);
- });
- }
- class C {
- message = "hello!";
- @init:bound
- m() {
- console.log(this.message);
- }
- }
- let { m } = new C();
- m(); // hello!
2.3.1.4 metadata
Decorators 提案还添加了一个新的 Symbol,Symbol.metadata,可以用来访问 setMetaData 中设置的元数据。
而 metadata 分为 constructor、public 和 private,例如:
- const MY_META = Symbol();
- function myMeta(value,
- context) { context.setMetadata(MY_META, 'metadata');
- }
- @myMeta
- class C {
- @myMeta a = 123
- @myMeta b() {}
- @myMeta #c = 456;
- @myMeta static x = 123;
- @myMeta static y() {}
- @myMeta static #z = 456;
- }
- C.prototype[Symbol.metadata][MY_META];
- // {
- // public: {
- // a: 'metadata',
- // b: 'metadata',
- // },
- // private: ['metadata']
- // }
- C[Symbol.metadata][MY_META];
- // {
- // constructor: 'metadata',
- // public: {
- // x: 'metadata',
- // y: 'metadata',
- // },
- // private: ['metadata']
- // }
这一部分的设计思路是让装饰同一个成员的 decorator 间有能力互相协调,不至于冲突(目前我还没看出有什么用处)。
2.3.2 Pipeline Operator
这个提案在进入 stage 2 之前,有3 个关于 pipeline 的竞争提案在 stage 1 徘徊了很久,分别是 minimal、fsharp、~~smart(已废弃)~~ 。但是... 真正进入 stage 2 的却是目前的 hack-style pipline operator。
这个是我非常期待的提案之一,因为会极大提升我们的代码可读性。想象这样一个场景:
- const obj = {
- '1': { id: '1', name: '小明' },
- '2': { id: '2', name: '小刚' },
- }
- _.union(_.values(_.mapValues(obj, i => i.name)), ['小张', '小刚'])
- // ['小明', '小刚', '小张']
出现了某个函数参数是另一个函数的结果,又作为参数传给另一个函数的情况。如果我们用 Pipeline Operator (hack-style) 的话,看起来就比较清晰了:
- obj
- |> _.mapValues(%, i => i.name) // { '1': '小明', '2': '小刚' }
- |> _.values(%) // ['小明', '小刚']
- |> _.union(%, ['小张', '小刚']) //
- ;
- // ['小明', '小刚', '小张']
这样是不是清晰多了。它非常适合解决数据需要串行处理的情况。
hack style 这一提案与其他提案不同的是,对于 % 占位符有强要求,即使是作为函数的唯一参数,也必须使用。相比其他方式,它可以更全面地涵盖 函数调用 %.()、 await %、(yield %) 等场景。
2.3.3 Destructuring Private
proposal-destructuring-private[6]
小小改动,作为 class fields 提案的补全。
private fields 也可以 destruct 了,但必须提供 alias。
- class Foo {
- #x = 1;
- constructor() {
- const { #x: x } = this;
- console.log(x); // => 1
- }
- }
2.3.4 Array Grouping
大概就是将 lodash 的 groupBy 方法引入 JavaScript 中。
- const array = [1, 2, 3, 4, 5];
- array.groupBy(i => {
- return i % 2 === 0 ? 'even': 'odd';
- });
- // => { odd: [1, 3, 5], even: [2, 4] }
2.3.5 Change Array by Copy
proposal-change-array-by-copy[8]
数组里面有些方法是原地操作的,比如 reverse sort splice ...
拷贝一份再操作又很麻烦,能不能直接返回给我一个新的数组呢?
- Array.prototype.withReversed() -> Array
- Array.prototype.withSorted(compareFn) -> Array
- Array.prototype.withSpliced(start, deleteCount, ...items) -> Array
- // [3, 3, 3].withAt(1, 1) => [1, 3, 3]
- Array.prototype.withAt(index, value) -> Array
2.3.6 Module Blocks
一个 Module 一定要放在独立的文件里?目前是,但,这个提案试图改变这一点...
是不是可以把所有的模块打包到一个文件里?
- let modulemoduleBlock = module {
- import { add } from 'lodash-es';
- export let y = add(1, 2);
- };
- let moduleExports = await import(moduleBlock);
- moduleExports.y === 3;
2.3.7 Record & Tuple
不可修改的对象和数组,提案目前的想法是:
- 创建一个不能修改的记录、元组,其中只能包含基本类型、记录和元组 和 封装📦的类型
- 相等比较时,基本类型对值进行比较,封装类型根据引用进行比较
- 提供一些修改、封装、解除封装的方法
- 提供除更新外其他对 object / array 的操作方法
- const record = #{
- a: #{
- foo: "string",
- },
- b: #{
- bar: 123,
- },
- c: #{
- baz: #{
- hello: #[
- 1,
- 2,
- 3,
- ],
- },
- },
- };
- //
- const ship1 = #[1, 2];
- const ship2 = [-1, 3];
- function move(start, deltaX, deltaY) {
- return #[
- start[0] + deltaX,
- start[1] + deltaY,
- ];
- }
- const ship1Moved = move(ship1, 1, 0);
- const ship2Moved = move(ship2, 3, -1);
- console.log(ship1Moved === ship2Moved); // true
- //
- const myObject = { x: 2 };
- const record = #{
- name: "rec",
- data: Box(myObject)
- };
- console.log(record.data.unbox().x);
- record.data.unbox().x = 3;
- console.log(myObject.x); // 3
- console.log(record === #{ name: "rec", data: Box(myObject) }); // true
2.4 Stage 1 (新功能)
提案通常会在 Stage 1 停留几年,可能会有很多变化,也可能会随时被废弃,因此这里只选取两个我比较期待的,也只会大概说明它的思路。
2.4.1 Partial Application
proposal-partial-application[11]
本质上就是 curry ,我们有时候,需要创建某些参数已经确定的函数,这一提案可以解决这种需求。
- const add = (x, y) => x + y;
- // apply from the left:
- const addaddOne = add~(1, ?);
- addOne(2); // 3
- // apply from the right:
- const addaddTen = add~(?, 10);
- addTen(2); // 12
2.4.2 Do Expressions
在写代码的时候出现这种情况...
- type Type = 'cat' | 'dog' | 'human';
- const itemType: Type = '...';
- let Component;
- if (itemType === 'cat') {
- Component = Cat;
- } else if (itemType === 'dog') {
- Component = Dog;
- } else if (itemType === 'human') {
- Component = Human;
- }
- <Component />;
这样用三目非常麻烦,代码可读性很差,而用这种方式或者 switch 还是怎样都没办法在一个语句内完成,因此,引入 do expression 可以很好的解决这个问题。
- let Component = do {
- if (itemType === 'cat') Cat;
- else if (itemType === 'dog') Dog;
- else if (itemType === 'human') Human;
- };
- return <Component />;
三、了解更多
提案们:
https://github.com/tc39/proposals[13]
TC39 中文站:
TC39 会议记录:
https://github.com/tc39/notes/tree/master/meetings[15]
参与中文讨论:
https://github.com/JSCIG/es-discuss/discussions[16]