JS数据类型:转换数组map、sort、reverse
转换数组
让我们继续学习进行数组转换和重新排序的方法。
map
arr.map 方法是最有用和经常使用的方法之一。
它对数组的每个元素都调用函数,并返回结果数组。
语法:
let result = arr.map(function(item, index, array) { // 返回新值而不是当前元素 })
例如,在这里我们将每个元素转换为它的字符串长度:
let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length); alert(lengths); // 5,7,6
sort(fn)
arr.sort 方法对数组进行 原位(in-place) 排序,更改元素的顺序。(译注:原位是指在此数组内,而非生成一个新数组。)
它还返回排序后的数组,但是返回值通常会被忽略,因为修改了 arr
本身。
语法:
let arr = [ 1, 2, 15 ]; // 该方法重新排列 arr 的内容 arr.sort(); alert( arr ); // 1, 15, 2
你有没有注意到结果有什么奇怪的地方?
顺序变成了 1, 15, 2
。不对,但为什么呢?
这些元素默认情况下被按字符串进行排序。
从字面上看,所有元素都被转换为字符串,然后进行比较。对于字符串,按照词典顺序进行排序,实际上应该是 "2" > "15"
。
要使用我们自己的排序顺序,我们需要提供一个函数作为 arr.sort()
的参数。
该函数应该比较两个任意值并返回:
function compare(a, b) { if (a > b) return 1; // 如果第一个值比第二个值大 if (a == b) return 0; // 如果两个值相等 if (a < b) return -1; // 如果第一个值比第二个值小 }
例如,按数字进行排序:
function compareNumeric(a, b) { if (a > b) return 1; if (a == b) return 0; if (a < b) return -1; } let arr = [ 1, 2, 15 ]; arr.sort(compareNumeric); alert(arr); // 1, 2, 15
现在结果符合预期了。
我们思考一下这儿发生了什么。arr
可以是由任何内容组成的数组,对吗?它可能包含数字、字符串、对象或其他任何内容。我们有一组 一些元素。要对其进行排序,我们需要一个 排序函数 来确认如何比较这些元素。默认是按字符串进行排序的。
arr.sort(fn)
方法实现了通用的排序算法。我们不需要关心它的内部工作原理(大多数情况下都是经过 快速排序 或 Timsort 算法优化的)。它将遍历数组,使用提供的函数比较其元素并对其重新排序,我们所需要的就是提供执行比较的函数 fn
。
顺便说一句,如果我们想知道要比较哪些元素 —— 那么什么都不会阻止 alert 它们:
[1, -2, 15, 2, 0, 8].sort(function(a, b) { alert( a + " <> " + b ); return a - b; });
该算法可以在此过程中,将一个元素与多个其他元素进行比较,但是它会尝试进行尽可能少的比较。
比较函数可以返回任何数字
实际上,比较函数只需要返回一个正数表示“大于”,一个负数表示“小于”。
通过这个原理我们可以编写更短的函数:
let arr = [ 1, 2, 15 ]; arr.sort(function(a, b) { return a - b; }); alert(arr); // 1, 2, 15
箭头函数最好
你还记得 箭头函数 吗?这里使用箭头函数会更加简洁:
arr.sort( (a, b) => a - b );
这与上面更长的版本完全相同。
使用 localeCompare
for strings
你记得 字符串比较 算法吗?默认情况下,它通过字母的代码比较字母。
对于许多字母,最好使用 str.localeCompare
方法正确地对字母进行排序,例如 Ö
。
例如,让我们用德语对几个国家/地区进行排序:
let countries = ['Österreich', 'Andorra', 'Vietnam']; alert( countries.sort( (a, b) => a > b ? 1 : -1) ); // Andorra, Vietnam, Österreich(错的) alert( countries.sort( (a, b) => a.localeCompare(b) ) ); // Andorra,Österreich,Vietnam(对的!)
reverse
arr.reverse 方法用于颠倒 arr
中元素的顺序。
例如:
let arr = [1, 2, 3, 4, 5]; arr.reverse(); alert( arr ); // 5,4,3,2,1
它也会返回颠倒后的数组 arr
。
split 和 join
举一个现实生活场景的例子。我们正在编写一个消息应用程序,并且该人员输入以逗号分隔的接收者列表:John, Pete, Mary
。但对我们来说,名字数组比单个字符串舒适得多。怎么做才能获得这样的数组呢?
str.split(delim) 方法可以做到。它通过给定的分隔符 delim
将字符串分割成一个数组。
在下面的例子中,我们用“逗号后跟着一个空格”作为分隔符:
let names = 'Bilbo, Gandalf, Nazgul'; let arr = names.split(', '); for (let name of arr) { alert( `A message to ${name}.` ); // A message to Bilbo(和其他名字) }
split
方法有一个可选的第二个数字参数 —— 对数组长度的限制。如果提供了,那么额外的元素会被忽略。但实际上它很少使用:
let arr = 'Bilbo, Gandalf, Nazgul, Saruman'.split(', ', 2); alert(arr); // Bilbo, Gandalf
拆分为字母
调用带有空参数 s
的 split(s)
,会将字符串拆分为字母数组:
let str = "test"; alert( str.split('') ); // t,e,s,t
arr.join(glue) 与 split
相反。它会在它们之间创建一串由 glue
粘合的 arr
项。
例如:
let arr = ['Bilbo', 'Gandalf', 'Nazgul']; let str = arr.join(';'); // 使用分号 ; 将数组粘合成字符串 alert( str ); // Bilbo;Gandalf;Nazgul
reduce/reduceRight
当我们需要遍历一个数组时 —— 我们可以使用 forEach
,for
或 for..of
。
当我们需要遍历并返回每个元素的数据时 —— 我们可以使用 map
。
arr.reduce 方法和 arr.reduceRight 方法和上面的种类差不多,但稍微复杂一点。它们用于根据数组计算单个值。
语法是:
let value = arr.reduce(function(accumulator, item, index, array) { // ... }, [initial]);
该函数一个接一个地应用于所有数组元素,并将其结果“搬运(carry on)”到下一个调用。
参数:
accumulator
—— 是上一个函数调用的结果,第一次等于initial
(如果提供了initial
的话)。item
—— 当前的数组元素。index
—— 当前索引。arr
—— 数组本身。
应用函数时,上一个函数调用的结果将作为第一个参数传递给下一个函数。
因此,第一个参数本质上是累加器,用于存储所有先前执行的组合结果。最后,它成为 reduce
的结果。
听起来复杂吗?
掌握这个知识点的最简单的方法就是通过示例。
在这里,我们通过一行代码得到一个数组的总和:
let arr = [1, 2, 3, 4, 5]; let result = arr.reduce((sum, current) => sum + current, 0); alert(result); // 15
传递给 reduce
的函数仅使用了 2 个参数,通常这就足够了。
让我们看看细节,到底发生了什么。
在第一次运行时,
sum
的值为初始值initial
(reduce
的最后一个参数),等于 0,current
是第一个数组元素,等于1
。所以函数运行的结果是1
。在第二次运行时,
sum = 1
,我们将第二个数组元素(2
)与其相加并返回。在第三次运行中,
sum = 3
,我们继续把下一个元素与其相加,以此类推……
计算流程:
或者以表格的形式表示,每一行代表的是对下一个数组元素的函数调用:
sum | current | result | |
---|---|---|---|
第 1 次调用 | 0 | 1 | 1 |
第 2 次调用 | 1 | 2 | 3 |
第 3 次调用 | 3 | 3 | 6 |
第 4 次调用 | 6 | 4 | 10 |
第 5 次调用 | 10 | 5 | 15 |
在这里,我们可以清楚地看到上一个调用的结果如何成为下一个调用的第一个参数。
我们也可以省略初始值:
let arr = [1, 2, 3, 4, 5]; // 删除 reduce 的初始值(没有 0) let result = arr.reduce((sum, current) => sum + current); alert( result ); // 15
结果是一样的。这是因为如果没有初始值,那么 reduce
会将数组的第一个元素作为初始值,并从第二个元素开始迭代。
计算表与上面相同,只是去掉第一行。
但是这种使用需要非常小心。如果数组为空,那么在没有初始值的情况下调用 reduce
会导致错误。
例如:
let arr = []; // Error: Reduce of empty array with no initial value // 如果初始值存在,则 reduce 将为空 arr 返回它(即这个初始值)。 arr.reduce((sum, current) => sum + current);
所以建议始终指定初始值。
arr.reduceRight 和 arr.reduce 方法的功能一样,只是遍历为从右到左。
Array.isArray
数组是基于对象的,不构成单独的语言类型。
所以 typeof
不能帮助从数组中区分出普通对象:
alert(typeof {}); // object alert(typeof []); // same
……但是数组经常被使用,因此有一种特殊的方法用于判断:Array.isArray(value)。如果 value
是一个数组,则返回 true
;否则返回 false
。
alert(Array.isArray({})); // false alert(Array.isArray([])); // true
大多数方法都支持 “thisArg”
几乎所有调用函数的数组方法 —— 比如 find
,filter
,map
,除了 sort
是一个特例,都接受一个可选的附加参数 thisArg
。
上面的部分中没有解释该参数,因为该参数很少使用。但是为了完整性,我们需要讲讲它。
以下是这些方法的完整语法:
arr.find(func, thisArg); arr.filter(func, thisArg); arr.map(func, thisArg); // ... // thisArg 是可选的最后一个参数
thisArg
参数的值在 func
中变为 this
。
例如,在这里我们使用 army
对象方法作为过滤器,thisArg
用于传递上下文(passes the context):
let army = { minAge: 18, maxAge: 27, canJoin(user) { return user.age >= this.minAge && user.age < this.maxAge; } }; let users = [ {age: 16}, {age: 20}, {age: 23}, {age: 30} ]; // 找到 army.canJoin 返回 true 的 user let soldiers = users.filter(army.canJoin, army); alert(soldiers.length); // 2 alert(soldiers[0].age); // 20 alert(soldiers[1].age); // 23
如果在上面的示例中我们使用了 users.filter(army.canJoin)
,那么 army.canJoin
将被作为独立函数调用,并且这时 this=undefined
,从而会导致即时错误。
可以用 users.filter(user => army.canJoin(user))
替换对 users.filter(army.canJoin, army)
的调用。前者的使用频率更高,因为对于大多数人来说,它更容易理解。