在我们的《JavaScript中的变量赋值和突变指南》中,我们介绍了变量赋值和突变的概念,研究了突变的问题和如何管理它们。在这篇文章中,我们将看到JavaScript是如何通过将突变原始数组的数组方法作为语言的一部分来使我们的生活变得更加困难。但是,这并不全是厄运和悲哀。在文章的最后,我们会写出一些函数来解决这些问题--而且你今天就可以开始在你的代码中使用这些函数了。
如果你想更详细地探讨这个话题,或者想了解现代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() 方法将0 的值添加到countdown 数组的末尾,它也会对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()
稍微令人困惑的是,数组也有一些方法并不改变原始数组,而是返回一个新的数组。
这些方法将根据它们所进行的操作返回一个新的数组。例如,map() 方法可以用来将一个数组中的所有数字加倍。
const numbers = [1,2,3];
const evens = numbers.map(number => number * 2);
<< [2,4,6]
现在,如果我们检查numbers 数组,我们可以看到它并没有受到调用该方法的影响。
numbers
<< [1,2,3]
似乎没有任何理由说明为什么有些方法会使数组变异,而有些则不会(尽管最近的趋势是使它们不变异),所以很难记住哪些方法会做哪些事。
Ruby有一个很好的解决方案,那就是使用bang符号的方式来解决这个问题。任何对调用它的对象造成永久性改变的方法都以bang结束,所以[1,2,3].reverse! 将会反转数组,而[1,2,3].reverse 将会返回一个元素反转的新数组。
继续阅读《不可改变的数组方法:如何编写非常干净的JavaScript代码》,请访问SitePoint。
