pop / push, shift / unshift 方法
队列是最常见的使用数组的方法之一。
- push 在末尾添加一个元素
- shift 取出队列首端的一个元素,整个队列往前移,这样原先排第二的元素现在排在了第一。
队列的应用在实践中经常会碰到,例如需要在屏幕上显示消息队列。
数组还有一个用例,就是数据结构——栈。
- push 在末端添加一个元素
- pop 从末端取出一个元素
栈通常被被形容成一叠卡片:要么在最上面添加卡片,要么从最上面拿走卡片:
队列先进先出,栈先进后出。JavaScript 中的数组既可以用作队列,也可以用作栈,它们允许你从首端/末端来添加/删除元素。
这在计算机科学中,允许这样的操作的数据结构被称为 双端队列。
- pop 取出并返回数组最后一个元素
- push 在数组末端添加元素
- shift 取出数组第一个元素并返回
- unshift 在数组首端添加元素
pop 和 unshift 方法都可以一次添加多个元素:
arr.push('a', 'b')
arr.unshift('c', 'd')
数组是一种特殊的对象,使用方括号来访问属性实际上还是来源于对象的语法。
但是数组真正特殊的是它们内部实现,JavaScript 引擎尝试把这些元素一个接一个地存储在连续的内存区域,还有一些其他的优化,以使数组运行得非常快。
但是如果我们像使用常规对象一样使用数组,那么针对数组的优化就不再适用了,然后对应的优化就会被关闭,这些优化所带来的优势也就荡然无存了。
数组误用的几种方式:
- 添加一个非数字的属性,比如 arr.test=5
- 制造空洞,比如:添加 arr[0],然后添加 arr[1000](它们中间什么都没有)
- 以倒叙填充数组,比如 arr[1000], arr[999]等等
性能
push/pop 方法运行的比较快,而 shift/unshift 比较慢
只获取并移除数字 0 对应的元素是不够的。其它元素也需要被重新编号。
shift 操作必须做三件事:
- 移除索引为 0 的元素。
- 把所有的元素向左移动,把索引 1 改成 0,2 改成 1 以此类推,对其重新编号。
- 更新 length 属性。
循环
- for...of 不能获取当前元素的索引,只是获取元素值,但是大多数情况是够用的。而且这样写更短。
let fruits = ['Apple', 'Orange', 'Plum'];
for(let fruit of fruits) {
alert( fruit )
}
- 技术上来讲,因为数组也是对象,所以使用 for..in 也是可以的:
let arr = ['a', 'b', 'c']
for(let key in arr) {
alert( arr[key] )
}
但这其实是一个很不好的想法。会有一些潜在问题存在:
-
for...in 循环会遍历所有属性,不仅仅是这些数字属性。
类数组对象,看似是数组,有 length 和索引属性,但是也可能有其他的非数字的属性和方法,这通常是我们不需要的。for...in 循环会把他们都列出来。所以如果我们需要处理类数组对象,这些额外的属性就会出现问题。
-
for...in 循环适用于普通对象,并且做了对应优化。但是不适用于数组,因此速度会慢 10~100 倍。当然即使是这样也非常快,只是在遇到瓶颈时可能会有问题,但我们仍然应该了解这其中的不同。
如何从数组中删除元素
数组是对象,所以我们可以尝试使用 delete,元素被删除,但是长度不变。这很正常,因为 delete obj.key 是通过 key 来移除对应的值。对于对象来说是可以的。但是对于数组来说,我们通常希望剩下的元素可以移动并占据被释放的位置。我们希望得到一个更短的数组。所以应该使用特殊的方法。
splice
语法:
arr.splice(index[, deleteCOunt,elem1,...elemN])
从 index 开始:删除 deleteCount 个元素并在当前位置插入 elem1, ..., elemN。最后返回已删除元素的数组。
let arr = ['I', 'study', 'javascript'];
arr.splice(1, 1); // 从索引 1 开始删除 1 个元素
alert( arr ); // ["I", "JavaScript"]
删除 3 个元素,并用另外两个替换
let arr = ["I", "study", "JavaScript", "right", "now"];
arr.splice(0, 3, "Let's", "dance");
console.log( arr ); // ["Let's", "dance", "right", "now"]
将 deleteCount 设置为 0,splice 方法就能够插入元素而不用删除任何元素:
负向索引
let arr = [1, 2, 5];
// 从索引 -1 (尾端前一位) 删除 0 个元素,然后插入 3 和 4
arr.splice(-1, 0, 3, 4)
arr // [1, 2, 3, 4, 5]
slice
arr.slice 方法比 arr.splice 简单得多。
arr.slice([start], [end])
它会返回一个新数组,将所有从索引 start 到 end(不包括end)的数组项复制到一个新的数组。start 和 end都可以是负数,在这种情况下,从末尾计算索引。
它和字符串的 str.slice 方法有点像,就是把子字符串替换成子数组
let arr = ['t', 'e', 's', 't'];
alert( arr.slice(1, 3) ); // e,s
alert( arr.slice(-2) ); // s,t (复制从位置-2到尾端的元素)
也可以不带参数地调用它:arr.slice() 会创建一个 arr 的副本。其通常用于获取副本,以进行不影响原始数组的进一步转换。
concat
arr.concat 创建一个新数组,其中包含来自其他数组和其他项的值。
语法:
arr.concat(arg1, arg2...)
它接受任意数量的参数 — 数组或值都可以。
结果是一个包含来自于 arr,然后是 arg1, arg2 的元素的新数组。
如果参数 argN 是一个数组,那么其中的所有元素都会被复制。否则,将复制参数本身
let arr = [1, 2];
alert(arr.concat([3, 4])); // 1,2,3,4
alert(arr.concat([3, 4], [5, 6])); // 1,2,3,4,5,6
alert(arr.concat([3, 4], 5, 6)); // 1,2,3,4,5,6
通常,它只复制数组中的元素,其他对象,即使看起来像数组,仍然会被作为一个整体添加:
let arr = [1, 2]
let arrayLike = {
0: "something",
length: 1
};
alert(arr.concat(arrayLike)); // 1, 2, [object Object]
...但是,如果类数组对象具有 Symbol.isConcatSpreadable 属性,那么它就会被 concat 当作一个数组来处理:此对象中的元素将被添加:
let arr = [1, 2]
let arrayLike = {
0: "something",
1: "else",
[Symbol.isConcatSpreadable]: true,
length: 2
};
alert( arr.concat(arrayLike) ); // 1, 2, something, else
遍历:forEach
arr.forEach(function(item,index,array) {
// ....
})
ES6 提供了三个新的方法 —— entries(), keys() 和 values() —— 用于遍历数组。它们都会返回一个遍历器对象( iterator ),可以用 for...of 循环进行遍历,唯一的区别是 keys() 是对键名的遍历、values() 是对键值的遍历,entries() 是对键值对的遍历。
for(let index of ['a', 'b'].keys()){
console.log(index); // 0 1
}
for(let elem of ['a', 'b'].values()) {
console.log(elem); // 'a' 'b'
}
for(let [index, elem] of ['a', 'b'].entries()){
console.log(index, elem); // 0 "a" 1 "b"
}
如果不使用for...of循环,可以手动调用遍历器对象的next方法,进行遍历。
let letter = ['a', 'b', 'c'];
let entries = letter.entries();
console.log(entries.next().value); // [0, 'a']
console.log(entries.next().value); // [1, 'b']
console.log(entries.next().value); // [2, 'c']
在数组中搜索
indexOf / lastIndexOf 和 includes
这三个方法与字符串操作具有相同的语法,并且作用基本上也与字符串的方法相同,只不过这里是对数组元素而不是字符进行操作:
- arr.indexOf( item, from ) 从索引 from 开始搜索 item,如果找到则返回索引,否则返回 -1
- arr.lastIndexOf( item, from ) —— 和上面相同,只是从右向左搜素
- arr.includes( item, from ) —— 从索引 from 开始搜素 item,找到则返回 true(否则返回 false)
let arr = [1, 0, false];
alert( arr.indexOf(0) ); // 1
alert( arr.indexOf(false) ); // 2
alert( arr.indexOf(null) ); // -1
alert( arr.includes(1) ); // true
请注意,这些方法使用的是严格相等 === 比较。所以如果我们搜索 false,会精确到的确是 false 而不是数字 0。
如果我们想检查是否包含某个元素,并且不想知道确切的索引,那么 arr.includes 是首选。
此外,includes 的一个非常小的差别是他能正确处理 NaN,而不像 indexOf / lastIndexOf:
const arr = [NaN];
alert( arr.indexOf(NaN) ); // -1(应该为 0,但是严格相等 === equality 对 NaN 无效)
alert( arr.includes(NaN) ); // true
find 和 findIndex
想象一下,我们有一个对象数组,我们如何找到具有特定条件的对象?
这时可以用 arr.find 方法。
语法如下:
let result = arr.find(function(item, index, array) {
// 如果返回 true,则返回 item 并停止迭代
// 对于 falsy 则返回 undefined
})
// 示例:
let users = [
{id: 1, name: "John"},
{id: 2, name: "Pete"},
{id: 3, name: "Mary"}
]
let user = users.find(item => item.id === 1)
alert(user.name); // John
在现实生活中,对象数组是很常见的,所以 find 方法非常有用。
注意在这个例子中,我们传给了 find 一个单参数函数 item => item.id == 1。这很典型,并且 find 方法的其他参数很少使用。
arr.findIndex 方法(与 arr.find 方法)基本上是一样的,但它返回找到元素的索引,而不是元素本身。并且在未找到任何内容时返回 -1。
filter
find 方法搜索的是使函数返回 true 的第一个(单个)元素。
如果需要匹配的有很多,我们可以使用 arr.filter(fn)。
语法与 find 大致相同,但是 filter 返回的是所有匹配元素组成的数组:
let results = arr.filter(function(item, index, array) {
// 如果 true item 被 push 到 results,迭代继续
// 如果什么都没找到,则返回空数组
})
例如:
let users = [
{id: 1, name: "John"},
{id: 2, name: "Pete"},
{id: 3, name: "Mary"}
]
let someUsers = users.filter(item => item.id < 3);
alert(someUsers.length); // 2
转换数组
数组转换和重新排序
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) 排序,更改元素的顺序。(译注:原位是指在此数组内,而非生成一个新数组。)
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}.`);
}
split 方法有一个可选的第二个数字参数 — 对数组长度的限制。如果提供了,那么额外的元素会被忽略。但实际上它很少使用:
let arr = 'Bilbo, Gandalf, Nazgul, Saruman'.split(', ', 2);
alert(arr); // Bilbo, Gandalf
调用带有空参数 s 的 split(s),会将字符串拆分为字母数组
arr.join(glue) 与 split 相反。它会在他们之间创建一串由 glue 粘合的 arr 项。
let arr = ['Bilbo', 'Gandalf', 'Nazgul'];
let str = arr.join(';')
alert(str); // Bilbo;Gandalf;Nazgul
reduce / reduceRight
它们用于根据数组计算单个值。
let value = arr.reduce(function(accumulator, item, index, array) {
//...
}, [initial]);
该函数一个接一个地应用于所有的数组元素,并将其结果“搬运”到下一个调用。
参数:
- accumulator — 是上一个函数调用的结果,第一次等于 inital(如果提供了 initial 的话)。
- item — 当前的数组元素
- index — 当前索引
- arr — 数组本身
应用函数时,上一个函数调用的结果将作为第一个参数传递给下一个函数。
因此,第一个参数本质上是累加器,用于存储所有先前执行的组合结果。最后,它成为 reduce 的结果。
通过一行代码得到一个数组的总和:
let arr = [1, 2, 3, 4, 5];
let result = arr.reduce((sum, current) => sum + current, 0)
alert(result); // 15
arr.reduceRight 和 arr.reduce 方法的功能一样,只是遍历为从右到左。
多维数组降维 flat() ,flatMap()
flat() 用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。
[1, 2, [3, 4]].flat()
// [1, 2, 3, 4]
flat() 默认只会“拉平”一层,如果想要"拉平"多层的嵌套数组,可以将 flat() 方法的参数写成一个整数,表示想要拉平的层数,默认为 1。
[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]]
[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]
如果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数。
如果原数组有空位,flat()方法会跳过空位。
[1, 2, , 4, 5].flat()
// [1, 2, 4, 5]
flatMap() 方法对原数组的每个成员执行一个函数(相当于执行 Array.prototype.map() ),然后对返回值组成的数组执行 flat() 方法。该方法返回一个新数组,不改变原数组。
// 相当于 [[[2]], [[4]], [[6]], [[8]]].flat()
[1, 2, 3, 4].flatMap(x => [[x * 2]])
// [[2], [4], [6], [8]]
flatMap()方法的参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。
arr.flatMap(function callback(currentValue[, index[, array]]) {
// ...
}[, thisArg])
// flatMap()方法还可以有第二个参数,用来绑定遍历函数里面的this。