跳到主要内容

ECMAScript 2023 新特性

1. Array 支持通过副本更改

为什么会有这个提案呢?

我们知道,大多数的数组方法都是非破坏性的,也就是说,在数组执行该方法时,不会改变原数组,比如 filter() 方法:

const arr = ['a', 'b', 'b', 'a'];
const result = arr.filter(x => x !== 'b');
console.log(result); // ['a', 'a']

当然,也有一些是破坏性的方法,它们在执行时会改变原数组,比如 sort() 方法:

const arr = ['c', 'a', 'b'];
const result = arr.sort();
console.log(result); // ['a', 'b', 'c']

在数组的方法中,下面的方法是具有破坏性的:

  • reverse()
  • sort()
  • splice()

如果我们想要这些数组方法应用于数组而不改变它,可以使用下面任意一种形式:

const sorted1 = arr.slice().sort();
const sorted2 = [...arr].sort();
const sorted3 = Array.from(arr).sort();

可以看到,我们首先需要创建数组的副本,再对这个副本进行修改

因此改提案就引入了这三个方法的非破坏性版本,因此不需要手动创建副本再进行操作:

  • reverse() 的非破坏性版本: toReversed()
  • sort() 非破坏性版本: toSorted(compareFn)
  • splice() 非破坏性版本: toSpliced(start, deleteCount, ...items)

该提案将这些函数属性引入到 Array.prototype

  • Array.prototype.toReversed() -> Array
  • Array.prototype.toSorted(compareFn) -> Array
  • Array.prototype.toSpliced(start, deleteCount, ...items) -> Array
  • Array.prototype.with(index, value) -> Array

除此之外,该提案还还提出了一个新的非破坏性方法: with()

该方法会以非破坏性的方式替换给定 index 处的数组元素,即 arr[index]=value 的非破坏性版本

1.1 Array.prototype.toReversed()

toReversed()reverse() 方法的非破坏性版本:

const arr = ['a', 'b', 'c'];
const result = arr.toReversed();
console.log(result); // ['c', 'b', 'a']
console.log(arr); // ['a', 'b', 'c']

下面是 toReversed() 方法的一个简单的 polyfill:

if (!Array.prototype.toReversed) {
Array.prototype.toReversed = function () {
return this.slice().reverse();
};
}

1.2 Array.prototype.toSorted()

toSorted()sort() 方法的非破坏性版本:

const arr = ['c', 'a', 'b'];
const result = arr.toSorted();
console.log(result); // ['a', 'b', 'c']
console.log(arr); // ['c', 'a', 'b']

下面是 toSorted() 方法的一个简单的 polyfill:

if (!Array.prototype.toSorted) {
Array.prototype.toSorted = function (compareFn) {
return this.slice().sort(compareFn);
};
}

1.3 Array.prototype.toSpliced()

splice() 方法比其他几种方法都复杂, 其使用形式: splice(start, deleteCount, ...items)

该方法会从从 start 索引处开始删除 deleteCount 个元素, 然后在 start 索引处开始插入 item 中的元素,最后返回已经删除的元素

toSplicedsplice() 方法的非破坏性版本,它会返回更新后的数组,原数组不会变化,并且我们无法再得到已经删除的元素:

const arr = ['a', 'b', 'c', 'd'];
const result = arr.toSpliced(1, 2, 'X');
console.log(result); // ['a', 'X', 'd']
console.log(arr); // ['a', 'b', 'c', 'd']

下面是 toSpliced() 方法的一个简单的 polyfill:

if (!Array.prototype.toSpliced) {
Array.prototype.toSpliced = function (start, deleteCount, ...items) {
const copy = this.slice();
copy.splice(start, deleteCount, ...items);
return copy;
};
}

1.4 Array.prototype.with()

with() 方法的使用形式: with(index, value),它是 arr[index] = value 的非破坏性版本

const arr = ['a', 'b', 'c'];
const result = arr.with(1, 'X');
console.log(result); // ['a', 'X', 'c']
console.log(arr); // ['a', 'b', 'c']

下面是 with() 方法的一个简单的 polyfill:

if (!Array.prototype.with) {
Array.prototype.with = function (index, value) {
const copy = this.slice();
copy[index] = value;
return copy;
};
}

2. 数组分组

2.1 概述

在日常开发中, 数组分组是一种极其常见的操作。因此, proposal-array-grouping 提案就提出了两个新的数组方法:

  • array.group(callback, thisArg?)
  • array.groupToMap(callback, thisArg?)

下面是这两个方法的类型签名:

Array<Elem>.prototype.group<GroupKey extends (string|symbol)>(
callback: (value: Elem, index: number, array: Array<Elem>) => GroupKey,
thisArg?: any
): {[k: GroupKey]: Array<Elem>}

Array<Elem>.prototype.groupToMap<GroupKey>(
callback: (value: Elem, index: number, array: Array<Elem>) => GroupKey,
thisArg?: any
): Map<GroupKey, Array<Elem>>

这两个方法都用来对数组进行分组:

  • 输入:一个数组
  • 输出: 组, 每个组都有一个组key, 以及一个包含组成员的数组

这两个方法都会对数组进行遍历,它们会向其回调请求组键并将元素添加到相应的组中。这两个方法在表示组的方式上有所不同:

  • group():将组存储在对象中:组键存储为属性键,组成员存储为属性值
  • groupToMap():将组存储在 Map 中:组键存储为 Map 键,组成员存储为 Map 值

那这两个方法该如何选择呢?我们知道, JavaScript 中对象是支持解构的,如果想要使用解构来获取数组中的值,比如,对于上面对象,可以通过解构获取三个不同组的值:

const {vegetables, fruit, meat} = result;

而 Map 的好处就是它的 key 不限于字符串和 symbol, 更加自由


2.2 使用

下面来看几个实用例子。假如执行 Promise.allSettled() 方法返回的数组如下:

const settled = [
{ status: 'rejected', reason: 'Jhon' },
{ status: 'fulfilled', value: 'Jane' },
{ status: 'fulfilled', value: 'John' },
{ status: 'rejected', reason: 'Jaen' },
{ status: 'rejected', reason: 'Jnoh' },
];

const {fulfilled, rejected} = settled.group(x => x.status);

// fulfilled 结果如下:
[
{ status: 'fulfilled', value: 'Jane' },
{ status: 'fulfilled', value: 'John' },
]

// rejected 结果如下:
[
{ status: 'rejected', reason: 'Jhon' },
{ status: 'rejected', reason: 'Jaen' },
{ status: 'rejected', reason: 'Jnoh' },
]

在这个例子中,使用 group() 的效果会更好,因为可以使用解构轻松获取需要组的值

假如想要对以下数组中人根据国家进行分组:

const persons = [
{ name: 'Louise', country: 'France' },
{ name: 'Felix', country: 'Germany' },
{ name: 'Ava', country: 'USA' },
{ name: 'Léo', country: 'France' },
{ name: 'Oliver', country: 'USA' },
{ name: 'Leni', country: 'Germany' },
];

const result = persons.groupToMap((person) => person.country);

// result 的执行结果和以下 Map 是等价的:
new Map([
[
'France',
[
{ name: 'Louise', country: 'France' },
{ name: 'Léo', country: 'France' },
]
],
[
'Germany',
[
{ name: 'Felix', country: 'Germany' },
{ name: 'Leni', country: 'Germany' },
]
],
[
'USA',
[
{ name: 'Ava', country: 'USA' },
{ name: 'Oliver', country: 'USA' },
]
],
])

在这个例子中, groupToMap() 是更好的选择, 因为哦嗯嗯可以在Map 中使用任何类型的键, 而在对象中, 键值只能是字符串或 symbol


3. Array 支持从尾到头搜索

新增两个方法: .findLast().findLastIndex() 从数组的最后一个元素开始查找,可以同 find()findIndex() 做一个对比

const arr = [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }];

// find vs findLast
console.log(arr.find(n => n.value % 2 === 1)); // { value: 1 }
console.log(arr.findLast(n => n.value % 2 === 1)); // { value: 3 }

// findIndex vs findLastIndex
console.log(arr.findIndex(n => n.value % 2 === 1)); // 0
console.log(arr.findLastIndex(n => n.value % 2 === 1)); // 2

4. Array.fromAsync

在 JavaScript 中内置了 Array.from 方法, 它用于将 类数组或者 可迭代对象生成一个新的数组实例

在 ECMAScript 2018中引入了异步可迭代对象

而JavaScript中一直缺少直接从异步可迭代对象生成数组的内置方法

proposal-array-from-async 提案中提出来的 Array.fromAsync 方法就是为了解决这个问题而提出来的

下面来看一个简单的例子:

async function * asyncGen (n) {
for (let i = 0; i < n; i++)
yield i * 2;
}

// arr 将变为 [0, 2, 4, 6]`
const arr = [];
for await (const v of asyncGen(4)) {
arr.push(v);
}

// 与上述方式是等价的
const arr = await Array.fromAsync(asyncGen(4));

Array.fromAsync 可以将异步迭代转换为 Promise, 并将解析为新数组

Promise 解析之前,它将从输入值中创建一个异步迭代器,进行惰性的迭代,并将每个产生的值添加到新数组中

与其他基于 Promise 的 API 一样, Array.fromAsync 总是会立即返回一个 Promise

Array.fromAsync 的输入在创建其异步或同步迭代器时引发错误时,则此 Promise 状态将被置为 rejected


5. WeakMap 支持 SymbolA 作为 Keys

这一提案支持了在 WeakMap 中使用 Symbol 类型作为键,而此前 WeakMap 中只允许对象类型作为键

这一特性实际上是为了允许在 RecordsTuples 数据类型中引用对象


Records 与 Tuples 提案为 JavaScript 引入了两个新的数据类型,它们的特性是基于值比较来判断相等性,如对于两个 Tuple 的比较中, #[1, 2,3] === #[1, 2, 3] 是成立的,因为内部的成员值完全一致

然而,这一基于值比较的特性导致了无法在 Record 与 Tuple 中使用基于引用地址比较的对象类型

而如果我们能够在 WeakMap 中使用 Symbol 类型作为键,就可以在 Record 与 Tuple 中使用 Symbol 存放引用,间接地实现对象类型值的存储


对于 Map 与 WeakMap 的差异,我们知道 Map 类型是通过两个数组来分别存储键和键值的,这两个数组对于其中对象类型键/键值的引用始终存在, 从而导致即使已经不存在其它的引用也无法回收处理

因此, WeakMap 持有的引用为弱引用,在对象类型不存在其它引用时,能正确地执行能垃圾回收


正是因为弱引用的要求, WeakMap 的键是无法枚举的,且需要是唯一的值

对象类型很好地满足了这个要求,两个完全一样的对象类型实际上也拥有着不同的引用

你肯定会想到 Symbol 也具有这种“唯一”的特性,这也是为何此提案想要允许 Symbol 作为 WeakMap 的键

同时, Symbol 也能够起到比对象类型更好的标识作用:

const weakMap = new WeakMap();

const key = Symbol('ref for data');
const data = { };

weakMap.set(key, data);

6. Hashbang 语法

如下所示,在 index.js 脚本文件里编写 JS 代码,如果要正确的执行,需要在控制台输入 node index.js

console.log("JavaScript");

如果直接执行 ./index.js 脚本文件会得到以下报错:

$ ./index.js
./index.js: line 1: syntax error near unexpected token `"JavaScript"'
./index.js: line 1: `console.log("JavaScript");'

很正常,因为我们并没有指定使用何种解释器来执行上述脚本文件

Hashbang 语法是用来指定脚本文件的解释器是什么,语法规则是在脚本文件头部增加一行代码: #!/usr/bin/env node

// #!/usr/bin/env node
console.log("JavaScript");

注意,还需修改脚本文件的权限 chmod +x index.js, 否则执行会报 permission denied: ./index.js 错误

参考链接