🔃 JavaScript 数组
JavaScript 的数组元素都是按顺序排列,对比 对象 ,数组是 有序集合 。
# 数组的声明 / 访问
创建空数组的两种方法:
let arr = new Array(); let arr = [];1
2
🌰 例子 / 一般常用第二种语法声明数组:
let fruits = ["Apple", "Orange", "Plum"];
🌰 例子 / 使用 new 方法创建数组可能存在问题:
点击查看
对于普通的字符串创建数组,可以直接作为 new Array() 的参数:
let arr = new Array("Apple", "Pear", "etc");
但是当参数为数字时,此时数字意味着指定数组的长度,创建空数组,而不是创建带有该参数的数组。注意区分!
let arr = new Array(2);
console.log(arr.length) // 2
2
获取数组中的元素:使用方括号加索引值
[](数组元素从 0 索引)console.log(fruits[0]) // 'Apple'1替换数组中的元素:直接访问数组元素,赋上新的值(区别于字符串,字符串不能这样操作)
fruits[2] = 'Pear'; // after: fruits = ["Apple", "Pear", "Plum"];1获取数组的长度:(与字符串相同,自带属性
length):console.log(fruits.length) // 31直接打印数组的内容:(与对象区分,对象需要重写
toString方法才能打印内容,下文有关于数组的toString方法详细介绍)console.log(fruits) // Apple,Orange,Plum1数组可以存储任何类型的元素:(单一类型或者混合类型都可以,甚至是对象、函数类型)
let arr = [ 'Apple', { name: 'John' }, true, function() { alert('hello'); } ]; console.log((arr[1].name)) // 获取数组中对象的属性的值 arr[3](); // 执行数组中元素的函数1
2
3
4
# 数组的基本操作
如果想要获取数组的 最后一个元素 。不可以
fruits[-1],因为在 JavaScript 中方括号的索引是按照其字面意思处理的,结果为undefined,只可以fruits[fruits.length - 1]通过获取数组长度显示计算索引。
⭐️ 使用
at()获取数组元素(最近版本更新) 可以直接使用-1:console.log(fruits.at(-1)) // 'Plum'1对于
at():i >= 0,则与arr[i]完全相同。i为负数的情况,它则从数组的尾部向前数。
JavaScript 中的数组既可以用作队列,也可以用作栈。它们允许你从首端 / 末端来添加 / 删除元素。(可以看作数组为双向队列)
队列使用数组的两个方法:
shift()在数组首端取出一个元素(出队)。push()在数组首端添加一个元素(入队)。
栈使用数组的两个方法:
push()在数组末端添加一个元素(进栈)。pop()在数组末端移除一个元素(推栈)。
进一步深入了解数组的方法:
# 作用于 数组末端 的方法
pop():取出并返回数组的最后一个元素。(影响原数组)push(...):在数组末端添加元素。(无返回值,参数为要添加的元素)(相当于arr[arr.length + 1 ] = ...)
# 作用于 数组首端 的方法
shift():取出并返回数组的第一个元素。unshift():在数组首端添加元素。
🌰 例子 / 使用 push() / unshift() 一次添加多个元素:
let fruits = ["Apple"];
fruits.push("Orange", "Peach");
fruits.unshift("Pineapple", "Lemon");
console.log(fruits) // ["Pineapple", "Lemon", "Apple", "Orange", "Peach"]
2
3
4
5
6
🌰 例子 / 接收用户输入的数(判断)求和:
点击查看
function sum() {
let sum = 0;
let arr = [];
while(true) {
let value = prompt('input number', 0)
if(value === '' || value === null || isFinite(value)) break;
value = +value // 处理为数字类型
arr.push(value)
}
for(value of arr) {
sum += value
}
return sum;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 理解数组的内部
数组是一种特殊的对象。使用方括号来访问属性 arr[0] 实际上是来自于对象的语法。它其实与 obj[key] 相同,其中 arr 是对象,而数字用作键(key)。
数组扩展了对象,提供了特殊的方法来处理有序的数据集合以及 length 属性。但从本质上讲,它仍然是一个对象。
JavaScript 只有 8 种基本的数据类型。其中数组仍属于对象类型。行为也与对象相似。
# 数组的复制
与对象的引用复制相同原理。
🌰 例子:
let fruits = ["Banana"]
let arr = fruits; // 实际上,两个数组引用同一个数组
console.log(arr === fruits) // true
arr.push("Pear");
console.log(fruits) // Banana, Pear
2
3
4
5
6
数组真正特殊的是它们的内部实现。JavaScript 引擎尝试把这些元素一个接一个地存储在 连续的内存区域,还有一些其它的优化,以使数组运行得非常快。
但如果破坏数组的连续性去使用数组,例如直接访问远超出初始索引的值,或者给数组对象添加属性,针对数组的优化就不再适用了,然后对应的优化就会被关闭,数组不将存在优势。
- 添加一个非数字的属性,比如
arr.test = 5。- 制造空洞,比如:添加
arr[0],然后添加arr[1000](它们中间什么都没有)。- 以倒序填充数组,比如
arr[1000],arr[999]等等。
注意
所以数组最好作用于 有序数据 的特殊结构,以正确的方式使用数组,才能发挥数组最大的优势。如果需要任意键值,那使用常规的对象。
# 数组的性能
可以从数据结构的「时间复杂度」思考:( push/pop 方法运行的比较快,而 shift/unshift 比较慢)
- 从首端操作,要从首端开始向后(或从后端向前端)推移元素, 数组里的元素越多,移动它们就要花越多的时间,也就意味着越多的内存操作 。
- 而从数组的末端操作,意味着 不需要移动任何元素 。 因为其它元素都保留了各自的索引。这就是为什么
pop/push的操作会特别快。
# 数组的循环
经典方式 / 使用
for的 完整形式 循环:let arr = ["Apple", "Orange", "Pear"]; for (let i = 0; i < arr.length; i++) { console.log(arr[i]); }1
2
3
4
5使用
for…of循环:for (let fruit of fruits) { console.log( fruit ); }1
2
3注意:
for..of不能获取当前元素的索引,只是获取元素值。根据对象特性使用
for…in:for (let key in arr) { alert( arr[key] ); // Apple, Orange, Pear }1
2
3使用
for…in会带来潜在的问题:for..in循环会遍历 所有属性,不仅仅是这些数字属性。对于浏览器和其它环境中的「类数组」对象,会有其它的非数字的属性和方法。这些额外的循环是不必要的。for..in循环适用于普通对象,并且做了对应的优化,但是不适用于数组。
所以不应该使用
for…in处理数组。
# 数组的长度 length
当修改数组的时候, length 属性会自动更新。实际上 length 不是数组里元素的个数,而是最大的数字索引值加一。
🌰 例子 / 一个数组只有一个元素,但是这个元素的索引值很大,那么这个数组的 length 也会很大:
let fruits = [];
fruits[123] = "Apple";
console.log( fruits.length );
2
3
4
但是通常都不会这样使用数组。
🌰 例子 / 手动修改 length :
let arr = [1, 2, 3, 4, 5];
arr.length = 2; // 截断到只剩 2 个元素
alert( arr ); // [1, 2]
arr.length = 5; // 又把 length 加回来
alert( arr[3] ); // undefined
2
3
4
5
6
7
可以看到,这个修改的过程是不可逆的。
利用 length ,清空数组的最简单的方法是 arr.length = 0。
# 多维数组
数组里的项也可以是数组。可以用来创建 矩阵:
let matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
console.log( matrix[1][1] ); // 5
2
3
4
5
6
7
# 数组的 toString()
数组自己的 toString 方法的实现,会返回 以逗号隔开的元素列表字符串。
🌰 例子 / 打印数组:
let arr = [1, 2, 3];
console.log( arr ); // 1,2,3
console.log( String(arr) === '1,2,3' );
2
3
4
🌰 例子 / 数组的打印结果拼接字符串:
console.log( [] + 1 ); // "1"
console.log( [1] + 1 ); // "11"
console.log( [1,2] + 1 ); // "1,21"
2
3
对于数组,没有
Symbol.toPrimitive,也没有valueOf,它们只能执行toString进行转换,所以这里[]就变成了一个空字符串,[1]变成了"1",[1,2]变成了"1,2"。
# 数组的比较
JavaScript 中的数组 不能使用 == 运算符比较,该运算符不会对数组进行特殊处理,它会像 处理任意对象 那样处理数组。
在对象中:
- 仅当两个对象引用的是同一个对象时,它们才相等
==。- 如果
==左右两个参数之中有一个参数是对象,另一个参数是原始类型,那么该对象将会被转换为原始类型。- 严格比较
===更简单,不会进行类型转换。
使用 == 比较数组,除非引用的是同一个数组对象,否则尽管数组的内容相同(实质上仍然不是同一个对象),比较结果永远都不会相等。
console.log([] == []) // 与原始对象比较 alert( 0 == [] ); // true []被转换为空字符串'' alert('0' == [] ); // false1
2
3
4
5
# 数组的更多方法
# 添加 / 移除数组元素
# 🍎 splice
arr.splice (opens new window) 方法可以说是处理数组的瑞士军刀,它可以做所有事情:添加,删除和插入元素。语法如下:
arr.splice(start[, deleteCount, elem1, ..., elemN])
它从索引 start 开始修改 arr :删除 deleteCount 个元素并在当前位置插入 elem1, ..., elemN 。最后 返回已被删除元素的数组。
🌰 例子 / 删除数组元素:
let arr = ["I", "study", "JavaScript"];
arr.splice(1, 1); // 从索引 1 开始删除 1 个元素
console.log(arr) // ["I", "JavaScript"]
2
3
当参数只有
start,从start索引开始删除所有的数组元素。
🌰 例子 / 删除数组元素并替换:
let arr = ["I", "study", "JavaScript", "right", "now"];
// 删除数组的前三项,并使用其他内容代替它们
let removed = arr.splice(0, 3, "Let's", "dance");
console.log(arr) // ["Let's", "dance", "right", "now"]
console.log(removed) // ["I", "study"]
2
3
4
5
🌰 例子 / 在指定位置新增元素:
let arr = ["I", "study", "JavaScript"];
// 从索引 2 开始
// 删除 0 个元素
// 然后插入 "complex" 和 "language"
arr.splice(2, 0, "complex", "language");
console.log(arr) // ["I", "study", "complex", "language", "JavaScript"]
2
3
4
5
6
7
提示
区别于方括号访问,数组的方法中可以使用「负向索引」,即索引值可以为负数,从数组末尾计算位置。
let arr = [1, 2, 5];
// 从索引 -1(尾端前一位)
// 删除 0 个元素,
// 然后插入 3 和 4
arr.splice(-1, 0, 3, 4);
console.log(arr); // 1,2,3,4,5
2
3
4
5
6
7
# slice
用于切割数组,将所有从索引 start 到 end ( ⚠️ 不包括 end )的数组项复制到一个新的数组,返回这个新的数组。
arr.slice([start], [end])
与字符串的
str.slice相似。
🌰 例子:
let arr = ["t", "e", "s", "t"];
console.log(arr.slice(1, 3)) // 'e' 's'
2
🌰 例子 / 不含参数的 slice() 获取原数组的副本,在此副本进行的操作不会影响原来的数组:
console.log(arr.slice) // ['t', 'e', 's', 't']
🌰 例子 / 从末尾开始索引(即使是末尾索引但不代表反过来切割):
console.log(arr.slice(-2)) s,t
# concat
拼接数组,用于创建一个新的数组,包含来自其他数组和其他项的值。可以接受 任意参数 (数组或者值)。结果是包含来自于 arr 、 arg1 、 arg2 等元素的新数组。如果参数 argN 是一个数组,那么其中的 所有元素 都会被复制。如果不是数组,将复制 参数本身(例如对象,即便其中有类似数组的结构,仍然会作为一个整体添加)。
arr.concat(arg1, arg2...)
🌰 例子 :
let arr = [1, 2];
// create an array from: arr and [3,4]
alert( arr.concat([3, 4]) ); // 1,2,3,4
// create an array from: arr and [3,4] and [5,6]
alert( arr.concat([3, 4], [5, 6]) ); // 1,2,3,4,5,6
// create an array from: arr and [3,4], then add values 5 and 6
alert( arr.concat([3, 4], 5, 6) ); // 1,2,3,4,5,6
2
3
4
5
6
7
8
9
10
🌰 例子:复制其他对象:
let arr = [1, 2];
let arrayLike = {
0: "something",
length: 1
};
console.log(arr.concat(arrayLike)) // 1,2,[object Object]
2
3
4
5
6
7
8
🌰 例子:如果类似数组的对象具有 Symbol.isConcatSpreadable 的属性,则可以被 concat 当作一个数组处理:
let arr = [1, 2];
let arrayLike = {
0: "something",
1: "else",
[Symbol.isConcatSpreadable]: true,
length: 2
};
console.log(arr.concat(arrayLike)); // 1,2,something,else
2
3
4
5
6
7
8
9
10
# forEach 遍历
使用 arr.forEach() 允许为数组的 每一个元素 都运行一个函数:
arr.forEach(function(item, index, array) {
// ... do something with item
});
2
3
🌰 例子 / 对每一个元素都运行 alert() :
["Bilbo", "Gandalf", "Nazgul"].forEach(alert);
🌰 例子 / 打印详细信息:
["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => {
console.log(`${item} is at index ${index} in ${array}`);
});
2
3
# 搜索数组
# indexOf / lastIndexOf 和 includes
与字符串的用法基本相同:
arr.indexOf(item, from): 从索引from开始搜索item,如果找到则 返回索引 ,否则返回-1。arr.lastIndexOf(item, from):逆序搜索。找到 返回索引,否则返回-1。arr.includes(item, from):从索引from开始搜索item,如果找到则返回true,否则返回false。
🌰 例子 :
let arr = [1, 0, false];
console.log(arr.indexOf(0)) // 1
console.log(arr.lsatIndexOf(false)) // 0
console.log(arr.includes(0)) // true
2
3
4
⚠️ 注意:这些方法使用的是 严格相等 === 比较。所以如果搜索 false ,会精确到的确是 false 而不是数字 0 。
🌰 例子: includes 它能正确处理 NaN ,不像 indexOf/lastIndexOf :
const arr = [NaN];
alert( arr.indexOf(NaN) ); // -1(应该为 0,但是严格相等对 NaN 无效)
alert( arr.includes(NaN) );// true
2
3
# find / findIndex
在对象数组中要找到特定的对象使用 find ,依次对数组中的 每个元素 调用该函数:
let result = arr.find(function(item, index, array) {
// item 元素,index 索引, array 数组本身
// 如果返回 true,则返回 item 并停止迭代
// 没有搜索到,则返回 undefined
});
2
3
4
5
🌰 例子 :
let users = [
{id: 1, name: "John"},
{id: 2, name: "Pete"},
{id: 3, name: "Mary"}
];
let result = users.find(item => item.id == 1)
2
3
4
5
6
7
最常用的用法就是使用
item去筛选条件,返回筛选后的结果(index和array不常用)。
findIndex 与 find 方法基本上是一样的,但它返回找到元素的索引,而不是元素本身。并且在未找到任何内容时返回 -1 。
# 🍎 filter
find 方法只会找到使函数返回 true 的 第一个(单个)元素。如果需要匹配的元素有很多,可以使用 filter 方法,语法与 find 方法大致相同, filter 返回的是 所有匹配元素组成的数组 。
let results = arr.filter(function(item, index, array) {
// 如果为 true , item 被 push 到 results,迭代继续
// 如果什么都没找到,则返回空数组
});
2
3
4
🌰 例子 :
let users = [
{id: 1, name: "John"},
{id: 2, name: "Pete"},
{id: 3, name: "Mary"}
];
let res = users.filter((item) => item.id < 2)
console.log(res) // [{id: 1, name: "John"}, {id: 2, name: "Pete"},]
2
3
4
5
6
7
8
# 转换数组
# 🍎 map
是最常使用、最有用的数组方法。对数组的每个元素都调用函数,并返回结果数组。
let result = arr.map(function(item, index, array) {
// 返回新值而不是当前元素
})
2
3
🌰 例子 / 将每个元素转换为它的字符串长度:
let lengths = ["Bilbo", "Gandalf", "Nazgul"].map((item)=> item.length)
console.log(lengths) // 5,7,6
2
# 数组排序
# 🍎 sort(fn)
该方法对数组进行 原位(in-place) 排序,更改元素的顺序。
原位:在此数组排序,并非生成新的数组。
🌰 例子:
let arr = [15, 1, 2]
arr.sort()
console.log(arr) // 1, 15, 2
2
3
注意:以上排序结果并非按照数字大小顺序排序,这些元素默认情况下被按字符串进行排序。 所有元素都被转换为字符串,然后进行比较。
如果要想要按照定义的排序顺序,需要提供一个函数作为 arr.sort() 的参数。该函数应该比较两个任意值并返回:
function compare(a, b){
if(a > b) return 1;
if(a == b) return 0;
if(a < b) return -1;
}
let arr = [1, 2, 15]
arr.sort(compare)
console.log(arr) // 1, 2, 15
2
3
4
5
6
7
8
9
由于数组可以由任何内容组成的数组,可能包含数字、字符串、对象或其他任何内容。要对这些不同类型的元素排序,需要一个 排序函数 来确认如何比较这些元素。数组默认是按字符串进行排序的。
arr.sort(fn)遍历数组,使用提供的函数比较其元素并对其重新排序。比较函数只需要返回一个正数表示「大于」,一个负数表示「小于」。
🌰 例子 / 常用比较大小函数(从小到大)(简化为 箭头函数 版本):
[5,4,3,2,1].sort((a, b) => a - b)
# reverse()
用于 颠倒 数组中的顺序(原位修改)。
🌰 例子 :
let arr = [1, 2, 3, 4, 5]
arr.reverse();
console.log(arr) // 5,4,3,2,1
// let res = arr.reverse()
// console.log(res) // 返回值也可以接收结果
2
3
4
5
6
# 🍎 数组拆分 / 组合 split / join
str.split(delim) 方法用于 字符串 上,通过给定的分隔符 delim 将字符串分割成一个数组。(字符串分割为数组)
🌰 例子 :
let names = 'Bilbo, Gandalf, Nazgul';
let arr = names.split(', ');
for (let name of arr) {
console.log(name)
}
2
3
4
5
split() 的第二个可以选择的参数,是对数组长度的限制。如果提供了,那么额外的元素会被忽略。但实际上它很少使用。
🌰 例子:
let arr = 'Bilbo, Gandalf, Nazgul, Saruman'.split(', ', 2);
console.log(arr); // Bilbo, Gandalf
2
🌰 例子 / 第一个参数为空字符串,将字符串拆分为字母数组:
let str = "test";
console.log(str.split('')); // t,e,s,t
2
arr.join(glue) 与 split 相反,它会在它们之间创建一串由 glue 粘合的 arr 项。(数组粘合为字符串)
🌰 例子:
let arr = ['Bilbo', 'Gandalf', 'Nazgul'];
let str = arr.join(';'); // 使用分号 ; 将数组粘合成字符串
console.log(str); // Bilbo;Gandalf;Nazgul
2
3
# 🍎 reduce / reduceRight
这个函数一个接一个地应用于所有数组元素,并将其结果「搬运」到下一个调用。语法如下:
let value = arr.reduce(function(accumulator, item, index, array) {
// ...
}, [initial]);
2
3
参数:
accumulator:(累加器)是上一个函数调用的结果,第一次等于initial(如果提供了initial的话)。item:当前的数组元素。index:当前索引。arr:数组本身。
🌰 例子 / 通过 reduce 计算数组元素总和:
let arr = [1, 2, 3, 4, 5]
let res = arr.reduce((sum, cur) => sum + cur, 0)
console.log(res) // 15
2
3
sum是累加器,cur是当前的元素,0是累加器的初始值。通常只需要这两个参数即可。
也可以省略初始值,数组的第一个元素作为初始值,第二个元素开始迭代,得出的结果相同:
let res = arr.reduce((sum, cur) => sum + cur)
注意省略初始值使用的情况不能为数组为空时,当数组为空时,第一个累加器返回的就是空值导致出错。所以始终建议指定初始值。
arr.reduceRight (opens new window) 和 arr.reduce (opens new window) 方法的功能一样,只是遍历为 从右到左。
# Array.isArray()
由于数组是基于对象的,不构成单独的语言类型,所以 typeof 不能帮助 从数组中区分出普通对象。Array.isArray(value) (opens new window) 用于检验一个对象是否为数组,如果 value 是一个数组,则返回 true ;否则返回 false 。
🌰 例子:
console.log(Array.isArray({})) // false
console.log(Array.isArray([])) // true
2
# thisArg 参数
几乎所有 调用函数的数组方法 , 比如 find , filter , map ,除了 sort 是一个 特例,都接受一个 可选的附加参数 thisArg 。由于较少使用所以涉及以上数组方法并没有介绍。
完整的用法:
arr.find(func, thisArg);
arr.filter(func, thisArg);
arr.map(func, thisArg);
// ...
2
3
4
thisArg 最后可选参数的值在 func 中变为 this 。
🌰 例子 / 使用 army 对象方法作为 过滤器, thisArg 用于传递上下文:
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
实际中,为了可读性,可以写成:
users.filter(user => army.canJoin(user))1
# 数组总结
关于数组的方法备忘录:
添加 / 移除元素相关:
🍎
push(...items):向 尾端 添加元素。🍎
pop():从 尾端 提取一个元素。🍎
shift():从 首端 提取一个元素。🍎
unshift(...items):向 首端 添加元素。🍎
splice(pos, deleteCount, ...items): 从pos开始删除deleteCount个元素,并插入items。🍎
slice(start, end):创建一个新数组,将从索引start到索引end(但不包括end)的元素 复制 进去。concat(...items):返回一个新数组:复制 当前数组的所有元素,并向其中添加items。如果items中的任意一项是一个数组,那么就取其元素。(其他对象则为对象本身)
查找元素相关:
indexOf/lastIndexOf(item, pos):从索引pos开始搜索item,搜索到则返回该项的 索引值,否则返回-1。includes(value):如果数组有value,则返回true,否则返回false。- 🍎
find(func): 通过func过滤元素,返回使func返回true的 第一个值。 - 🍎
filter(func): 通过func过滤元素,返回使func返回true的 所有值。 findIndex和find类似,但返回 索引 而不是值。
遍历元素相关:
- 🍎
forEach(func)—— 对每个元素都调用func,不返回任何内容
- 🍎
转换数组相关:
- 🍎
map(func): 根据对每个元素调用func的结果创建一个新数组。 - 🍎
sort(func): 对数组进行 原位排序,然后返回排序后的数组。 reverse(): 原位反转 数组,然后返回反转后的数组。- 🍎
split/join:将字符串 转换为数组 并返回。 - 🍎
reduce/reduceRight(func, initial):通过对每个元素调用func计算数组上的单个值,并在调用之间传递中间结果。(常见用法累加)
- 🍎
其他:
Array.isArray(arr):检查arr是否是一个数组。
补充数组方法:
- 🍎
arr.some(fn)/arr.every(fn)检查数组。对数组的每个元素调用函数fn:- 有一
true所有就返回true:如果fn返回一个真值,arr.some()立即返回true并停止迭代其余数组项; - 所有
true才返回true:如果fn返回一个假值,arr.every()立即返回false并停止对其余数组项的迭代。
- 有一
🌰 例子 / 使用 every() 检验数组是否全等:
function compareArray(arr1, arr2){
return arr1.length && arr2.length && arr1.every((value, index) => value === arr2[index])
}
compareArray([1,2], [1,2]) // true
2
3
4
5
arr.fill(value, start, end):从索引start到end,用重复的value填充数组。arr.copyWithin(target, start, end):将从位置start到end的所有元素复制到 自身 的target位置(覆盖现有元素)。arr.flat(depth)/arr.flatMap(fn): 从多维数组创建一个新的扁平数组。Array.of(element0[, element1[, …[, elementN]]]): 基于 可变数量的参数 创建一个新的Array实例,而不需要考虑参数的数量或类型。
