在JavaScript中,数组出现的频率还是很高的,梳理整理了下数组的常用操作。
1. 创建数组 #
1.1 创建一维数组 #
const arr1 = []; // 创建一个空数组
const arr2 = ["a", "b", "c", 1, 2]; // 创建并初始化数组
const arr3 = new Array(); // 创建一个空数组
const arr4 = new Array(10); // 长度为10,各项均为空的数组
const arr5 = new Array(6).fill(3); // 长度为6,初始化所有内容均为3的数组
使用fill()
方法填充数据时,最好只填充基本数据类型的数据。若填充的是 object 格式或者数组格式,会导致修改其中一项,其他所有项都会一起改动:
// 错误,不要这么做
const arr5 = new Array(3).fill({ name: "" }); // 创建长度为3,且所有项的name为空的数组
arr5[0].name = "蚊子"; // 打算只修改第0项的name
console.log(arr5); // [{name: '蚊子'},{name: '蚊子'},{name: '蚊子'}]
本来打算只修改第 0 项的数据,结果发现所有项的 name 都变了。这是因为填充的是同一个复杂类型的数据,即引用地址是同一个,导致修改某一项时,其他项也一起变动。
正确的初始化,还是回到原始的循环方式,每次循环,都创建一个新的数据接口来填充。
const arr = [];
let n = 3;
while (n--) {
const obj = { name: "" };
arr.push(obj);
}
arr[0].name = "蚊子";
console.log(arr); // [{name: '蚊子'},{name: ''},{name: ''}]
1.2 初始化二维或者更高维数组 #
有的同学图省事,会嵌套调用两次new Array()
,但这样依然会出现上面的问题,修改某一项的数据,会导致其他项的数据也一起发生变动:
// 错误,不要这么做
const plat = new Array(5).fill(new Array(3).fill(0)); // 创建一个5x3且全部是0的二维数组
如何正确修改?外层使用循环:
const plat = [];
for (let i = 0; i < 5; i++) {
const item = new Array(3).fill(0);
plat.push(item);
}
1.3 创建连续数字的范围数组 #
JavaScript 的数组中并没有原生的range()
方法来创建连续数字的范围数组。这就需要我们自己来实现了。
1.3.1 使用循环 #
循环的方式,简洁易懂。
const range = (start, end) => {
const arr = [];
for (let i = start; i <= end; i++) {
arr.push(i);
}
return arr;
};
1.3.2 使用数组下标 #
数组中无论是什么值,但下标的变化是固定的:从 0 开始自增。利用这个特点,我们也可以创建范围数组。
const range = (start, end) => {
// return Array(end - start + 1)
// .fill(null)
// .map((_, i) => start + i);
return Array(end - start + 1)
.fill(start)
.map((value, i) => value + i);
};
若想获取到范围数组是从 0 开始的,可以直接使用keys()
来获取数组的下标数组:
const rangeFormZero = (end) => {
return [...Array(end).keys()];
};
2. 判断是否是数组类型 #
如何判断一个变量是否是数组类型。
2.1 Array.isArray() #
在 ES2016 的标准中,专门新增了一个方法 Array.isArray()
,来判断变量是否是数组类型。若是数组类型就返回 true,否则返回 false。
Array.isArray([]); // true
Array.isArray(new Array()); // true
Array.isArray(document.querySelectorAll("div")); // false,类数组不是数组
Array.isArray(undefined); // false
优先推荐使用该方法。
2.2 instanceof #
instanceof
运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
[] instanceof Array; // true
new Array() instanceof Array; // true
"abc" instanceof Array; // false
2.3 constructor #
Object 实例的 constructor 数据属性返回一个引用,指向创建该实例对象的构造函数。注意,此属性的值是对函数本身的引用,而不是一个包含函数名称的字符串。
除了 null 原型对象之外,任何对象都会在其 [[Prototype]]
上有一个 constructor 属性。使用字面量创建的对象也会有一个指向该对象构造函数类型的 constructor 属性,例如,数组字面量创建的 Array 对象和对象字面量创建的普通对象。
const o1 = {};
o1.constructor === Object; // true
const o2 = new Object();
o2.constructor === Object; // true
const a1 = [];
a1.constructor === Array; // true
const a2 = new Array();
a2.constructor === Array; // true
const n = 3;
n.constructor === Number; // true
3. 查找 #
查找数组中是否有符合条件的元素,要根据数组中的数据类型和使用场景,来决定使用哪种方法。比如有的只需要判断是否包含即可,有的是要根据某属性获取到完整的元素。
3.1 查找基本类型的元素 #
若只需要判断数组中是否有该元素,并且数组中的数据全都是基本数据类型,可以使用 includes()
, indexOf()
, lastIndexOf()
等。
有同学可能会说 indexOf()
, lastIndexOf()
是用来查找元素下标的,若能查找到则返回该元素所在的下标(若存在多个相同的,则返回第一个检索到的元素的下标),若没有查到则返回-1。但在实际中,也能通过下标的特性来判断元素是否存在。
不过我在这里推荐优先使用 includes()
,从语义上和使用方式上,这才是真正的用来判断元素是否存在的。而前者只是间接上能判断而已。
const arr = ["a", 2, 5, 3, "b", "c", 2];
arr.includes(2); // true
arr.includes("b"); // true
arr.includes(4); // false
arr.includes("a"); // true
arr.includes("a", 2); // false,从下标2开始查找
arr.indexOf(2); // 1,元素2所在的下标为1
arr.indexOf("c"); // 5, 元素c所在的下标为5
arr.indexOf("d"); // -1 没找到
arr.indexOf(2, 3); // 6, 从下标2开始查找
arr.lastIndexOf(5); // 2,从后往前找,但下标依然是从左往右的
arr.lastIndexOf(2); // 6
上面的这几种方法可以接收第 2 个参数,表示从哪个下标开始查找,不传则默认是 0。
3.2 查找复杂类型的元素 #
若数组中的元素是 object 类型或者 array 类型等复杂类型结构的元素,则上面的几种方法就不太适用了。
- find(callback):接收一个回调函数,若元素满足设置条件,则返回第一个满足约定的元素,否则返回 undefined ;
- findIndex(callback):与
find()
类似,但返回的是该元素的下标,若没有满足条件的元素,则返回-1; - findLastIndex(callback):与
findIndex()
类似,从后往前找; - some(callback):任意一个元素满足条件,则返回 true,若所有元素都不满足条件,则返回 false;
- every(callback):所有元素都满足条件时,返回 true;若任一个元素不满足条件,则返回 false;
我们来看下这几个方法的使用:
const students = [
{ name: "张三", score: 89 },
{ name: "李四", score: 96 },
{ name: "王五", score: 54 },
{ name: "赵六", score: 92 },
];
students.find((item) => item.score > 90); // { name: "李四", score: 96 } ,查找第一个超过90分的元素
students.find((item) => item.score === 0); // undefined ,查找0分的元素,没找到,则为 undefined
students.findIndex((item) => item.score > 90); // 1 ,查找超过90分的元素所在的下标
students.findLastIndex((item) => item.score > 90); // 3 ,从后往前查找超过90分的元素所在的下标
students.some((item) => item.score >= 60); // true ,查找是否有及格的同学
students.every((item) => item.score >= 60); // false ,查找是否所有同学都及格
// 还有最朴素的循环的方式
const find = (arr) => {
if (!Array.isArray(arr)) {
return undefined;
}
for (let i = 0; i < arr.length; i++) {
if (arr[i].name === "张三") {
return arr[i];
}
}
return undefined;
};
上面的这些方法,复杂类型的数据都能查找了,基本数据类型就更没问题了。
有的同学可能在想为什么不使用forEach()
和map()
这两个方法?因为这两个方法是会对数组中的所有都执行一次回调函数,即便是遇到了满足条件的元素,也不会停止循环,直到执行完最后一个元素。
3.3 查找下标 #
查找元素下标的方法,我们在前面大致都介绍过了,这里再稍微讲解下。
- indexOf(element, fromIndex?): 返回数组中第一次出现给定元素的下标,如果不存在则返回 -1;
- lastIndexOf(element, fromIndex?): 从后往前找;
- find(callback):接收一个回调函数,若元素满足设置条件,则返回第一个满足约定的元素,否则返回 undefined ;
- findIndex(callback):与
find()
类似,但返回的是该元素的下标,若没有满足条件的元素,则返回-1;
3.4 查找多个满足条件的元素 #
我们可以使用filter()
:包含通过所提供函数实现的测试的所有元素。
filter()
方法是一个迭代方法。它为数组中的每个元素调用提供的 callbackFn 函数一次,并构造一个由所有返回真值的元素值组成的新数组。未通过 callbackFn 测试的数组元素不会包含在新数组中。
const students = [
{ name: "张三", score: 89 },
{ name: "李四", score: 96 },
{ name: "王五", score: 54 },
{ name: "赵六", score: 92 },
];
// 获取所有90分及以上的同学的信息
const arr = students.filter((item) => item.score >= 90);
console.log(arr);
// arr = [
// { name: "李四", score: 96 },
// { name: "赵六", score: 92 },
// ];
filter()
是返回一个包含与原始数组相同的元素(其中某些元素已被过滤掉)的浅拷贝
。即修改 arr 中的数据,也会导致原数据产生变动。
arr[0].score = 100;
console.log(students[1]); // {name: '李四', score: 100}
3.5 借助 Map 或 Set #
对于数据量比较大,并且又需要频繁查找的操作,以空间换时间的思想,我们可以把数组的元素转为 Map 实例或者 Set 实例。
这种方式首先保证查找的 key 是唯一的,否则会导致数据丢失。
const students = [
{ id: 1, name: "张三", sex: "male", score: 89 },
{ id: 2, name: "李四", sex: "male", score: 72 },
{ id: 3, name: "韩梅梅", sex: "female", score: 94 },
{ id: 4, name: "王五", sex: "male", score: 68 },
];
const map = new Map();
students.forEach((student) => {
map.set(student.id, student);
});
map.get(2); // { id: 2, name: "李四", sex: "male", score: 72 }
4. 循环 #
数组循环的方法有很多,除一些常规的for
, while
等循环,还有 JavaScript 数组中特有的循环方法。
const arr = ["a", 2, 5, 3, "b", "c", 2];
for (let i = 0; i < arr.length; i++) {
console.log(i, arr[i]);
}
// for-of
for (const value of arr) {
console.log(value);
}
// for-in
for (const key in arr) {
console.log(key, arr[key]);
}
arr.forEach((ele, index) => {
console.log(ele, index);
});
const newArr = arr.map((ele, index) => {
console.log(ele, index);
return `${ele}-${index}`;
});
arr.reduce((prev, cur) => prev + cur, "");
除 for 循环能控制范围外,其他方法都是遍历整个数组。
5. 数组操作 #
JavaScript 数组内置了很多操作数组的方法。
- push(): 向数组尾部追加元素,可追加多个,返回追加元素后的数组的长度;
- pop(): 移除数组最后一项,返回的是被移除项;
- shift(): 删除数组的第一项元素,返回被删除的元素;
- unshift(): 向数组的头部添加元素,返回的是结果数组的长度;
- slice(start, end?): 浅拷贝数组下标是[start, end)位置的元素,然后返回新的数组;
- splice(start, deleteCount?, item1?): 在原数组某位置上,删除几个元素,并插入新的元素,然后返回被删除的几个元素(若删除项为空,则返回空数组);
- concat(): 用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组;
除 concat()
和 slice()
不影响原数组外,上面的方法都会影响到原数组。
const arr = ["a", 2, 5, 3, "b", "c", 2];
arr.push(4); // 8, 追加元素4后,长度变为0
arr.pop(); // 4,返回被移除的那个元素
arr.shift(); // 'a' ,返回被删除的第一项元素
arr.unshift("d"); // 7
arr.slice(1, 4); // [2, 5, 3]
arr.splice(1, 0, "abc"); // ['d', 'abc', 2, 5, 3, 'b', 'c', 2]
arr.concat([1, 2, 3], 4, 5); // ['d', 'abc', 2, 5, 3, 'b', 'c', 2, 1, 2, 3, 4, 5]
在数组中任意位置删除多个元素或者插入多个元素或删除和插入操作同时进行时,可以使用 splice()
方法。
const arr = ["a", 2, 5, 3, "b", "c", 2];
arr.splice(2, 3); // [5, 3, 'b'],从下标位置2开始,删除3个元素
arr.splice(3, 0, "d"); // arr: ['a', 2, 'c', 'd', 2] ,从下标位置3开始,删除0个元素,然后再插入元素d
arr.splice(3, 0, "e", "f", "g"); // 插入多个元素
6. 拷贝复制数组 #
我们这里讨论的拷贝复制,说的是浅拷贝
。因为 JavaScript 中并没有内置深拷贝的方法,需要开发者自行实现。
6.1 解构 #
ES6 中新增了解构
的操作,可以把数组的元素结构,然后再以数组的类型返回。
const arr = ["a", 3, false, { score: 34 }, 5];
const newArr = [...arr];
6.2 concat()或 slice() #
concat()
和slice()
接收约定的参数,返回新的数组副本。若这两个方法不传入任何参数,则将原数组浅拷贝一份并返回。
const arr = ["a", 3, false, { score: 34 }, 5];
const newArr1 = arr.concat();
const newArr2 = arr.slice();
这两个方法得到的数据一样的。
6.3 直接赋值不是拷贝 #
直接把数组变量赋值给另一个变量,这不是拷贝,而是两个变量指向到了同一个引用地址。
const arr = ["a", 3, false, { score: 34 }, 5];
// 错误,不要这么做
const newArr1 = arr;
6.4 浅拷贝对原数组的影响 #
深拷贝和浅拷贝的区别以及深拷贝的实现,我们就不在这里展开了。这里主要说下浅拷贝对原数组的影响。
在浅拷贝中,若数组中有复杂类型结构的数据(如 object 类型或者数组类型),则原数组和新副本数组,引用的是该复杂类型数据的同一个地址。那修改其中一个对象的属性,另一个对象的属性也一起改变。
但在操作新副本数组的基本数据类型,或者调整数组元素的顺序,或者添加或删除元素等,都不会影响到原数组。
const arr = ["a", 3, false, { score: 34 }, 5];
const newArr = [...arr];
newArr.sort();
console.log(newArr, arr); // [3, 5, { score: 34 }, 'a', false], ["a", 3, false, { score: 34 }, 5]
newArr.shift(); // 删除开头的元素
newArr.push(6);
console.log(newArr, arr); // [5, { score: 34 }, 'a', false, 6], ["a", 3, false, { score: 34 }, 5]
7. 不改变原数组的方法 #
数组执行 reverse()
, sort()
, splice()
这几种方法后,原数组会受到影响。之前为了避免原数组受到影响,会使用前面浅拷贝的方式,先拷贝出一个数组副本,然后再进行操作。
在 ES2023 标准规范里,新发布了 toReversed()
,toSorted()
,toSpliced()
这几种方法,调用后不影响原数组,并返回执行后的新数组数据。
const arr2 = [4, 5, 1, 2, 3];
const reversed = arr2.toReversed();
console.log(arr2, reversed); // [ 4, 5, 1, 2, 3 ] [ 3, 2, 1, 5, 4 ]
const sorted = arr2.toSorted();
console.log(arr2, sorted); // [ 4, 5, 1, 2, 3 ] [ 1, 2, 3, 4, 5 ]
const spliced = arr2.toSpliced(2, 1);
console.log(arr2, spliced); // [ 4, 5, 1, 2, 3 ] [ 4, 5, 2, 3 ]
8. 总结 #
我在这篇文章中,梳理了 JavaScript 数组的一些常用操作,欢迎批评指正。