大咖直播-鸿蒙原生开发与智能提效实战!>>> 了解详情
写点什么

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

评论

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

Android 组件化实战,kotlin协程实现原理

android 程序员 移动开发

Android 使用 Kotlin 重写 Gradle 文件,kotlin教程

android 程序员 移动开发

Android 反编译利器,jadx 的高级技巧,看完这篇彻底明白了

android 程序员 移动开发

Android 开发行业真的不行了嘛?,深入讲解Android

android 程序员 移动开发

Android 无缝换肤深入了解与使用,android快速开发

android 程序员 移动开发

Android WebView独立进程解决方案(1),flutter推送通知

android 程序员 移动开发

Android 关于CPU类型的so文件兼容问题(ABI),十年Android编程开发生涯

android 程序员 移动开发

Android 插桩入门,腾讯T2大牛手把手教你

android 程序员 移动开发

Android 开发必备知识点整理,34岁程序员年薪50w

android 程序员 移动开发

android 快速开发(三)巧用公共标题栏,音视频编解码开发

android 程序员 移动开发

Android 性能监控系列一(原理篇),闭关60天学懂NDK+Flutter

android 程序员 移动开发

Android 技术的下半场(1),android开发书籍下载

android 程序员 移动开发

Android WebView独立进程解决方案,手撕面试官

android 程序员 移动开发

Android 开发市场是盛是衰?你应该知晓,android实战项目

android 程序员 移动开发

Android 初中级开发社招面试总结!,android自定义控件开发入门与实战

android 程序员 移动开发

Android 可能你想要的APK瘦身笔记,android2018面试题

android 程序员 移动开发

Android 组件通信中有哪些不为人知的细节?,面试题分享

android 程序员 移动开发

Android 可能你想要的APK瘦身笔记(1),2021金三银四面试季

android 程序员 移动开发

Android 开发一比一年难做!面试题都这么难了,android插件化

android 程序员 移动开发

android 快速开发(三)巧用公共标题栏(1),android开发从入门到精通

android 程序员 移动开发

Android 技术的下半场,kotlin开源

android 程序员 移动开发

Android 基础掌握好,面试基本不会倒!,面向Android开发者的复习指南

android 程序员 移动开发

Android 开发必备知识点及面试题汇总(Android+Java,斩获offer

android 程序员 移动开发

Android 开发经验分享:挺重要的网络基础,android实现选择题模式

android 程序员 移动开发

Android 架构组件的最新进展 (上篇),flutter登录界面设计

android 程序员 移动开发

Android 约束布局(ConstraintLayout)1,最详细的解释小白也能听懂

android 程序员 移动开发

Android 网络框架之Retrofit源码解析,android嵌入式开发及实训答案

android 程序员 移动开发

Android 基础与底层机制面试题,万字解析

android 程序员 移动开发

Android 已发行多年,移动 App 已经趋近饱和,那么 Android 开发还会有那么吃香吗

android 程序员 移动开发

Android 开发之深入浅出 NavigationUI,2020-2021阿里巴巴安卓面试真题解析

android 程序员 移动开发

Android 开发面试心得:BAT大厂Android面试题整理,面试8家大厂后终于拿到Offer

android 程序员 移动开发

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