写点什么

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

评论

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

Java开发面经分享,springboot项目案例百度云,实战篇

Java 程序员 后端

模块一作业

心怀架构

Java开发自学技巧!极客学院百度云资源,2021最新Java笔试题目

Java 程序员 后端

Java开发自学教程!尚学堂java,我被面试官绝地反杀了

Java 程序员 后端

Java开发面试题!牛客网java开发高频面试题,让我成功在寒冬中站稳脚步

Java 程序员 后端

Java开发教程,极客时间架构师训练营,面试流程4轮技术面+1轮HR

Java 程序员 后端

Java开发经验的有效总结,以商品超卖为例讲解Redis分布式锁

Java 程序员 后端

Java开发者应该会哪些东西才不会被公司淘汰,美团Java面试

Java 程序员 后端

第1周作业

波波

「架构实战营」

Java开发前景怎么样,java全套教程百度云,linux基础入门教程

Java 程序员 后端

Java开发经验谈,linux视频教程百度网盘,逆袭面经分享

Java 程序员 后端

Java开发视频教程,linux使用教程,BIO和NIO有啥区别

Java 程序员 后端

Java开发还不会这些,极客学院和黑马,进阶学习工作最全指南

Java 程序员 后端

Java开发还会吃香吗,Java微服务架构从入门到精通

Java 程序员 后端

Java开发岗还不会这些问题,想拿高工资

Java 程序员 后端

Java开发究竟该如何学习,年末阿里百度等大厂技术面试题汇总,程序员翻身之路

Java 程序员 后端

Java开发者跳槽指,牛客网算法初级班,春招我借这份PDF的复习思路

Java 程序员 后端

Java开发面试基础,java架构师全套百度网盘,Java基础面试重点

Java 程序员 后端

Java性能优化最佳实践,mybatis入门视频

Java 程序员 后端

Java开发者跳槽面试,尚硅谷java课程,netty框架面试题

Java 程序员 后端

Java开发面试基础,牛客网客户端,【面试总结】

Java 程序员 后端

Java技术基础知识总结,菜鸟教程mysql,Java重要知识点

Java 程序员 后端

模块一作业

忘记喝水的猫

架构训练营

Java开发最佳实践手册全网独一份,vue视频教程百度网盘,正式加入字节跳动

Java 程序员 后端

Java开发核心知识笔记共2100页,如何保证Redis与数据库的双写一致性

Java 程序员 后端

Java开发经典实战!自学java教程百度云盘,阿里程序员的Java之路

Java 程序员 后端

云栖收官:想跟远道而来的朋友们说

阿里巴巴云原生

云原生 云栖大会 收官 致谢

Java开发实战讲解,牛客网面试经验,Java编程入门教材

Java 程序员 后端

Java开发实战讲解,牛客网面试经验,Java高级知识图谱

Java 程序员 后端

Java开发工程师笔试题目,图灵学院vip百度云盘,阿里P8大牛手把手教你

Java 程序员 后端

Java微服务架构图,nginx视频教程百度云,学习指南

Java 程序员 后端

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