写点什么

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

评论

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

网络攻防学习笔记 Day98

穿过生命散发芬芳

态势感知 网络攻防 8月日更

PNG文件解读(2):PNG格式文件结构与数据结构解读—解码PNG数据

zhoulujun

png jpg

MongoDB 客户端怎么做负载均衡

海明菌

mongodb 负载均衡 客户端

【架构实战营】毕业总结

swordman

架构实战营

白手起家之搜索利器Elastic search

卢卡多多

ES 8月日更

看完必让你直呼好家伙!阿里巴巴 6 月新作:“Java架构手册”

Java 编程 程序员 IT 计算机

Kafka 和 Kinesis 之间的对比和选择

HoneyMoose

架构训练营毕业总结

冬天的树

一周拿下百度Offer!211本+985硕+计算机专业~

Java 编程 面试 IT 计算机

2021年最新最全:30W字!千道Java 后端面试大全(值得收藏)

Java 编程 程序员 架构 面试

年薪50W阿里P7架构师就会点这?并发丨JVM丨多线程丨Netty丨MySQL!

编程 架构 面试 IT 计算机

13年培训出身!八年后成功坐上了阿里P7架构师的位置

Java 编程 程序员 架构 计算机

JIT-动态编译与AOT-静态编译:java/ java/ JavaScript/Dart乱谈

zhoulujun

Java dart JIT

分享三个可改进的体验

石云升

用户体验 体验设计 8月日更

云原生之可观测性【日志篇】 Logstash组件初探

路上的小崔哥

云原生 Logstash 日志 可观测性

数字新基建助推能源互联网“一体两翼”区块链中台应用建设思考

CECBC

Tensorflow API(一)

毛显新

人工智能 深度学习 tensorflow keras

ipfs挖矿怎么选择公司?ipfs挖矿收益怎么计算?

IPFS挖矿收益怎么计算 ipfs挖矿怎么选择公司

Docker可视化管理工具Portainer

xcbeyond

Docker Portainer 8月日更

流处理基本概念(二)

Databri_AI

大数据 flink 窗口函数

喜获蚂蚁金服、拼多多、字节跳动offer!纠结之后入职拼多多。

Java 编程 程序员 面试 计算机

PNG文件解读(1):PNG/APNG格式的前世今生

zhoulujun

png

【前端 · 面试 】HTTP 总结(七)—— HTTP 缓存概述

编程三昧

面试 HTTP 8月日更 HTTP缓存

深度解析区块链数字票据及其优势

CECBC

模块10作业

wade

#架构实战营

SLO(服务等级目标)与SLA(服务等级协议)

一个大红包

8月日更

我找遍了全网,总结出足足60W字“阿里大厂面试手册”

Java 程序员 IT 计算机 知识分享

三十多岁跳槽无路,晋升无门,濒临绝望之际受贵人指点,成功上岸阿里(Java 岗)

Java 编程 程序员 架构 计算机

JavaScript 开发人员应该理解的 this

devpoint

JavaScript js变量声明 this 8月日更

Tensorflow随笔(一)

毛显新

人工智能 深度学习 tensorflow keras

Java进程cpu100%问题排查

陈皮的JavaLib

Java Linux 面试 8月日更

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