龙空技术网

ES2023 引入数组拷贝新方法

不秃头程序员 300

前言:

现时咱们对“js定义数组并动态赋值”大致比较关注,朋友们都想要了解一些“js定义数组并动态赋值”的相关文章。那么小编在网摘上汇集了一些关于“js定义数组并动态赋值””的相关内容,希望我们能喜欢,大家一起来学习一下吧!

ECMAScript 2023 规范已尘埃落定。它涵盖了 Array 对象的若干新方法,这些方法有助于使我们的 JS 程序更加可预测和可维护。toSorted/toReversed/toSpliced/with 允许我们在不就地更新的情况下操作数组,而是通过拷贝副本,并修改该副本。

变更与副作用

Array 对象始终存在某些奇葩之处。sort/reverse/splice 等方法会就地更新数组。而 concat/map/filter 等其他方法则会创建数组的副本,然后操作该副本。当我们对对象执行一个就地更新的操作时,这被称为“副作用”(side effects),并且可能会导致系统其他地方出现意外行为。

举个栗子,这是当我们反转数组时的实际情况。

const framework = ['Vue', 'React', 'Angular']const reversed = framework.reverse()console.log(reversed)// => ['Angular', 'React', 'Vue']console.log(framework)// => ['Angular', 'React', 'Vue']console.log(Object.is(framework, reversed))// => true

如你所见,原数组也被反转了,尽管我们将反转数组的结果赋值给了一个新变量,但这两个变量只是指向了同一个数组。

数组变更和 React

数组变更方法最臭名昭著的问题之一是,当我们在 React 组件中使用它们时。我们无法变更数组,然后尝试将其设置为新状态,因为数组本身是同一个对象,这不会触发新的渲染。相反,我们需要先拷贝数组,然后变更副本,并将其设置为新状态。因此,React 提供了整整一页文档,解释了如何更新数组状态。

先拷贝,再变更

解决此问题的方案是,先拷贝数组,然后变更它。存在若干不同的数组拷贝方案,包括但不限于:

Array.from()... 展开运算符slice() 方法无参调用

const framework = ['Vue', 'React', 'Angular']const reversed = Array.from(languages).reverse()console.log(reversed)// => ['Angular', 'React', 'Vue']console.log(framework)// => ['Vue', 'React', 'Angular']console.log(Object.is(framework, reversed))// => false

存在解决方案固然很好,但粉丝请记住,首先执行某种不同的拷贝方法并不优雅。

通过拷贝修改的新方法

此乃新方法的用武之地。toSorted/toReversed/toSpliced/with 中的每一个都会为我们拷贝原数组,更改副本,并将其返回。它使得执行这些操作之一都更易编写,因为我们只需要调用一个函数,且更易读。

Array.prototype.toSorted()

toSorted 方法返回一个全新的已排序数组。

const framework = ['Vue', 'React', 'Angular']const sorted = framework.toSorted()console.log(sorted)// => ['Angular', 'React', 'Vue']console.log(framework)// => ['Vue', 'React', 'Angular']

sort 方法存在某些意外行为(排序问题),除了拷贝之外,toSorted 也有同样的问题。如果我们要排序特殊字符的数字或字符串,我们仍要小心。确保我们提供的比较器回调函数(比如 StringlocaleCompare)会产生预期的排序结果。

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 framework = ['Vue', 'React', 'Angular']const reversed = framework.toReversed()console.log(reversed)// => ['Angular', 'React', 'Vue']

Array.prototype.toSpliced()

toSpliced 方法与其原版 splice 略有不同。splice 通过在指定的索引处删除和添加元素,就地更改现有数组,并返回包含数组中已删除元素的数组。toSpliced 返回一个新数组,不包含已删除的元素,但包含任何添加的元素。其工作原理如下:

const framework = ['Vue', 'React', 'Angular']const spliced = framework.toSpliced(2, 1, 'Nuxt', 'Next')console.log(spliced)// => ['Vue', 'React', 'Nuxt', 'Next']

如果我们使用 splice 作为其返回值,那么 toSpliced 将不会被替换。如果我们想在不改变原数组的情况下知道被删除的元素,那么我们应该使用 slice() 拷贝方法。

令人头大的是,splice 采用与 slice 不同的参数。splice 采用一个索引以及该索引之后要删除的元素数量,slice 采用两个索引:开始和结束。如果我们想使用 toSpliced 代替 splice,但又想获取被删除的元素,我们可以将 toSplicedslice 应用于原数组,如下所示:

const languages = ['JavaScript', 'TypeScript', 'CoffeeScript']const startDeletingAt = 2const deleteCount = 1const 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。这就是从 Int8ArrayBigUint64Array 的所有内容。TypedArray 没有 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 和 Deno 1.31 都支持这四种方法,并且有适用于尚不支持的平台的 polyfill(功能补丁)和 shim。

标签: #js定义数组并动态赋值