专业的编程技术博客社区

网站首页 > 博客文章 正文

ES2023 引入数组拷贝新方法(es6解决数组深拷贝)

baijin 2024-10-27 08:08:21 博客文章 4 ℃ 0 评论

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 = 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。这就是从 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)
// => true

const 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。

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表