NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

ECMAScript 2023:为 JavaScript 带来新的数组复制方法

  • 2023-05-26
    北京
  • 本文字数:4590 字

    阅读完需:约 15 分钟

ECMAScript 2023:为JavaScript带来新的数组复制方法

ECMAScript 2023 规范最近已经定稿,其中提出的 Array 对象新方法将为 JavaScript 带来更好的可预测性和可维护性。toSorted、toReversed、toSpliced 和 with 方法允许用户在不更改数据的情况下对数据执行操作,实质是先制造副本再更改该副本。

 

变异与副作用

 

Array 对象总是有点自我分裂。sort、reverse 和 splice 等方法会就地更改数组,concat、map 和 filter 等其他方法则是先创建数组副本,再对副本执行操作。当我们通过操作让对象产生变异时,则会产生一种副作用,导致系统其他位置发生意外行为。

 

举例来说,当 reverse 一个数组时会发生如下情况。

 

const languages = ["JavaScript", "TypeScript", "CoffeeScript"];const reversed = languages.reverse();console.log(reversed);// => [ 'CoffeeScript', 'TypeScript', 'JavaScript' ]console.log(languages);// => [ 'CoffeeScript', 'TypeScript', 'JavaScript' ]console.log(Object.is(languages, reversed));// => true
复制代码

 

可以看到,原始数组已经反转,但即使我们将反转数组的结果分配给一个新变量,两个变量也仍指向同一数组。

 

变异数组和 React

 

数组变异方法中一个最著名的问题,就是在 React 组件中使用时的异常。我们无法变异数组,之后尝试将其设置为新状态,因为数组本身是同一个对象且不会触发新的渲染。相反,我们需要先复制该数组,然后改变副本再将其设置为新状态。因此,React 文档专门有一整页解释了如何更新状态数组。

 

先复制,后变异

 

解决这个问题的方法,是先复制数组,之后再执行变异。我们可以通过几种不同方法来生成数组副本,包括:Array.from,展开运算符,或者调用不带参数的 slice 函数。

 

const languages = ["JavaScript", "TypeScript", "CoffeeScript"];const reversed = Array.from(languages).reverse();// => [ 'CoffeeScript', 'TypeScript', 'JavaScript' ]console.log(languages);// => [ 'JavaScript', 'TypeScript', 'CoffeeScript' ]console.log(Object.is(languages, reversed));// => false
复制代码

 

有办法能解决当然很好,总之请千万注意不同复制操作间是有区别的。

 

新方法可随副本变化

 

此次公布的新方法正是为此而生。toSorted、toReversed、toSpliced 和 with 都能复制原始数组、变更副本再返回结果。如此一来,每项操作都更易于编写,开发者只需调用一个函数即可,代码阅读起来也更容易、不必预先考虑到底要用具体哪种数组复制方法。下面,我们来看这几种新方法的区别。

 

Array.prototype.toSorted


其中 toSorted 函数会返回一个新的、经过排序的数组。

 

const languages = ["JavaScript", "TypeScript", "CoffeeScript"];const sorted = languages.toSorted();console.log(sorted);// => [ 'CoffeeScript', 'JavaScript', 'TypeScript' ]console.log(languages);// => [ 'JavaScript', 'TypeScript', 'CoffeeScript' ]
复制代码

 

除了复制之外,sort 函数还会引发一些意想不到的行为,toSorted 也继承了这种特点。所以在对带有重音字符的数字或字符串进行排序时,大家仍然要小心。比如准备一个 comparator 比较器函数(例如 String's localeCompare)来生成当前查找的结果。

 

const numbers = [5, 3, 10, 7, 1];const sorted = numbers.toSorted();console.log(sorted);// => [ 1, 10, 3, 5, 7 ]const sortedCorrectly = numbers.toSorted((a, b) => a - b);console.log(sortedCorrectly);// => [ 1, 3, 5, 7, 10 ]
复制代码

 

const strings = ["abc", "äbc", "def"];const sorted = strings.toSorted();console.log(sorted);// => [ 'abc', 'def', 'äbc' ]const sortedCorrectly = strings.toSorted((a, b) => a.localeCompare(b));console.log(sortedCorrectly);// => [ 'abc', 'äbc', 'def' ]
复制代码

 

Array.prototype.toReversed

 

使用 toReversed 函数,会返回一个按相反顺序排序的新数组。

 

const languages = ["JavaScript", "TypeScript", "CoffeeScript"];const reversed = languages.toReversed();console.log(reversed);// => [ 'CoffeeScript', 'TypeScript', 'JavaScript' ]
复制代码

 

之前将 reverse 的结果分配给新变量时会出问题,因为原始数组也发生了变异。但现在,大家可以使用 toReversed 或者 toSorted 来复制数组并更改副本。

 

Array.prototype.toSpliced

 

toSpliced 函数与原始版本的 splice 略有不同。splice 是在提供的索引处删除和添加元素来更改现有数组,再返回一个包含数组中所删除元素的数组。toSpliced 则直接返回一个新数组,其中不含被删除的元素,且包含所添加的元素。其工作方式如下:

 

const languages = ["JavaScript", "TypeScript", "CoffeeScript"];const spliced = languages.toSpliced(2, 1, "Dart", "WebAssembly");console.log(spliced);// => [ 'JavaScript', 'TypeScript', 'Dart', 'WebAssembly' ]
复制代码

 

如果我们使用 splice 作为返回值,那么 toSpliced 就不能直接作为替代使用。换言之,如果大家想在不改变原始数组的情况下知晓被删除的元素是什么,就应使用 slice 复制方法。

 

更麻烦的是,splice 和 slice 使用的参数也有不同。splice 使用的是一个索引加该索引之后待删除的元素数量;slice 则使用两个索引,分别对应开始和结束。如果要使用 toSpliced 代替 splice,但又想获取被删除的元素,则可对原始数组应用 toSpliced 和 slice,如下所示:

 

const languages = ["JavaScript", "TypeScript", "CoffeeScript"];const startDeletingAt = 2;const deleteCount = 1;const spliced = languages.toSpliced(startDeletingAt, deleteCount, "Dart", "WebAssembly");const removed = languages.slice(startDeletingAt, startDeletingAt + deleteCount);console.log(spliced);// => [ 'JavaScript', 'TypeScript', 'Dart', 'WebAssembly' ]console.log(removed);// => [ 'CoffeeScript' ]
复制代码

 

Array.prototype.with

 

with 函数所代表的复制方法,等同于使用方括号表示方来更改数组内的一个元素。因此,与其通过以下方式直接更改数组:

 

const languages = ["JavaScript", "TypeScript", "CoffeeScript"];languages[2] = "WebAssembly";console.log(languages);// => [ 'JavaScript', 'TypeScript', 'WebAssembly' ]
复制代码

 

可以复制该数组再执行更改:

 

const languages = ["JavaScript", "TypeScript", "CoffeeScript"];const updated = languages.with(2, "WebAssembly");console.log(updated);// => [ 'JavaScript', 'TypeScript', 'WebAssembly' ]console.log(languages);// => [ 'JavaScript', 'TypeScript', CoffeeScript' ]
复制代码

 

不只是数组


此次发布的新方法不仅适用于常规的数组对象。您可以在任意 TypedArray 上使用 toSorted、toReversed 和 with 方法,包括 Int8Array 到 BigUint64Array 等各种类型。但因为 TypedArrays 没有 splice 方法,因此无法使用 toSpliced 方法。

 

注意事项

 

前文提到,map、filter 和 concat 等方法也都采取先复制再更改的思路,但这些方法与新的复制方法间仍有不同。如果对内置的 Array 对象进行扩展,并在实例上使用 map、flatMap、filter 或 concat,则会返回相同类型的新实例。但如果您扩展一个 Array 并使用 toSorted、toReversed、toSpliced 或者 with,则返回的仍是普通 Array。

 

class MyArray extends Array {}const languages = new MyArray("JavaScript", "TypeScript", "CoffeeScript");const upcase = languages.map(language => language.toUpperCase());console.log(upcase instanceof MyArray);// => trueconst reversed = languages.toReversed();console.log(reversed instanceof MyArray);// => false
复制代码

 

可以使用 MyArray.from 将其转回您的自定义 Array:

 

class MyArray extends Array {}const languages = new MyArray("JavaScript", "TypeScript", "CoffeeScript");const reversed = MyArray.from(languages.toReversed());console.log(reversed instance of MyArray);// => true
复制代码

 

支持


虽然 ECMAScript 2023 的规范刚刚成形,但已经为本文提到的新数组方法提供了良好支持。Chrome 110、Safari 16.3、Node.js 20 和 Deno1.31 都支持这四种新方法,尚不支持的平台也有 polyfills 和 shims 作为过渡方案。

 

JavaScript 仍在不断改进


很高兴看到 ECMAScript 标准新增了这么多有意义的内容,让我们能轻松编写出可预测性更好的代码。其他一些提案也已被纳入 ES2023,感兴趣的朋友可以移步此处:

https://github.com/tc39/proposals/blob/HEAD/finished-proposals.md

 

至于未来的规范发展方向,推荐大家参考整个 TC39 提案库:

https://github.com/tc39/proposals

 

附录:ES2023 新特性概述

 

数组倒序查找


Array.prototype.findLast 和 Array.prototype.findLastIndex

 

let nums = [5,4,3,2,1];let lastEven = nums.findLast((num) => num % 2 === 0); // 2let lastEvenIndex = nums.findLastIndex((num) => num % 2 === 0); // 3
复制代码

 

Hashbang 语法


#! for JS

 

此脚本的第一行以 #!开头,表示可在注释中包含任意文本。

 

#!/usr/bin/env node// in the Script Goal'use strict';console.log(1);
复制代码

 

将符号作为 WeakMap 键


在弱集合和注册表中使用符号

 

注意:注册的符号不可作为 weakmap 键。

 

let sym = Symbol("foo");let obj = {name: "bar"};let wm = new WeakMap();wm.set(sym, obj);console.log(wm.get(sym)); // {name: "bar"}
复制代码

 

sym = Symbol("foo");let ws = new WeakSet();ws.add(sym);console.log(ws.has(sym)); // true
复制代码

 

sym = Symbol("foo");let wr = new WeakRef(sym);console.log(wr.deref()); // Symbol(foo)
复制代码

 

sym = Symbol("foo");let cb = (value) => {  console.log("Finalized:", value);};let fr = new FinalizationRegistry(cb);obj = {name: "bar"};fr.register(obj, "bar", sym);fr.unregister(sym);
复制代码

通过副本更改数组


返回更改后的 Array 和 TypeArray 副本。

 

注意:类型数组不可 tospliced。

const greek = ['gamma', 'aplha', 'beta']greek.toSorted(); // [ 'aplha', 'beta', 'gamma' ]greek; // [ 'gamma', 'aplha', 'beta' ]const nums = [0, -1, 3, 2, 4]nums.toSorted((n1, n2) => n1 - n2); // [-1,0,2,3,4]nums; // [0, -1, 3, 2, 4]
复制代码

 

const greek = ['gamma', 'aplha', 'beta']greek.toReversed(); // [ 'beta', 'aplha', 'gamma' ]greek; // [ 'gamma', 'aplha', 'beta' ]
复制代码

 

const greek = ['gamma', 'aplha', 'beta']greek..toSpliced(1,2); // [ 'gamma' ]greek; // [ 'gamma', 'aplha', 'beta' ]greek.toSpliced(1,2, ...['delta']); // [ 'gamma', 'delta' ]greek; // [ 'gamma', 'aplha', 'beta' ]
复制代码

 

const greek = ['gamma', 'aplha', 'beta']greek..toSpliced(1,2); // [ 'gamma' ]greek; // [ 'gamma', 'aplha', 'beta' ]greek.toSpliced(1,2, ...['delta']); // [ 'gamma', 'delta' ]greek; // [ 'gamma', 'aplha', 'beta' ]
复制代码

 

const greek = ['gamma', 'aplha', 'beta'];greek.with(2, 'bravo'); // [ 'gamma', 'aplha', 'bravo' ]greek; //  ['gamma', 'aplha', 'beta'];
复制代码

 

参考链接:

https://h3manth.com/ES2023/


相关阅读:

全网最全 ECMAScript 攻略

“TypeScript 不值得!... 反向迁移到 JavaScript 引争议

JavaScript 作用域深度剖析:动态作用域

TypeScript 与 JavaScript:你应该知道的区别

2023-05-26 15:208984

评论

发布
暂无评论
发现更多内容

选择无服务器:Babbel 的迁移故事

亚马逊云科技 (Amazon Web Services)

2023-05-04:用go语言重写ffmpeg的scaling_video.c示例,用于实现视频缩放(Scaling)功能。

福大大架构师每日一题

golang 音视频 流媒体 福大大架构师每日一题

Python文件和操作系统基础

timerring

Python

巴别时代基于 Apache Paimon 的 Streaming Lakehouse 的探索与实践

Apache Flink

大数据 flink 实时计算

大咖观点| AIGC与因果推断的双向赋能

九章云极DataCanvas

【Python实战】Python采集图片数据

BROKEN

三周年连更

八股MQ003——聊聊Consumer

Codyida

后端

论存储在智算与超算平台建设中的重要性

焱融科技

#分布式文件存储 #全闪存储 #高性能存储

cleanmymac x4.13.2中文版mac清理软件

茶色酒

CleanMyMac X

重塑数据活力 | 焱融科技与DaoCloud 道客完成云原生兼容性认证

焱融科技

#云原生 #高性能 #分布式文件存储 #文件存储 #分布式存储

华秋干货铺 | PCB板为什么要做树脂塞孔?

华秋电子

如何参与Cetus和Oi! Network联合ISO认购和空投奖励?

股市老人

MES系统中的BOM为何如此重要?先进的BOM体系怎么建立?

优秀

MES系统 BOM

涨薪60%,从小厂逆袭,坐上美团技术专家(面经+心得)

程序知音

Java 后端 java面试 java架构 Java进阶

一文带你了解EPM系统的发展史

智达方通

EPM 业财融合 智达方通 企业绩效管理 海波龙

AIGC遇上低代码的碰撞与融合

力软低代码开发平台

AIGC产业研究报告 2023——图像生成篇

易观分析

产业 智能

Python函数基础回顾

timerring

Python

五月到了,再来看看ChatGPT给我们带来了什么吧!

加入高科技仿生人

AI AIGC ChatGPT

标签系列:标签的价值、生产与评价

Taylor

标签 CDP 用户画像 标签体系 精准营销

我的职业规划 |社区征文

来碗老郭

三周年征文

FL Studio 2023中文版水果音乐制作软件

茶色酒

FL Studio 21

推动变革,打造全新的全面预算管理解决方案

智达方通

智能多维数据库 多维数据库 业财融合 全面预算管理

iOS MachineLearning 系列(10)—— 自然语言分析之文本拆解

珲少

从入门到放弃再到成功—我的 Jira 插件探索之路

跟YY哥学Jira

Scrum cli Jira插件 Forge 团队速度

致敬青年——开源的生力军

开放原子开源基金会

开放原子开源基金会

文心一言 VS chatgpt (14)-- 算法导论3.2 2~3题

福大大架构师每日一题

ChatGPT 文心一言 福大大架构师每日一题

膜拜,国内算法大佬亲撰:数据结构与算法全解笔记

程序知音

Java 算法 数据结构与算法 后端技术

使用篇丨链路追踪(Tracing)很简单:链路拓扑

阿里巴巴云原生

阿里云 云原生 链路追踪 Tracing

MySQL 8.0中InnoDB buffer pool size进度更透明

GreatSQL

MySQL InnoDB greatsql社区

得物直播低延迟探索 | 得物技术

得物技术

直播技术 直播推流 直播优化

ECMAScript 2023:为JavaScript带来新的数组复制方法_大前端_丁晓昀_InfoQ精选文章