51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

使用JavaScript的不可变数组方法编写更干净的代码

今天我们来了解下JavaScript中原始数组变异的数组方法,并且我们将编写一些可解决这些问题的函数去深入研究,目的是编写更干净的代码。

JavaScript中的数组突变

JavaScript中的数组只是对象,这意味着它们可以被突变。实际上,许多内置的数组方法都会使数组本身发生变异。这可能意味着仅通过使用一种内置方法就可以打破上面的黄金法则。

这是一个示例,显示了它可能如何引起一些问题:

const numbers = [1,2,3];
const countdown = numbers.reverse();

这段代码看起来不错。我们有一个叫做的数组numbers,我们想要另一个叫countdown以相反顺序列出数字的数组,而且似乎可行。如果您检查countdown变量的值,这就是我们期望的结果:

countdown
<< [3,2,1]

但是,该操作的不幸副作用是该reverse()方法也使numbers数组发生了变异,而这根本不是我们想要的:

numbers
<< [3,2,1]

更糟糕的是,这两个变量都引用相同的数组,因此我们随后对一个变量所做的任何更改都会影响另一个变量。

例如,如果我们使用该Array.prototype.push()方法0countdown数组的末尾添加一个值,它将对数组执行相同的操作numbers(因为它们都引用同一数组):

countdown.push(0)
<< 4
countdown
<< [3,2,1,0]
numbers
<< [3,2,1,0]

这些副作用可能不会引起注意,尤其是在大型应用程序的深度中,并且会导致某些非常难以跟踪的错误。

并且reverse不是导致这种突变恶作剧的唯一数组方法。这是一个数组方法列表,这些方法会改变它们被调用的数组:

  • Array.prototype.pop()

  • Array.prototype.push()

  • Array.prototype.shift()

  • Array.prototype.unshift()

  • Array.prototype.reverse()

  • Array.prototype.sort()

  • Array.prototype.splice()

有点令人困惑的是,数组还有一些方法不会改变原始数组,而是返回一个新数组:

  • Array.prototype.slice()

  • Array.prototype.concat()

  • Array.prototype.map()

  • Array.prototype.filter()

这些方法将根据它们执行的操作返回一个新数组。例如,该map()方法可用于将数组中的所有数字加倍:

const numbers = [1,2,3];
const evens = numbers.map(number => number * 2);
<< [2,4,6]

现在,如果我们检查numbers数组,则可以看到该方法没有受到调用该方法的影响:

numbers
<< [1,2,3]

似乎没有任何原因可以解释为什么某些方法会改变数组,而另一些则不会(尽管最近添加的趋势是使它们不变),因此很难记住该做什么。

Ruby使用bang表示法对此有一个很好的解决方案。导致对对象进行永久更改的任何方法都以爆炸结束,因此[1,2,3].reverse!将反转数组,而[1,2,3].reverse将返回带有反转元素的新数组。

因此,既然我们已经确定了变异可能是潜在的不良后果,并且许多数组方法导致了变异,那么让我们看一下如何避免使用它们。

事实证明,编写一些功能与变异方法相同的方法并不难,但是可以返回一个新的数组对象而不是变异原始数组。

因为我们不准备补丁 Array.prototype,所以这些函数将始终接受数组本身作为第一个参数。

Pop {#toc_2}

让我们从编写一个新pop函数开始,该函数返回原始数组的副本,但没有最后一项。请注意,Array.prototype.pop()返回从数组末尾弹出的值:

const pop = array => array.slice(0,-1);

此函数用于Array.prototype.slice()返回数组的副本,但删除了最后一项(第二个参数-1表示停止在end之前切片1个位置)。我们可以在下面的示例中看到它的工作方式:

const food = ['苹果','香蕉','胡萝卜','面包'];
pop(food)
<< ['苹果','香蕉','胡萝卜']

push() {#toc_3}

接下来,让我们创建一个push()函数,该函数将返回一个新数组,但在末尾附加一个新元素:

const push = (array, value) => [...array,value];

这使用散布运算符创建数组的副本,然后将作为第二个参数提供的值简单地添加到新数组的末尾,如下面的示例所示:

const food = ['苹果','香蕉','胡萝卜','面包'];
push(food,'茄子')
<< ['苹果','香蕉','胡萝卜','面包','茄子']

Shift and Unshift

我们可以用Array.prototype.shift()Array.prototype.unshift()相似的方式编写替代:

const shift = array => array.slice(1);

对于我们的shift()函数,我们只是从数组中切出第一个元素,而不是最后一个,如下面的示例所示:

const food = ['苹果','香蕉','胡萝卜','面包'];
shift(food)<< ['香蕉','胡萝卜','面包']

我们的unshift()方法将返回一个新数组,并在数组的开头附加一个新值:

const unshift = (array,value) => [value,...array];

Spread运算符允许我们将值以任何顺序放置在数组中,因此我们只需将新值放置在原始数组的副本之前。我们可以在下面的示例中看到它的工作方式:

const food = ['苹果','香蕉','胡萝卜','面包'];
unshift(food,'茄子')
<< ['茄子','苹果','香蕉','胡萝卜','面包']

reverse()

现在让我们开始编写替代Array.prototype.reverse()方法,该方法将以相反的顺序返回数组的副本,而不是变异原始数组:

const reverse = array => [...array].reverse();

该方法仍然使用该Array.prototype.reverse()方法,但适用于我们使用散布运算符制作的原始数组的副本。创建对象后立即对其进行突变没有错,这就是我们在这里所做的。我们可以在下面的示例中看到它的工作原理:

const food = ['苹果','香蕉','胡萝卜','面包'];
reverse(food)
<< ['苹果','香蕉','胡萝卜','面包']

splice()

最后,让我们处理Array.prototype.splice()。这是一个非常通用的函数,因此我们不会完全重写它的功能(提示:使用spread运算符和splice()。相反,我们将专注于两种主要用途对于切片:从数组中删除项目并将项目插入到数组中。

让我们从一个将返回新数组的函数开始,但是删除给定索引处的项目:

const remove = (array, index) => [...array.slice(0, index),...array.slice(index + 1)];

这用于Array.prototype.slice()将数组切成两半-我们要删除的项目的任一侧。第一个分片返回一个新数组,该数组从原始数组的开头复制所有元素,并到达作为参数提供的索引(但不包括它)。第二个切片返回一个数组,该数组在我们要删除的元素之后一直复制到元素的所有元素,直到原始数组的末尾。然后,我们使用散布运算符将它们放到一个新数组中。

我们可以通过尝试删除food以下数组中索引2处的项目来检查此项是否有效:

const food = ['苹果','香蕉','胡萝卜','面包'];
remove(food,2)
<< ['苹果','香蕉','面包']

最后,让我们编写一个函数,该函数将返回一个新数组,并在特定索引处插入新值:

const insert = (array,index,value) => [...array.slice(0, index), value, ...array.slice(index)];

这与该remove()函数的工作方式类似:它创建了数组的两个切片,但是这次包括了在提供的索引处的元素。当我们将两个切片放回一起时,我们将在两个切片之间插入作为参数提供的值。

我们可以通过尝试将蛋糕表情符号插入food数组的中间位置来检查此作品:

const food = ['苹果','香蕉','胡萝卜','面包']
insert(food,2,'冰淇淋')
<< ['苹果','香蕉','冰淇淋','胡萝卜','面包']

现在,我们有了一组函数,这些函数可以转换数组而不改变原始数组。我已经将它们全部保存在CodePen上的一个位置中,因此可以随时复制它们并在项目中使用它们。您可以通过使它们成为单个对象的方法来命名它们,或者仅在需要时按原样使用它们。

这些对于大多数数组操作应该足够了,但是如果您需要执行其他操作,只需记住一条黄金法则:首先使用spread运算符创建原始数组的副本,然后立即将任何变异方法应用于此副本。

赞(0)
未经允许不得转载:工具盒子 » 使用JavaScript的不可变数组方法编写更干净的代码