6月27日 ECMA 大会批准了 ECMAScript 2023 (es14)规范,意味着新的一些语法将正式成为标准。
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(): 返回第一个查找到的元素,如果没有找到,返回undefinedArray.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() ,用法跟 find 和 findIndex 完全一致,唯一的区别就是它们会从数组的最后一个成员开始,依次向前检查。这两个方法都很方便,可跳过创建临时的重复、突变和混乱的索引减法。
Array.prototype.findLast(): 返回第一个查找到的元素,如果没有找到,返回undefinedArray.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) | fnFindLast | findLast | fnFindLastIndex | findLastIndex |
|---|---|---|---|---|
| 10000 | 15.899999856948853 | 3.3000001907348633 | 9.899999856948853 | 3.5 |
| 100000 | 8.700000047683716 | 7.099999904632568 | 7.300000190734863 | 3.3999998569488525 |
| 10000000 | 429.40000009536743 | 347.2000000476837 | 454.19999980926514 | 378.7000000476837 |
| 100000000 | 3535.2999999523163 | 3430.7999999523163 | 4267.099999904633 | 3500.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作为key(Symbols 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 添加到允许的键列表中。
WeakMap 与 Map的区别,以及为什么要用WeakMap
- WeakMap只接受对象(null除外)和 Symbol 值作为键名,Map 对象的键可以是任何类型
- Map所构建的实例需要手动清理,才能被垃圾回收清除,可能会导致内存泄漏,因为数组会一直引用着每个键和值;而WeakMap只要外部的引用消失,所对应的键值对就会自动被垃圾回收清除。
- WeakMap 的键是弱引用的,因此对象不可枚举。如果想要对象的 key 值列表,应该使用Map。如果要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。
- 使用
Symbol作为WeakMap的key可以更清晰地表明它的键和映射项的角色关系,而不需要创建一个只用作键的新对象。由于 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) | reverse | toReversed | sort | toSorted | splice | toSpliced | edit | with |
|---|---|---|---|---|---|---|---|---|
| 10000 | 3.299999952316284 | 1.0999999046325684 | 8.599999904632568 | 6.8999998569488525 | 5.6000001430511475 | 3.5999999046325684 | 1.9000000953674316 | 13.700000047683716 |
| 100000 | 10 | 8.099999904632568 | 43.40000009536743 | 20.5 | 26 | 12.900000095367432 | 3.3999998569488525 | 3.9000000953674316 |
| 10000000 | 306.7999999523163 | 212.09999990463257 | 1961.6000001430511 | 1639.2000000476837 | 1782.3999998569489 | 1331.2999999523163 | 199.30000019073486 | 324 |
| 100000000 | 2833.300000190735 | 1987.4000000953674 | 16033.799999952316 | 14935.700000047684 | 16168.299999952316 | 13545.299999952316 | 2131.5 | 3273.0999999046326 |
当次数在1万时,破坏性和非破坏性方法的差异并不显著,基本都在2-3ms以内;
从1000万次开始,破坏性和非破坏性方法耗时差距拉大。从这里可以感觉到,等待的时间明显变长;
到1亿次时,reverse和toReversed差距≈1s,sort和toSorted差距≈1.2s,splice和toSpliced差距≈2.6s。
对比结束~
总结是:在频繁操作的情况,除了with(),非破坏性方法比破坏性方法性能表现更好,可以有效提高性能。
目前浏览器已经全部支持,可以在调试工具中尝试新的特性。