ES2023新特性

694 阅读11分钟

6月27日 ECMA 大会批准了 ECMAScript 2023 (es14)规范,意味着新的一些语法将正式成为标准。

链接:proposals/finished-proposals.md at a827ab7cff94351a4e5bca5ff38424c205d83304 · tc39/proposals · GitHub

TC39组织

在介绍这些新特性之前,我们先来了解下 TC39 这个组织以及提案阶段。

TC39 是ECMA 国际组织第 39 号技术委员会( Technical Committee ),是一个推动 JavaScript 发展的委员会。它是 ECMA 的一部分, ECMA 是 “ ECMAScript ” 规范下的 JavaScript 语言标准化的机构。

TC39 由各个主流浏览器厂商的代表构成,包括国内一些大型的科技公司(阿里和字节)。他们的主要工作就是制定 ECMAScript 标准,以保证 JavaScript 在不同的实现间具有一致的行为,并不断推进语言的演进。

每两个月,TC39 都会召开会议,成员指定的代表和受邀的专家参加。 这些会议的记录在 GitHub 存储库[22] 中公开。

ECMAScript 版本每年发布一次,包括在发布截止日期之前已达到第 4 阶段的所有功能。每个ECMAScript 功能提案都经历从 0 到 4 编号的阶段:

  • Stage 0:"Strawman",想法提出阶段。在这个阶段,任何讨论、想法、改变或者还没加到提案的特性都在这个阶段。
  • Stage 1:"Proposal",提案阶段,产出一个正式的提案。
  • Stage 2:"Draft",草案阶段,与最终标准中包含的特性不会有太大差别。草案之后,原则上只接受增量修改。
  • Stage 3:"Candidate",候选阶段,获得具体实现和用户的反馈。此后,只有在实现和使用过程中出现了重大问题才会修改。
  • Stage 4:"Finished",完成阶段,进入这个阶段代表获得主流浏览器的支持,该特性会出现在下个版本的ECMAScript规范之中。

以下是新特性:

从后往前查找数组(Array find from last

在数组中查找元素是常见的需求, 以下是我们遍历数组中常用的两个方法,但是目前这两种方法都是从前往后遍历的。

  • Array.prototype.find() : 返回第一个查找到的元素,如果没有找到,返回 undefined
  • Array.prototype.findIndex() : 返回第一个查找到的元素的索引,如果没有找到,返回 -1
const arr = [1, 2, 3, 4, 5];

const res = arr.find(i => i > 3);
console.log(res); // 4

const res2 = arr.findIndex(i => i > 3);
console.log(res2); // 3

如果我们想查找满足条件的最后一个元素时,需要先将数组进行reverse后再处理。

现在新增了两个数组方法findLast()findLastIndex() ,用法跟 findfindIndex 完全一致,唯一的区别就是它们会从数组的最后一个成员开始,依次向前检查。这两个方法都很方便,可跳过创建临时的重复、突变和混乱的索引减法。

  • Array.prototype.findLast() : 返回第一个查找到的元素,如果没有找到,返回 undefined
  • Array.prototype.findLastIndex() : 返回第一个查找到的元素的索引,如果没有找到,返回 -1
const arr = [1, 2, 3, 4, 5];

const res = arr.findLast(i => i > 3);
console.log(res);  // 5

const res2 = arr.findLastIndex(i => i > 3);
console.log(res2); // 4

下面是findLast()findLastIndex() 的代码实现

function findLast(arr, callback, thisArg) {
  for (let i = arr.length-1; i >= 0; i--) {
    if (callback.call(thisArg, arr[i], i, arr)) {
      return arr[i];
    }
  }
  return undefined;
}

const arr = [1, 2, 3, 4, 5];
const res = findLast(arr, i => i > 1);
console.log(res); // 5
function findLastIndex(arr, callback, thisArg){
  for(var i = arr.length - 1; i >= 0; i--){
    if (callback.call(thisArg, arr[i], i, arr)) {
      return i;
    }
  }
  return -1;
}

const arr = [1, 2, 3, 4, 5];
const res = findLastIndex(arr, i => i > 1);
console.log(res); // 4

手动实现和新方法的性能比较

const arr = [1, 2, 3, 4, 5];

function fnFindLast(time) {
    const start = performance.now();
    for (let i = 0; i < time; i++) {   
        let copyArr = arr.slice().reverse();
        const res = copyArr.find(i => i > 3);
    }
    const end = performance.now();
    return end - start;
}
function findLast(time) {
    const start = performance.now();
    for (let i = 0; i < time; i++) {
        const res = arr.findLast(i => i > 3);
    }
    const end = performance.now();
    return end - start;
}

function fnFindLastIndex(time) {
    const start = performance.now();
    for (let i = 0; i < time; i++) {
        let copyArr = arr.slice().reverse();
        const targerVal = copyArr.find(i => i > 3);
        const res = arr.findIndex(i => i == targerVal);
    }
    const end = performance.now();
    return end - start;
}

function findLastIndex(time) {
    const start = performance.now();
    for (let i = 0; i < time; i++) {
        const res = arr.findLastIndex(i => i > 3);
    }
    const end = performance.now();
    return end - start;
}

console.table([
    { '次数': 10000, 'fnFindLast': fnFindLast(10000), 'findLast': findLast(10000), 'fnFindLastIndex': fnFindLastIndex(10000), 'findLastIndex': findLastIndex(10000) },
    { '次数': 100000, 'fnFindLast': fnFindLast(100000), 'findLast': findLast(100000), 'fnFindLastIndex': fnFindLastIndex(100000), 'findLastIndex': findLastIndex(100000) },
    { '次数': 10000000, 'fnFindLast': fnFindLast(10000000), 'findLast': findLast(10000000), 'fnFindLastIndex': fnFindLastIndex(10000000), 'findLastIndex': findLastIndex(10000000) },
    { '次数': 100000000, 'fnFindLast': fnFindLast(100000000), 'findLast': findLast(100000000), 'fnFindLastIndex': fnFindLastIndex(100000000), 'findLastIndex': findLastIndex(100000000) },
]);
次数\时间(ms)fnFindLastfindLastfnFindLastIndexfindLastIndex
1000015.8999998569488533.30000019073486339.8999998569488533.5
1000008.7000000476837167.0999999046325687.3000001907348633.3999998569488525
10000000429.40000009536743347.2000000476837454.19999980926514378.7000000476837
1000000003535.29999995231633430.79999995231634267.0999999046333500.5

总结:findLast()findLastIndex(),方便且有效提高性能。

Hashbang 语法(Hashbang Grammar

Hashbang,也称为 shebang,就是在文件开头的一行,以 #! 开头的注释,用来指定脚本的解释器。

#!usr/bin/env node // 这个注释的意思是,使用 `Node.js` 作为解释器来运行这个脚本。
console.log('hashbang');

在终端用路径执行脚本即可:./demo.js

console.log('no hashbang')

没有 Hashbang 在终端执行时,需要使用 node 指令才能执行:node ./demo2.js

只有当脚本直接在 shell 中运行时,Hashbang 语法才有语意意义,其他环境下 JavaScript 解释器会把它视为普通注释。

WeakMap支持使用Symbol作为keySymbols as WeakMap keys

WeakMap

WeakMap结构与Map结构类似,也是用于生成键值对的集合。

const wm = new WeakMap();
let key = { name: 'aaa' };
wm.set(key, 2);
wm.get(key) // 2

在 JavaScript 中,Objects 和 Symbols 被保证是唯一并且不能被重新创建的,这使得它们都是 WeakMapkeys 的理想候选者。以前的版本或规范只允许以这种方式使用 Objects ,但新的提案将 Symbols 添加到允许的键列表中。

WeakMapMap的区别,以及为什么要用WeakMap

  • WeakMap只接受对象(null除外)和 Symbol 值作为键名,Map 对象的键可以是任何类型
  • Map所构建的实例需要手动清理,才能被垃圾回收清除,可能会导致内存泄漏,因为数组会一直引用着每个键和值;而WeakMap只要外部的引用消失,所对应的键值对就会自动被垃圾回收清除。
  • WeakMap 的键是弱引用的,因此对象不可枚举。如果想要对象的 key 值列表,应该使用Map。如果要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。
  • 使用 Symbol 作为 WeakMapkey 可以更清晰地表明它的键和映射项的角色关系,而不需要创建一个只用作键的新对象。由于 Symbol 类型的属性名是唯一的,可以避免属性名冲突问题。
  • Map 能够记住键的原始插入顺序,且一个键只能出现一次
const map = new WeakMap();
map.set(1, 2) // 报错
map.set(null, 2) // 报错
map.set(Symbol(), 2) // 不报错
map.set({ name: 'aaa' }, 2) // 不报错

下面看例子:

// 通过arr数组,对e1和e2两个对象添加文字说明,但是就形成了arr对e1和e2的引用
const e1 = document.getElementById('head');
const e2 = document.getElementById('body');
const arr = [
  [e1, 'head 元素'],
  [e2, 'body 元素'],
];

// 不需要e1和e2这两个对象时,我们必须手动删除引用,否则垃圾回收机制就不会释放他们占用的内存。
arr [0] = null;
arr [1] = null;
const a = new Map();
let key = new Array(5 * 1024 * 1024);
a.set(key, 'hi');
// 手动删除建
a.delete(key);

如果我们忘记了删除,就有可能会造成内存泄露。

WeakMap 可以解决这个问题,它的键名所引用的对象是弱引用,只要外部的引用消失,WeakMap 内部的引用,就会自动被垃圾回收清除,不用手动删除引用。

// 在命令行中输入以下命令,--expose-gc参数表示允许手动执行垃圾回收机制
node --expose-gc

// 先手动gc一次
global.gc();
undefined

// 查看内存占用
process.memoryUsage();
{
  rss: 33144832,
  heapTotal: 6868992,
  heapUsed: 5462816,
  external: 993560,
  arrayBuffers: 108784
}

let wm = new WeakMap();
let key = new Array(5 * 1024 * 1024);
wm.set(key, 'hi');

// 再查看内存,已经增加了
process.memoryUsage()
{
  rss: 75702272,
  heapTotal: 48558080,
  heapUsed: 47668184,
  external: 992227,
  arrayBuffers: 174357
}

// 清除变量 key 对数组的引用,但不清除 WeakMap 的键名对数组的引用
key = null

// 再次gc
global.gc();

// 再获取内存时可以看到,内存已经被回收了
process.memoryUsage();
{
  rss: 33230848,
  heapTotal: 6868992,
  heapUsed: 5081816,
  external: 992277,
  arrayBuffers: 207085
}

不改变原数组的新的原型方法(Change Array by Copy

很多数组的方法会改变原数组,比如push()pop()reversed()sort()等等,我们称它们为破坏性方法,只要调用了数组这些方法,它的值就变了。

非破坏性方法,比如我们经常用到的 filter、some、map、find 等方法,都是不会改变原数组的。

const arr = [3, 2, 1];
const res = arr.sort(); 

console.log(res); // [1, 2, 3]
console.log(arr); // 原数组也变成了 [1, 2, 3]

如果想要不改变原数组,就意味着我们需要复制一个新数组,再使用这些方法。

const res1 = arr.slice().sort();
const res2 = [...arr].sort();
const res3 = Array.from(arr).sort();

现在新增了4个数组的方法,允许对数组进行操作时,不改变原数组,返回一个原数组的拷贝。

  • toReversed():是 reverse() 的非破坏性版本:
const arr = [1, 2, 3];
const res = arr.toReversed();
console.log(res); // [3, 2, 1]
console.log(arr); // [1, 2, 3] 原数组不变
Array.prototype.toReversed = function () {
    return this.slice().reverse();
};
  • toSorted():是 sort() 的非破坏性版本:
const arr = [3, 2, 1];
const res = arr.toSorted();
console.log(res); // [1, 2, 3]
console.log(arr); // [3, 2, 1] 原数组不变
Array.prototype.toSorted = function () {
    return this.slice().sort();
};
  • toSpliced(start, deleteCount, ...items):是splice()的非破坏性版本:
const arr = [1, 2, 3];
const res = arr.toSpliced(1, 1, 'z');
console.log(res); // [1, 'z', 3]
console.log(arr); // [1, 2, 3] 原数组不变
Array.prototype.toSpliced = function (start, deleteCount, ...items) {
    const copy = this.slice();
    copy.splice(start, deleteCount, ...items);
    return copy;
};
  • with():是修改数组指定索引值的非破坏性版本
const arr = [1, 2, 3];
const res = arr.with(1, 'z');
console.log(res);  // [1, 'z', 3]
console.log(arr); // [1, 2, 3] 原数组不变
Array.prototype.with = function (index, value) {
    const copy = this.slice();
    copy[index] = value;
    return copy;
};

破坏性和非破坏性方法是否有性能差异?

const arr = [1, 2, 3];
function reverse(time) {
    const start = performance.now();
    for (let i = 0; i < time; i++) {
        const res = arr.slice().reverse();
    }
    const end = performance.now();
    return end - start;
}
function toReversed(time) {
    const start = performance.now();
    for (let i = 0; i < time; i++) {
        const res = arr.toReversed();
    }
    const end = performance.now();
    return end - start;
}

const arr2 = [3, 2, 1];
function sort(time) {
    const start = performance.now();
    for (let i = 0; i < time; i++) {
        const res = arr2.slice().sort();
    }
    const end = performance.now();
    return end - start;
}
function toSorted(time) {
    const start = performance.now();
    for (let i = 0; i < time; i++) {
        const res = arr2.toSorted();
    }
    const end = performance.now();
    return end - start;
}

const arr3 = [3, 2, 1];
function splice(time) {
    const start = performance.now();
    for (let i = 0; i < time; i++) {
        let copyArr = arr3.slice();
        copyArr.splice(1, 0, 'z');
    }
    const end = performance.now();
    return end - start;
}
function toSpliced(time) {
    const start = performance.now();
    for (let i = 0; i < time; i++) {
        const res = arr3.toSpliced(1, 0, 'z');
    }
    const end = performance.now();
    return end - start;
}

const arr4 = [3, 2, 1];
function edit(time) {
    const start = performance.now();
    for (let i = 0; i < time; i++) {
        let copyArr = arr4.slice();
        copyArr[1] = 4;
    }
    const end = performance.now();
    return end - start;
}
function fnWith(time) {
    const start = performance.now();
    for (let i = 0; i < time; i++) {
        const res = arr4.with(1, 4);
    }
    const end = performance.now();
    return end - start;
}

console.table([
    { '次数': 10000, 'reverse': reverse(10000) , 'toReversed': toReversed(10000), 'sort': sort(10000) , 'toSorted': toSorted(10000), 'splice': splice(10000) , 'toSpliced': toSpliced(10000), 'edit': edit(10000) , 'with': fnWith(10000) },
    { '次数': 100000, 'reverse': reverse(100000) , 'toReversed': toReversed(100000), 'sort': sort(100000) , 'toSorted': toSorted(100000), 'splice': splice(100000) , 'toSpliced': toSpliced(100000), 'edit': edit(100000) , 'with': fnWith(100000) },
    { '次数': 10000000, 'reverse': reverse(10000000) , 'toReversed': toReversed(10000000), 'sort': sort(10000000) , 'toSorted': toSorted(10000000), 'splice': splice(10000000) , 'toSpliced': toSpliced(10000000), 'edit': edit(10000000) , 'with': fnWith(10000000) },
    { '次数': 100000000, 'reverse': reverse(100000000) , 'toReversed': toReversed(100000000), 'sort': sort(100000000) , 'toSorted': toSorted(100000000), 'splice': splice(100000000) , 'toSpliced': toSpliced(100000000), 'edit': edit(100000000) , 'with': fnWith(100000000) },
]);
次数\时间(ms)reversetoReversedsorttoSortedsplicetoSplicededitwith
100003.2999999523162841.09999990463256848.5999999046325686.89999985694885255.60000014305114753.59999990463256841.900000095367431613.700000047683716
100000108.09999990463256843.4000000953674320.52612.9000000953674323.39999985694885253.9000000953674316
10000000306.7999999523163212.099999904632571961.60000014305111639.20000004768371782.39999985694891331.2999999523163199.30000019073486324
1000000002833.3000001907351987.400000095367416033.79999995231614935.70000004768416168.29999995231613545.2999999523162131.53273.0999999046326

当次数在1万时,破坏性和非破坏性方法的差异并不显著,基本都在2-3ms以内;

从1000万次开始,破坏性和非破坏性方法耗时差距拉大。从这里可以感觉到,等待的时间明显变长;

到1亿次时,reverse和toReversed差距≈1s,sort和toSorted差距≈1.2s,splice和toSpliced差距≈2.6s。

对比结束~

总结是:在频繁操作的情况,除了with(),非破坏性方法比破坏性方法性能表现更好,可以有效提高性能。

目前浏览器已经全部支持,可以在调试工具中尝试新的特性。