写点什么

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:2013567

评论

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

IntelliJ IDEA中文版安装教程 附IntelliJ IDEA永久激活码2024最新

Rose

代码编辑 IntelliJ IDEA中文版 IntelliJ IDEA2024安装 intellij idea激活码2024

企业数据怎么定义?包含哪些?如何保护企业数据?

行云管家

数据安全 企业数据安全 企业数据

泉州等保测评机构电话是多少?在哪里?

行云管家

等级保护 等保测评 泉州

Serial for Mac v2.0.17激活版 全功能串行终端管理软件

Rose

币价与数据持续低迷,比特币和以太坊能否从低谷中恢复?

区块链软件开发推广运营

dapp开发 区块链开发 链游开发 NFT开发 公链开发

如何将MySQL迁移到TiDB,完成无缝业务切换?

NineData

MySQL 迁移 TiDB 迁移复制 一键迁移

如何做好API安全

德迅云安全杨德俊

小小的引用计数,大大的性能考究

bin的技术小屋

Netty Java' netty

coconutBattery Plus:苹果mac电脑 电池健康检测工具

Rose

fxfactory视觉特效下载 FxFactory 8 Pro mac破解资源

Rose

FxFactory Pro 8 fxfactory视觉特效

Parallels Desktop 19完美破解版 附PD虚拟机永久密钥

Rose

Parallels Desktop 19 Parallels虚拟机下载 Mac虚拟机安装 PD19密钥

锐起安全会议室方案:提升涉密会议效率与安全级别!

上海锐起科技

信息安全 文件管理 涉密会议

MacDroid Pro:Mac电脑和Android设备之间的文件传输和数据管理

Rose

数据传输 MacDroid pro 安卓手机数据传输助手

中文汉化版Bartender 5 mac下载 菜单栏图标管理软件

Rose

Bartender 5中文版 Bartender 5破解版 Mac菜单栏管理工具

大数据处理与智慧营销系统性能优化

鲸品堂

大数据 营销 流程化 企业号2024年8月PK榜

倒计时3天!数智时代下大数据应用的“道”与“术”闭门会议即将开幕

望繁信科技

数字化转型 流程挖掘 流程智能 智能化应用

一站式系统清理维护工具MacBooster 8 Pro Mac中文版

Rose

苹果电脑系统优化 MacBooster 8 Pro 系统清理维护 MacBooster 破解版

Word 2021 LTSC for Mac永久破解版 含word激活工具 支持M1/M2

Rose

Word 2021 许可证 Word 2021破解版 Word 2021 mac

极简接入|七牛云 QPlayer2 播放器再升级

七牛云

音视频开发 播放器

TON链上游戏项目开发基本要求及模式创建与海外宣发策略

区块链软件开发推广运营

dapp开发 区块链开发 链游开发 NFT开发 公链开发

mac防火墙软件Radio Silence 完整激活版 支持M1/M2

Rose

香城档案利用 NocoBase 快速实现智能档案管理

NocoBase

低代码 无代码 档案管理

6 个Spring tx 事务注解:4种隔离&7种事务传播业务案例(必须收藏)

肖哥弹架构

Java spring 注解

基于“日志审计应用”的 DNS 日志洞察实践

阿里巴巴云原生

阿里云 云原生 sls

扬帆蓝海,智起未来!和鲸科技助力第十三届全国海洋航行器设计与制作大赛智能感知赛道精彩收官!

ModelWhale

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