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得到一个最终累积处理的结果——新的数值、对象等。

参考


  1. 数据结构和抽象数据类型(例如类或对象)的涵义是有区别的,数据结构是为辅助解决特定问题而设计的语言构件,常见数据结构有列表或数组,字典或哈希表,树,图等。数据结构具有针对某种问题的解题亲近性,例如使唯一键值的字典用来解决数据查询问题。
  2. 这或许也是设计数组的目的,线性数组作为一种数据结构被设计用来解决何种问题,这个问题倒是很有趣,并且这类问题非常的常见。
裸男
Nakeman.cn 2023 Build by Gatsby and Tailwind, Deploy on Netlify.