JS第五版的七个新高阶的数组实例方法(Array Extras)
线性数组(Arrays)是很多编程语言都内置的最基础的「数据结构」注1,JavaScript也不例外的支持使用数组。数组在原初设计还是比较接近原始类型的,表征数据和对数据操作都比较原始,故在使用数组解决一个较复杂计算问题时显得比较烦琐(看这篇)。经历了一段时间的演化,ES5引入了一些「数组的实例方法」,通过抽象隐去了一些不必要的烦琐的重复的细节。这部分新的实例方法被称为“array extras”,一共九个:
1. Array.isArray (这个不是实例方法)
2. Array.prototype.indexOf
3. Array.prototype.lastIndexOf
4. **Array.prototype.every**
5. **Array.prototype.some**
6. **Array.prototype.forEach**
7. **Array.prototype.map**
8. **Array.prototype.filter**
9. **Array.prototype.reduce**
10. **Array.prototype.reduceRight**
本文简略介绍这些extras方法中的五类七个。
背景及原因
几乎所有使用数组的情景都会迭代处理数组的每一项注2。如下面的代码,使用一个for循环逐个打印出数组元素值到控制台。
var foo = ["a", "b", "c", "d"];
for (var i = 0, len = foo.length; i < len; i++) {
console.log(foo[i]);
}
这段代码没什么问题的,因为数组很小,数组项处理也很简单。但是当问题变得复杂,需要多个循环的时候,你的大脑内存不够用的,需要记住for循环的多个变量。既然都是循环处理每一数组项,那么循环控制细节是不必关注的,可以抽象掉细节来提高代码的可读性(readability);
另外,「数组处理任务」上也出现重复模式可进行抽象(以高阶函数的形式),事实上每一种array extras都在数组处理的基础上针对某一种任务/问题而特定设计的,forEach是对“逐个循环”的包装,map则在forEach的基础上进行「映射转换」——对数组每一项进行特定处理后生成新数组。
以上二是促成array extras的原因。现在,让我们看看这些新数组迭代工具。
高阶函数与自定义回调
在开始讲每一个(或种类)extras方法前,先说它们的通性。
- 第一,它们都是所谓高阶函数(higher-order function )——接受一个「函数对象」作为入参;这个函数对象(以函数表达式的形式定义)是你「自定义的数组项处理函数」——或读取数据,或处理后返回数据等;
- 第二,自定义回调函数会在每一个数组项上执行(在内部实现上就是每次循环调用一次回调函数);
- 第三,大多数的回调函数有三个参数(也有四个的):数组项、数组项的索引(index),和数组本身,一般的迭代任务只需要第一个参数就够了,很少需要访问项索引值,和数组其他数据;
array.XXXX(function(elem, index, array) {
...
}, thisArg)
array.reduce(function(prevVal, elem, index, array) {
...
}, initialValue);
- 第四,大多数高阶extras的第一个参数是必选「自定义回调」,还有第二个可选的参数,如果提供了,这个参数将作为自定义回调的父对象(回调是它的方法),也就是说你传递的第二个参数将作回调函数的this值;
- 第五,自定义回调的返回值很重要,不同种的任务,返回值都不同,如map返回新数级项,filter返回Boolean;
- 最后,高阶extras本身是不会修改数组实例的数据的,当然你的提供的回调可能修改数组的数据。
forEach()
与高级for
2023 review: 现在有了iteraion,大多数情况 可以for of代替了,更为现代。
forEach()
高阶是最通用的,它只是对循环控制的一个封装或抽象,对自定义回调没有预期,你可以对数组项干嘛都可以;它就像一个高级的for循环,循环体就是回调函数。例如:
["a", "b", "c", "d"].forEach(function(element, index, array) {
console.log(element);
});
var data = [1,2,3,4,5]; // An array to sum
// Compute the sum of the array elements
var sum = 0;
data.forEach(function(value) { sum += value; });
sum // => 15
// Now increment each array element
data.forEach(function(v, i, a) { a[i] = v + 1; });
data // => [2,3,4,5,6]
和for循环体不同,回调函数是一个闭包,有独立的符号作用域。自动的forEach唯一不足就是性能稍逊于手动的for,不过只微不足道的一点点,除非你要处理海量数据,否则大多数情况可完全用forEach替代for,代码可读性也是价值之一。
注意:forEach不像for那样可以手动停止循环(通过break),如果需要,你只能通过try语句,请参考《犀牛书》
map()
与转换新数组
只有forEach()
高阶是通用的,不对自定义回调有任何规定,其它的高阶都对回调有规定。由于有规定,高阶函数变得具体,有特定的计算涵义,看名字可知。map( )是对数组(的每一项)进转换映射,得到一个新数组。故自定义回调必须返回一个项作为新数组的项。例如以下的开方和乘方的转换:
var sqrts = [1, 4, 9, 16, 25].map(Math.sqrt);
console.log(sqrts);
// displays "[1, 2, 3, 4, 5]"
a = [1, 2, 3];
b = a.map(function(x) { return x*x; });
// b is [1, 4, 9]
filter()
与去除不要的项
filter()
和 map( )同类,只是filter( ) 任务是对原数组(项)进行过滤,将符合条件的数组项收集起来生成新的数组。所以回调必须返回一个布尔值,决定当前项是否符合条件,加入到新的数组中去。
例如,将字符串数组中去除首字母不是x的项:
["x", "abc", "x1", "xyz"].filter(RegExp.prototype.test, /^x/);
再看两个有趣的实例,去除数据中的“坑”和无用的项目:
var dense = sparse.filter(function() { return true; });
a = a.filter(function(x) { return x !== undefined && x != null; });
every()
/ some()
与数组是怎样的
与前面两个高阶利用原始数组来「制作新数组」不同,every()
and some()
是对原数组的「性质」进行检测,例如数组(项)是否都小于10,或数组项有没有偶数:
a = [1,2,3,4,5];
a.every(function(x) { return x < 10; }) // => true: all values < 10.
a.every(function(x) { return x % 2 === 0; }) // => false: not all values even.
故高阶和回调都返回真值,当所有回调(所有的项都符合条件)都返回true时,every为true; 只需有一个回调为true时,some返回true。
reduce()
/ reduceRight()
与累积处理
2023review: reduce有演化的涵义 ,演化都是连续的,结果受前一次影响,和影响下一次演变。数组项可看成是演化序列。
reduce()
高阶方法和前面的方法最大的不同,是循环之间有联系,前面的方法每次调用回调都是独立的,独立产生一个结果(例如新的数组项),而reduce()
的前一次回调被用于后一次回调,所以回调常用入参有两个(不只一个)——prevVal和elem,并且reduce高阶有一个可选的初始值(这个参数不再是this):
array.reduce(function(prevVal, elem, index, array) {
...
}, initialValue);
reduce意思不太好翻译,它的功能主要是「累积处理」,看看一些常见用例:
1`求和、求积和找最大项
var a = [1,2,3,4,5]
var sum = a.reduce(function(pre,item) { return pre+item }, 0); // Sum of values
var product = a.reduce(function(pre,item) { return pre*item }, 1); // Product of values
var max = a.reduce(function(pre,item) { return (pre>item)?pre:item; }); // Largest value
2`将二维数组扁平化:
var flattened = [[0, 1], [2, 3], [4, 5]].reduce(function(preArray, item) {
return preArray.concat(item);
});
// flattened is [0, 1, 2, 3, 4, 5]
3`将多个类对象(的属性)合并
var objects = [{x:1}, {y:2}, {z:3}];
var merged = objects.reduce(union);
// => {x:1, y:2, z:3}
4 ` 合计 2014 年全球轨道火箭发射数
var rockets = [
{ country:'Russia', launches:32 },
{ country:'US', launches:23 },
{ country:'China', launches:16 },
{ country:'Europe(ESA)', launches:7 },
{ country:'India', launches:4 },
{ country:'Japan', launches:3 }
];
var sum = rockets.reduce(function(prevVal, elem) {
return prevVal + elem.launches;
}, 0);
// ES6
// rockets.reduce((prevVal, elem) => prevVal + elem.launches, 0);
sum // 85
有人将reduce的回调看成累加器,从上面的例子可见,“累加”不只有数值,reduce不只是针对数值处理的,它的原意应该是累积处理——每次处理一项(reduce下降一项),直到处理完成所有项,处理之间有side effect。回调一般会利用上次的返回结果产生一新的结果给下一次回调,最后reduce得到一个最终累积处理的结果——新的数值、对象等。