笔记参考javascript.info中文站
数组
1. 声明和取数
常规声明有两种方法:
-
let arr = new Array();
麻烦而且有特殊的问题,一般不使用 -
let arr = [];
很常用,在括号内就可以初始化:
let fruits = ["Apple", "Orange", "Plum"];
取数方面,数组从 0 开始编号,通过方括号取对应位置的数字
let fruits = ["Apple", "Orange", "Plum"];
alert( fruits[0] ); // Apple
也可以通过这种方式替换元素,甚至添加新元素:
let testArr = [5, 3, 'd'];
testArr[6] = 1;
alert(testArr); // [ 5, 3, 'd', <3 empty items>, 1 ]
另外,从上面的例子我们可以看出,类似 python,Javascript 的数组可以存储任何类型的元素
在调取倒数第几位元素时,我们需要注意,不能直接上负数,需要用 at()
函数:
let fruits = ["Apple", "Orange", "Plum"];
alert( fruits[fruits.length-1] ); // Plum,这样写最麻烦
alert( fruits[-1] ); // undefined,这样最直观,符合我们的习惯,但是是错误的
alert( fruits.at(-1) ); // Plum,我们应该这样写
2. pop/push, shift/unshift 方法
-
pop()
取出并返回数组的最后一个元素 -
push( value )
在数组末端添加若干个元素 -
shift()
取出并返回数组的第一个元素 -
unshift()
在数组首端添加若干个元素
举个例子:
let fruits = ["Apple", "Orange", "Pear"];
alert( fruits.pop() ); // 移除 "Pear" 然后 alert 显示出来
alert( fruits ); // Apple, Orange
fruits.push("Pear");
alert( fruits ); // Apple, Orange, Pear
alert( fruits.shift() ); // 移除 Apple 然后 alert 显示出来
alert( fruits ); // Orange, Pear
fruits.unshift('Apple');
alert( fruits ); // Apple, Orange, Pear
push/pop 方法运行的比较快,而 shift/unshift 比较慢。
也就是末端进行操作更方便,这是因为首端操作需要将后面一系列元素全都向前或向后挪动,而末尾操作只需要移动最后的若干元素
3. 内部本质
明确一点:数组是一种特殊的对象
其实与 obj[key]
相同,其中 arr
是对象,而数字用作键(key),其中的元素就是值,而区别在于有无顺序
所以实际上,数组的内部实现是基于对象的,因此很多对象可以进行的操作,从语法层面讲数组也可以进行,举个例子:
let fruits = []; // 创建一个数组
fruits[99999] = 5; // 分配索引远大于数组长度的属性,中间的就是空洞
fruits.age = 25; // 创建一个具有任意名称的属性
但我们创建数组的目的就是构造一个有序集合,因此请不要利用语法漏洞随意书写
4. for
循环和 length
最古朴的方式:
let arr = ["Apple", "Orange", "Pear"];
for (let i = 0; i < arr.length; i++) {
alert( arr[i] );
}
更现代的方式,for..of
:
let fruits = ["Apple", "Orange", "Plum"];
// 遍历数组元素
for (let fruit of fruits) {
alert( fruit );
}
两种方式各有缺点,for 循环太麻烦、可读性差,for...of
无法获取元素索引,只能获取值。
事实上,我们知道数组是一种特殊的对象,因此也可以用 for..in
来遍历
但问题是,有些对象长得特别像数组,当我们统一使用 for..in
来循环的时候,就无法区分他们的区别,从而导致一些错误:
- “类数组” 中有一些隐形的属性,尽管平时看不到,循环过程中依然会遍历到,这些内容不是我们想要的
- for…in 循环适用于普通对象,并且做了对应的优化。但是不适用于数组,因此速度要慢 10-100 倍
5. 一些特性
- length
当我们修改数组的时候,length 属性会自动更新
有趣的是,我们可以手动修改 length,此时数组会自动从头截取对应长度的子数组
let arr = [1, 2, 3, 4, 5];
arr.length = 2; // 截断到只剩 2 个元素
alert( arr ); // [1, 2]
- 多维数组
数组里的元素也可以是另一个数组,这就组成了多维数组,就好像矩阵
let matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
alert( matrix[1][1] ); // 最中间的那个数
- toString
数组转字符串有独特的一套逻辑
显示转换:
let arr = [1, 2, 3];
alert( arr ); // 1,2,3
alert( String(arr) === '1,2,3' ); // true
隐式转换:
alert( [] + 1 ); // "1"
alert( [1] + 1 ); // "11"
- 不要使用
==
比较数组
数组的本质是对象,只有两个同源的对象才会被判定为相等,因此即使两个数组元素一模一样,用==
判断也是false
另外,在数组与原始类型的比较中,数组会自动转换成字符串形式,这更证明用==
比较数组是没意义的
const testArr = [5, 3];
console.log( '5,3' == testArr ); // true
6. 添加/移除数组元素
移除元素可以使用对象使用的 delete
,但移除后对应位置的元素只是会变成 undefined
,而我们希望的是将这个位置一并删除
let arr = ["I", "go", "home"];
delete arr[1]; // remove "go"
alert( arr[1] ); // undefined
所以我们使用的方法是 arr.splice(start[, deleteCount, elem1, ..., elemN])
这个方法可以进行增删改三种操作
let arr = ["I", "study", "JavaScript", 1, 2, 3];
// 不写后面的元素,只有起始位置和个数,则从起始位置开始,删除对应个数个元素
arr.splice(1, 1); // 从索引 1 开始删除 1 个元素
alert( arr ); // ["I", "JavaScript", 1, 2, 3]
// 删除数组的前三项,并使用对应内容代替它们
arr.splice(0, 3, "Let's", "dance");
alert( arr ) // 现在 ["Let's", "dance", 2, 3]
// 将deleteCount设置成0,那就不需要删除任何元素,直接新增
arr.splice(2, 0, "complex", "language");
alert( arr ); // ["Let's", "dance", "complex", "language", 2, 3]
注意,splice
方法支持负向索引
除此之外,还有 arr.slice([start], [end])
可以帮我们返回子数组
let arr = ["t", "e", "s", "t"];
alert( arr.slice(1, 3) ); // e,s(复制从位置 1 到位置 3 的元素)
alert( arr.slice(-2) ); // s,t(复制从位置 -2 到尾端的元素)
**`arr.concat(arg1, arg2...)`** 方法可以创建新数组,数组的元素由 arg 参数决定:
- 如果参数是原始类型,那么直接写进新数组
- 如果参数也是数组,那就把该数组遍历一遍,内部的元素再写进新数组,而如果内部元素本身还是数组,那就直接当作数组写入
- 如果参数是对象,那就被当做对象写作
[object Object]
- 如果对象内好巧不巧有一个属性
Symbol.isConcatSpreadable
,那按照顺序,该属性之前的属性的值会当作一般元素写入新数组,而该属性本身及后面的属性就会被抛弃
7. 遍历:forEach
arr.forEach(function(item, index, array)
方法允许为数组的每个元素都运行一个函数。
// 对每个元素调用 alert
["Bilbo", "Gandalf", "Nazgul"].forEach(alert);
// 而这段代码更详细地介绍了它们在目标数组中的位置:
["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => {
alert(`${item} is at index ${index} in ${array}`);
});
注意,其中的 ${index}
和 ${array}
都是特定字符,有固定的意义
8. 在数组中搜索
8.1 indexOf/lastIndexOf 和 includes
indexOf(item, from)/lastIndexOf(item, from)
和 includes(item, from)
类似,都是看看某元素是否在数组内,区别在于:前者返回 index 或 -1,后者返回 true 或 false
参数 item
是索引目标内容,from
则是一个位置,、从该位置开始查询
let arr = [1, 0, false];
alert( arr.indexOf(false) ); // 2
alert( arr.lastIndexOf(0) ); // 1
alert( arr.includes(1) ); // true
从上面例子中我们还可以注意到一个小细节,indexOf(item, from)
在查找 “false” 的过程中返回的并不是 “0” 的位置 1,而是 false 的位置 2,这是因为该方法使用的比较是严格比较 ===
所以 false 和 0 不相等。
还有一点需要注意,indexOf
不能处理 NaN,而 include
可以
8.2 find 和 findIndex/findLastIndex
如果想查询一个由对象组成的数组,则需要 arr.find(function(item, index, array)
方法
其中 item 是元素(对象),index 是对应的索引,array 是目标数组
如果找到了对应内容,则返回 true
并返回对应的 item
,否则返回 undefined
find()
的用法比较特殊,需要用箭头 =>
:
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
8.3 filter
find()
方法只能返回第一个找到的对象,如果需要返回很多,则需要使用 arr.filter(function(item, index, array)
方法,找到则返回包含所有符合条件的对象组成的数组,如果没有符合的则返回空数组
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
9. 转换数组
9.1 map
map 函数对数组的每个元素都调用内部的一个简单的操作,并返回结果数组。
举个例子:
// 将数组的元素变成每个元素的字符串长度
let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length);
alert(lengths); // 5,7,6
9.2 sort(fn)
arr.sort
方法对数组进行 原位(in-place) 排序,也就是在原有数组内进行修改,而非生成一个新数组,但事实上,该函数依然会返回新数组
另外注意,比较的时候会采用参数 fn
的比较方法进行排序,如果没有这个函数,则默认采用字符串比较,而比较函数只需要返回一个正数表示“大于”,一个负数表示“小于”。
举个例子:
let arr = [ 15, 2, 13 ];
// 该方法重新排列 arr 的内容
arr.sort();
alert( arr ); // 2, 13, 15
// 采用这个比较方法
function compareNumeric(a, b) {
if (a > b) return 1;
if (a == b) return 0;
if (a < b) return -1;
}
arr.sort(compareNumeric);
alert(arr); // 2, 13, 15
比较函数也可以采用以前提过的常用库中的函数,如 localeCompare()
函数
9.3 reverse
arr.reverse 方法用于颠倒 arr 中元素的顺序
举个例子:
let arr = [1, 2, 3, 4, 5];
arr.reverse();
alert( arr ); // 5,4,3,2,1
9.4 split 和 join
split()
函数可以将字符串转成数组,由某一符号进行分割标准:
let names = 'Bilbo, Gandalf, Nazgul';
let arr = names.split(', '); // 由 “, ” 分割
for (let name of arr) {
alert( `A message to ${name}.` ); // A message to Bilbo(和其他名字)
}
arr.join(glue)
函数恰好相反,可以让数组的元素粘合成由 glue
分割的字符串:
let arr = ['Bilbo', 'Gandalf', 'Nazgul'];
let str = arr.join(';'); // 使用分号 ; 将数组粘合成字符串
alert( str ); // Bilbo;Gandalf;Nazgul
9.5 reduce/reduceRight
reduce()
方法和前面的 map()
非常相似,都是对数组内的元素一个个进行函数调用,而区别在于,reduce/reduceRight
是对上一个函数的结果进行函数操作,也就是 “累加” 式的:
let value = arr.reduce(function(accumulator, item, index, array) {
// ...
}, [initial]);
举个例子:
let arr = [1, 2, 3, 4, 5];
let result = arr.reduce((sum, current) => sum + current, 0);
alert(result); // 15
可以看到,上一个调用的结果如何成为下一个调用的第一个参数
此外,初始值,也就是上例中的 “0” 是可以省略的,默认初始值是数组的第一个元素,但如果数组为空则会报错
reduceRight
和 reduce
一样,只是顺序是从右往左
10. 其他特性
10.1 Array.isArray()
数组是基于对象的,不构成单独的语言类型,因此使用 typeof()
查询数组的类型会得到 Object,无法区分
alert(typeof {}); // object
alert(typeof []); // object(相同)
想要判断数组,应该使用 Array.isArray(value)
方法
alert(Array.isArray({})); // false
alert(Array.isArray([])); // true
value
是数组则返回 true
否则返回 false
10.2 “thisArg”
几乎所有调用函数的数组方法 —— 比如 find
,filter
,map
,除了 sort
,都接受一个可选的附加参数 thisArg
,但它很少被使用。
thisArg
参数的值在 func
中变为 this
,者可以帮我们自定义 this
指的是哪个变量
举个例子:
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); // 如果没有第二个参数 army,系统就找不到this是哪个,就会返回 undefined,导致报错
alert(soldiers.length); // 2
alert(soldiers[0].age); // 20
alert(soldiers[1].age); // 23