数组初印象:从基础到进阶
在 JavaScript 的奇妙世界里,数组(Array)就像是一个超级百宝箱📦,是我们最常用的数据结构之一。它可以用来存储一系列的数据,无论是数字、字符串、对象,还是其他数组,统统都能装进去,就像哆啦 A 梦的口袋,啥都有!
数组:可遍历的宝藏盒
从本质上来说,数组是一种可遍历的对象。这意味着我们可以按顺序一个一个地访问数组中的每一个元素,就像打开宝箱,依次拿出里面的宝贝一样。比如,我们有一个包含水果名称的数组:
const fruits = ['apple', 'banana', 'cherry'];
我们可以使用循环来遍历这个数组,把每个水果的名字都打印出来:
for (let i = 0; i < fruits.length; i++) {
console.log(fruits[i]);
}
这样,我们就能依次看到apple、banana和cherry啦!是不是很简单?
new Array ():创建数组的传统方式
创建数组的方式有好几种,其中一种是使用new Array()构造函数。这种方式就像是请工匠按照你的要求打造一个宝箱🧰。
- 创建空数组:
const emptyArray = new Array();
这就好比打造了一个空空如也的宝箱,里面暂时没有任何宝贝。
- 创建指定长度的数组:
const fiveLengthArray = new Array(5);
console.log(fiveLengthArray); // [empty × 5]
这里创建了一个长度为 5 的数组,不过里面的元素都是空的,就像一个有 5 个格子的宝箱,但每个格子都还没放东西。在 V8 引擎的设计中,这种方式创建的数组有点像 C++ 里固定大小的分配,不过 JavaScript 的数组可灵活多啦,后面我们会讲到。
- 创建包含元素的数组:
const numberArray = new Array(1, 2, 3);
console.log(numberArray); // [1, 2, 3]
这时候,宝箱里就已经放好了1、2、3这几个宝贝啦!
数组字面量:更简洁的宝箱创建法
除了new Array(),我们还有一种更简洁的方式来创建数组,那就是使用数组字面量,用[]来表示。这就像是直接拿出一个已经装满宝贝的宝箱,简单又直接😎。
const fruitsArray = ['apple', 'banana', 'cherry'];
对比一下new Array('apple', 'banana', 'cherry'),是不是数组字面量看起来清爽多了?所以在实际开发中,我们更常用数组字面量来创建数组。
两者区别大揭秘
- 语法简洁性:数组字面量语法更简洁直观,代码看起来更清爽,就像穿了件简约的衣服,轻便又好看;而new Array()语法相对繁琐一些,就像穿了件厚重的外套。
- 参数解析差异:当使用new Array()时,如果只传递一个数字参数,它会被当作数组的长度,而不是元素;但数组字面量不会有这种歧义,[5]就是包含一个元素5的数组,而new Array(5)是长度为 5 的空数组。这一点可一定要注意,不然就容易闹笑话啦!
静态方法的魔法:Array.of () 与 Array.from ()
JavaScript 的数组还有两个非常实用的静态方法:Array.of()和Array.from()。这两个方法就像是数组的超级助手,能帮我们解决很多实际问题。
Array.of ():创建数组的新姿势
Array.of()方法用于创建一个包含传入参数的数组。这听起来好像和直接用数组字面量创建数组差不多,但它有一个很厉害的地方,就是能避免new Array()在某些情况下的歧义。比如说,当你使用new Array(5)时,它创建的是一个长度为 5 的空数组;而Array.of(5)创建的是一个包含元素5的数组。看下面的例子,就一目了然啦:
const array1 = new Array(5);
console.log(array1); // [empty × 5]
const array2 = Array.of(5);
console.log(array2); // [5]
是不是很神奇?Array.of()就像是一个贴心的助手,总是能准确理解你的意图,创建出你想要的数组。无论你传入多少个参数,它都会把这些参数作为数组的元素,组成一个新的数组返回给你。比如:
const mixedArray = Array.of(1, 'apple', true);
console.log(mixedArray); // [1, 'apple', true]
这样,我们就轻松创建了一个包含不同类型元素的数组。在实际开发中,当你需要快速创建一个包含特定元素的数组时,Array.of()就派上用场啦!
Array.from ():神奇的转换大师
Array.from()方法则是一个神奇的转换大师,它可以将类数组对象或可迭代对象转换为真正的数组。什么是类数组对象呢🧐?简单来说,就是一个拥有length属性和若干索引属性的对象,看起来有点像数组,但又不是真正的数组。比如arguments对象,它是函数内部的一个类数组对象,保存了函数调用时传入的所有参数。
function sum() {
const argsArray = Array.from(arguments);
return argsArray.reduce((acc, num) => acc + num, 0);
}
console.log(sum(1, 2, 3)); // 6
在这个例子中,我们使用Array.from()将arguments对象转换为真正的数组,然后就可以使用数组的reduce()方法来计算这些参数的和啦。
Array.from()还可以将可迭代对象(如Set、Map)转换为数组。比如,我们有一个Set集合,里面包含了一些不重复的数字,现在想把它转换成数组:
const numberSet = new Set([1, 2, 2, 3, 4, 4]);
const numberArray = Array.from(numberSet);
console.log(numberArray); // [1, 2, 3, 4]
通过Array.from(),我们轻松地把Set集合转换成了数组,而且自动去除了重复的元素。
另外,Array.from()还接受第二个参数mapFn,这是一个映射函数,它会对每个元素进行处理,然后将处理后的结果放入新数组中。这就像是给每个元素都施了一个魔法,让它们变成你想要的样子😎。比如:
const originalArray = [1, 2, 3, 4];
const doubledArray = Array.from(originalArray, num => num * 2);
console.log(doubledArray); // [2, 4, 6, 8]
这里,我们使用Array.from()的映射函数,将原数组中的每个数字都乘以 2,得到了一个新的数组。
数组遍历大赏:不同场景下的遍历策略
遍历数组是我们在日常开发中经常要做的事情,就像在百宝箱里翻找宝贝一样,我们需要把数组里的元素一个一个地拿出来处理。JavaScript 为我们提供了多种遍历数组的方式,每种方式都有它的特点和适用场景,接下来我们就来一一了解一下。
for 循环:经典永流传
for循环是最经典的遍历方式,它就像是一把万能钥匙,虽然看起来普通,但在很多场景下都非常实用🔑。它特别适合需要索引的场景,比如我们要给数组里的每个元素都加上一个索引前缀:
const numbers = [10, 20, 30, 40];
for (let i = 0; i < numbers.length; i++) {
numbers[i] = 'Item ' + (i + 1) + ': ' + numbers[i];
}
console.log(numbers);
// ["Item 1: 10", "Item 2: 20", "Item 3: 30", "Item 4: 40"]
在这个例子中,for循环的索引i就派上了大用场,我们通过它可以方便地访问和修改数组中的每个元素。而且for循环还可以很灵活地控制循环的起始条件、终止条件和步长。比如,我们只想遍历数组的前半部分:
const fruits = ['apple', 'banana', 'cherry', 'date', 'elderberry'];
for (let i = 0; i < fruits.length / 2; i++) {
console.log(fruits[i]);
}
这样就只会打印出apple和banana啦!
for...of:ES6 的清新之风
for...of是 ES6 引入的新特性,它就像是一阵清新的风,给我们带来了更简洁、语义更清晰的遍历体验🍃。它直接遍历数组的元素,而不需要像for循环那样通过索引来访问。比如,我们要打印出数组中的每个水果:
const fruits = ['apple', 'banana', 'cherry'];
for (const fruit of fruits) {
console.log(fruit);
}
是不是感觉代码简洁了很多?而且for...of还可以和break、continue、return等语句配合使用,让我们在遍历过程中可以更灵活地控制流程。比如,我们只想打印出banana之前的水果:
const fruits = ['apple', 'banana', 'cherry'];
for (const fruit of fruits) {
if (fruit === 'banana') {
break;
}
console.log(fruit);
}
这样就只会打印出apple了。
for...in:不适合数组的遍历者
for...in主要用于遍历对象的可枚举属性,虽然它也可以用来遍历数组,但并不推荐,就像让一个篮球运动员去踢足球,虽然也能踢,但不是他的强项🏀。因为for...in遍历数组时,返回的是数组元素的索引,但这些索引是字符串类型的,而不是数字类型,这可能会导致一些意想不到的问题。比如:
const numbers = [10, 20, 30];
for (const index in numbers) {
console.log(index, typeof index);
}
这里打印出来的index是"0"、"1"、"2",类型是字符串,而不是我们期望的数字。而且for...in还会遍历到数组的一些非元素属性,比如我们给数组添加了一个自定义属性:
const numbers = [10, 20, 30];
numbers.customProperty = 'This is a custom property';
for (const index in numbers) {
console.log(index, numbers[index]);
}
这样就会把customProperty也打印出来,这显然不是我们想要的结果。所以,除非你有特殊需求,否则尽量不要用for...in来遍历数组。
forEach:回调遍历的便捷之选
forEach方法通过回调函数来遍历数组,它就像是一个勤劳的小助手,帮你把每个元素都送到回调函数里处理🧑✈️。比如,我们要打印出数组中每个元素的平方:
const numbers = [1, 2, 3, 4];
numbers.forEach((number) => {
console.log(number * number);
});
forEach的回调函数可以接受三个参数:当前元素、当前元素的索引和数组本身。比如:
const numbers = [10, 20, 30];
numbers.forEach((number, index, array) => {
console.log(`Element at index ${index} is ${number} in array ${array}`);
});
不过要注意,forEach没有返回值,它主要用于对数组的每个元素执行一些操作,而不是生成一个新的数组。而且在forEach中不能使用break和continue语句来中断循环,如果需要中断循环,可以使用some或every方法来代替。
map/filter/reduce/some/every:高阶函数的魅力
这几个方法都是数组的高阶函数,它们就像是数组的超级技能,让我们可以更高效地处理数组数据。
- map:数据映射大师
map方法会对数组中的每个元素执行一个回调函数,并返回一个新的数组,新数组中的元素是回调函数的返回值。这就像是给每个元素都照了一面神奇的镜子,镜子里映出的是经过处理后的元素。比如,我们要把数组中的每个数字都乘以 2:
const numbers = [1, 2, 3];
const doubledNumbers = numbers.map((number) => number * 2);
console.log(doubledNumbers);
这样就得到了一个新的数组[2, 4, 6]。
- filter:数据筛选专家
filter方法用于筛选数组中的元素,它会根据回调函数的返回值来决定是否保留该元素,返回一个新的数组,新数组中只包含满足条件的元素。就像用一个筛子筛选豆子,把不符合条件的豆子都筛掉。比如,我们要筛选出数组中的偶数:
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter((number) => number % 2 === 0);
console.log(evenNumbers);
这样就得到了[2, 4]。
- reduce:数据归约神器
reduce方法可以对数组中的每个元素执行一个回调函数,将其结果汇总为单个返回值。它就像是一个收纳大师,把所有的元素都整理归纳到一起。比如,我们要计算数组中所有数字的和:
const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((acc, number) => acc + number, 0);
console.log(sum);
这里的acc是累加器,初始值为 0,每次循环都会把当前元素加到acc上,最后返回的就是所有元素的和。
- some:存在性探测器
some方法用于检查数组中是否至少有一个元素满足指定的条件,如果有一个元素满足条件,就返回true,否则返回false。就像在一堆石头里找有没有钻石,只要找到一颗,就可以说有钻石存在💎。比如,我们要检查数组中是否有大于 10 的元素:
const numbers = [5, 8, 12, 3];
const hasGreaterThan10 = numbers.some((number) => number > 10);
console.log(hasGreaterThan10);
这里因为有12大于10,所以返回true。
- every:一致性检查器
every方法用于检查数组中的所有元素是否都满足指定的条件,如果所有元素都满足条件,就返回true,否则返回false。就像检查一群学生的作业是否都完成了,只要有一个没完成,就返回false。比如,我们要检查数组中的所有元素是否都大于 0:
const numbers = [1, 2, 3, 4];
const allGreaterThan0 = numbers.every((number) => number > 0);
console.log(allGreaterThan0);
因为所有元素都大于 0,所以返回true。
特殊数组与对象:稀疏数组和类数组对象
稀疏数组:神秘的空槽
在 JavaScript 的数组世界里,还有两种比较特殊的存在,那就是稀疏数组和类数组对象。稀疏数组就像是一个有很多空房间的酒店🏨,数组中的某些位置是空的,也就是存在空槽(holes)。比如:
const sparseArray = [1, , 3];
console.log(sparseArray);
console.log(sparseArray.length);
这里的sparseArray就是一个稀疏数组,它的第二个位置是空槽,但是它的length属性仍然是 3,就好像酒店虽然有些房间没人住,但房间总数还是不变的。当我们使用for...in循环来遍历稀疏数组时,会发现它不会遍历到空槽,就像在酒店里找住客,不会去敲那些空房间的门一样:
const sparseArray = [1, , 3];
for (const index in sparseArray) {
console.log(index);
}
这里只会打印出0和2,而不会打印出1。不过要注意,使用for...of循环或者数组的一些方法(如forEach、map、filter等)时,空槽会被当作undefined来处理。
类数组对象:形似数组的 “模仿者”
类数组对象则像是一个模仿数组的演员🎭,它看起来有点像数组,有length属性,也可以通过索引来访问元素,但它并不是真正的数组,不能直接使用数组的方法。比如函数内部的arguments对象,就是一个类数组对象:
function showArgs() {
console.log(arguments);
console.log(Array.isArray(arguments));
}
showArgs(1, 2, 3);
这里的arguments有length属性,也可以通过arguments[0]、arguments[1]这样的方式来访问元素,但它不是数组。不过不用担心,我们可以使用Array.from()方法把类数组对象转换成真正的数组,就像给演员换上合适的服装,让他真正成为数组的一员:
function showArgs() {
const argsArray = Array.from(arguments);
console.log(argsArray);
console.log(Array.isArray(argsArray));
}
showArgs(1, 2, 3);
这样,我们就得到了一个真正的数组,可以尽情使用数组的各种方法啦!除了arguments对象,像document.querySelectorAll()返回的结果也是类数组对象,同样可以用Array.from()方法进行转换。
常用方法大盘点:数组操作的得力助手
掌握数组的各种常用方法,是我们用好数组这个百宝箱的关键。这些方法就像是数组的十八般武艺,能帮我们实现各种复杂的数据操作。接下来,我们就来详细了解一下数组的增删改查、排序反转等常用方法。
增删改查方法
- push/pop:数组尾部的魔法
push方法用于在数组的末尾添加一个或多个元素,并返回添加新元素后的数组长度。这就像是在宝箱的底部又放进去几件宝贝。比如:
const fruits = ['apple', 'banana'];
const newLength = fruits.push('cherry');
console.log(fruits);
console.log(newLength);
这里fruits数组末尾添加了cherry,数组变成了['apple', 'banana', 'cherry'],push方法返回的新长度是 3。
pop方法则相反,它用于删除数组的最后一个元素,并返回该元素。就像是从宝箱底部拿出一件宝贝。例如:
const fruits = ['apple', 'banana', 'cherry'];
const removedFruit = fruits.pop();
console.log(fruits);
console.log(removedFruit);
这里cherry被从数组末尾移除,数组变成了['apple', 'banana'],pop方法返回的是被移除的cherry。
- unshift/shift:数组头部的操作
unshift方法用于在数组的开头添加一个或多个元素,并返回新数组的长度。这就像是在宝箱的顶部放进去几件宝贝,让新放进去的宝贝排在最前面。比如:
const numbers = [2, 3];
const newLength = numbers.unshift(1);
console.log(numbers);
console.log(newLength);
这里numbers数组开头添加了1,数组变成了[1, 2, 3],unshift方法返回的新长度是 3。
shift方法用于删除数组的第一个元素,并返回该元素。就像是从宝箱顶部拿出一件宝贝。例如:
const numbers = [1, 2, 3];
const removedNumber = numbers.shift();
console.log(numbers);
console.log(removedNumber);
这里1被从数组开头移除,数组变成了[2, 3],shift方法返回的是被移除的1。
- splice:数组的万能手术刀
splice方法可以说是数组的万能手术刀,它既可以删除元素,也可以插入元素,还可以替换元素。它的第一个参数是起始位置,第二个参数是要删除的元素个数,后面的参数是要插入的元素。比如,我们要删除数组中的某个元素:
const numbers = [1, 2, 3, 4, 5];
const removedElements = numbers.splice(2, 1);
console.log(numbers);
console.log(removedElements);
这里从numbers数组的索引 2(也就是元素 3)开始,删除 1 个元素,数组变成了[1, 2, 4, 5],splice方法返回的是被删除的3。
如果我们要插入元素:
const numbers = [1, 2, 4, 5];
numbers.splice(2, 0, 3);
console.log(numbers);
这里从numbers数组的索引 2 开始,删除 0 个元素(也就是不删除),插入3,数组变成了[1, 2, 3, 4, 5]。
如果要替换元素,只需要把要删除的元素个数设置为要替换的元素个数,再传入新的元素即可:
const numbers = [1, 2, 3, 4, 5];
const replacedElements = numbers.splice(2, 1, 99);
console.log(numbers);
console.log(replacedElements);
这里从numbers数组的索引 2 开始,删除 1 个元素(也就是3),插入99,数组变成了[1, 2, 99, 4, 5],splice方法返回的是被替换的3。
- slice:数组的片段截取
slice方法用于截取数组的片段,它返回一个新的数组,不会修改原数组。就像是从宝箱里拿出一部分宝贝,单独放在一个小盒子里。它接受两个参数,第一个参数是起始位置(包含),第二个参数是结束位置(不包含)。比如:
const numbers = [1, 2, 3, 4, 5];
const subArray = numbers.slice(1, 3);
console.log(subArray);
console.log(numbers);
这里从numbers数组的索引 1 开始,截取到索引 3(不包含),得到的新数组是[2, 3],原数组numbers保持不变。
如果省略第二个参数,slice会截取到数组的末尾:
const numbers = [1, 2, 3, 4, 5];
const subArray = numbers.slice(2);
console.log(subArray);
这样就得到了[3, 4, 5]。
slice方法还支持负数索引,表示从数组末尾开始计算。比如:
const numbers = [1, 2, 3, 4, 5];
const subArray = numbers.slice(-3, -1);
console.log(subArray);
这里从倒数第 3 个元素(也就是3)开始,截取到倒数第 1 个元素(不包含,也就是不包含5),得到的新数组是[3, 4]。
- concat:数组合并的魔法棒
concat方法用于合并两个或多个数组,它会返回一个新的数组,不会修改原数组。就像是把几个宝箱里的宝贝都倒在一起,组成一个新的大宝箱。比如:
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combinedArray = arr1.concat(arr2);
console.log(combinedArray);
console.log(arr1);
console.log(arr2);
这里arr1和arr2合并成了一个新的数组[1, 2, 3, 4, 5, 6],原数组arr1和arr2都保持不变。
concat方法还可以接受非数组参数,这些参数会被当作单个元素添加到新数组中:
const arr = [1, 2, 3];
const newArray = arr.concat(4, [5, 6]);
console.log(newArray);
这里4被当作单个元素,[5, 6]被展开,最终得到的新数组是[1, 2, 3, 4, 5, 6]。
- join:数组转字符串的桥梁
join方法用于将数组的所有元素连接成一个字符串,它接受一个可选的分隔符参数,默认的分隔符是逗号。就像是把宝箱里的宝贝用一根绳子串起来。比如:
const fruits = ['apple', 'banana', 'cherry'];
const fruitString = fruits.join(', ');
console.log(fruitString);
这里fruits数组的元素用逗号和空格连接起来,得到的字符串是"apple, banana, cherry"。
如果不传递分隔符,所有元素会紧密连接在一起:
const fruits = ['apple', 'banana', 'cherry'];
const fruitString = fruits.join('');
console.log(fruitString);
这样得到的字符串就是"applebananacherry"。
- sort/reverse:数组排序与反转
sort方法用于对数组进行排序,默认情况下,它会按照字符串的 Unicode 编码顺序进行排序。就像是给宝箱里的宝贝按照某种顺序重新排列。比如:
const numbers = [3, 1, 4, 1, 5, 9];
numbers.sort();
console.log(numbers);
这里numbers数组按照字符串顺序排序后,变成了[1, 1, 3, 4, 5, 9]。
如果要按照数字大小进行排序,可以传递一个比较函数:
const numbers = [3, 1, 4, 1, 5, 9];
numbers.sort((a, b) => a - b);
console.log(numbers);
这样就按照升序排列,得到[1, 1, 3, 4, 5, 9]。如果要降序排列,只需要把比较函数改为(a, b) => b - a即可。
reverse方法用于反转数组的元素顺序,就像是把宝箱里的宝贝倒过来放。比如:
const numbers = [1, 2, 3, 4, 5];
numbers.reverse();
console.log(numbers);
这里numbers数组反转后,变成了[5, 4, 3, 2, 1]。
- find/findIndex/includes:数组查找小能手
find方法用于查找数组中第一个满足指定条件的元素,并返回该元素。就像是在宝箱里找特定的宝贝,找到第一个就拿出来。比如,我们要在数组中找到第一个大于 5 的元素:
const numbers = [1, 3, 7, 9, 4];
const foundNumber = numbers.find((number) => number > 5);
console.log(foundNumber);
这里找到的第一个大于 5 的元素是7。
findIndex方法则是查找数组中第一个满足指定条件的元素的索引,如果没有找到则返回 - 1。就像是在宝箱里找特定宝贝的位置。比如,我们要找到数组中第一个大于 5 的元素的索引:
const numbers = [1, 3, 7, 9, 4];
const foundIndex = numbers.findIndex((number) => number > 5);
console.log(foundIndex);
这里第一个大于 5 的元素7的索引是 2。
includes方法用于检查数组中是否包含某个元素,返回一个布尔值。就像是检查宝箱里有没有某个特定的宝贝。比如:
const fruits = ['apple', 'banana', 'cherry'];
const hasBanana = fruits.includes('banana');
console.log(hasBanana);
const hasOrange = fruits.includes('orange');
console.log(hasOrange);
这里fruits数组中包含banana,所以hasBanana是true;不包含orange,所以hasOrange是false。
数组常见面试题
在面试中,数组相关的问题经常出现,下面我们就来看看几个经典的数组面试题。
数组去重:多种方法大比拼
数组去重是一个很常见的需求,就像是从宝箱里挑出重复的宝贝,只留下独一无二的。
- 方法一:利用 Set 数据结构
ES6 中的Set数据结构可以自动去除重复的值,我们可以利用这一点来实现数组去重。就像是有一个神奇的筛子,能自动把重复的宝贝筛掉。代码如下:
const arr = [1, 2, 2, 3, 3, 4];
const uniqueArr = [...new Set(arr)];
console.log(uniqueArr);
这里先把数组arr转换成Set,Set会自动去除重复的元素,然后再把Set转回数组,就得到了去重后的数组[1, 2, 3, 4]。
- 方法二:filter + indexOf
我们还可以使用filter方法结合indexOf方法来实现数组去重。filter方法会过滤出满足条件的元素,indexOf方法可以查找元素在数组中的第一个索引。我们让filter只保留那些在数组中第一次出现的元素,就实现了去重。代码如下:
const arr = [1, 2, 2, 3, 3, 4];
const uniqueArr = arr.filter((item, index) => arr.indexOf(item) === index);
console.log(uniqueArr);
这里filter会遍历数组中的每个元素,indexOf(item)会返回item在数组中第一次出现的索引,如果这个索引和当前的index相等,说明这个元素是第一次出现,就保留下来,最终得到去重后的数组[1, 2, 3, 4]。
数组扁平化:层层剥开的艺术
数组扁平化是指将一个多维数组转换为一维数组,就像是把一个嵌套的宝箱一层一层打开,把里面的宝贝都放在一个平面上。
- 方法一:使用 flat 方法
ES6 中的flat方法可以用于数组扁平化,它接受一个可选的参数depth,表示要扁平化的深度,默认值是 1。如果传入Infinity,则可以扁平化任意深度的数组。就像是有一把神奇的铲子,能按照你的要求把嵌套的宝箱铲平。代码如下:
const arr = [1, [2, 3], [4, [5, 6]]];
const flattenedArr = arr.flat(Infinity);
console.log(flattenedArr);
这里使用flat(Infinity)把arr数组扁平化,得到[1, 2, 3, 4, 5, 6]。
- 方法二:递归 reduce
我们也可以使用reduce方法结合递归的方式来实现数组扁平化。reduce方法可以对数组中的每个元素执行一个回调函数,将其结果汇总为单个返回值。在回调函数中,我们判断当前元素是否是数组,如果是数组,就递归调用扁平化函数,否则就直接把元素添加到结果数组中。代码如下:
const arr = [1, [2, 3], [4, [5, 6]]];
function flattenArray(arr) {
return arr.reduce((acc, item) => {
return acc.concat(Array.isArray(item)? flattenArray(item) : item);
}, []);
}
const flattenedArr = flattenArray(arr);
console.log(flattenedArr);
这里reduce方法会遍历数组中的每个元素,对于数组元素,递归调用flattenArray进行扁平化,对于非数组元素,直接添加到累加器acc中,最终得到扁平化后的数组[1, 2, 3, 4, 5, 6]。
数组乱序:Fisher-Yates 洗牌算法
数组乱序是指将数组中的元素随机打乱顺序,就像是洗牌一样,把宝箱里的宝贝打乱重新排列。
Fisher-Yates 洗牌算法是一种常用的数组乱序算法,它的基本思想是从数组的末尾开始,依次与前面的元素进行交换,每次交换的位置是随机的。代码如下:
function shuffleArray(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
const arr = [1, 2, 3, 4, 5];
const shuffledArr = shuffleArray(arr);
console.log(shuffledArr);
这里从数组的末尾开始,对于每个元素arr[i],随机生成一个索引j(0到i之间),然后将arr[i]和arr[j]进行交换,经过一轮循环后,数组就被打乱了,每次运行得到的结果都可能不同。
ES6 + 新特性:为数组带来新活力
ES6 及以后的版本为数组带来了许多强大的新特性,这些特性就像是给数组这个百宝箱添加了一些神奇的工具,让我们在处理数组时更加得心应手。
解构赋值:便捷的变量提取
解构赋值是 ES6 中一个非常实用的特性,它可以让我们从数组或对象中快速提取值并赋给变量,就像是有一双灵巧的手,能准确地从百宝箱里拿出你想要的宝贝并放在指定的地方。
- 基本解构:
从数组中提取值赋给变量,变量的顺序与数组元素的顺序相对应。比如:
const numbers = [10, 20, 30];
const [a, b, c] = numbers;
console.log(a);
console.log(b);
console.log(c);
这里a被赋值为10,b被赋值为20,c被赋值为30,是不是很方便?就像从宝箱里依次拿出三个宝贝,分别放在a、b、c这三个位置。
- 默认值设置:
当数组中对应的位置没有值或者为undefined时,可以为变量设置默认值。比如:
const numbers = [10];
const [a, b = 20, c = 30] = numbers;
console.log(a);
console.log(b);
console.log(c);
这里a被赋值为10,由于数组中没有第二个和第三个元素,所以b和c使用默认值,b为20,c为30。这就好比宝箱里没有你要的第二个和第三个宝贝,那就拿出提前准备好的备用宝贝放在b和c的位置。
展开运算符:数组操作的神器
展开运算符(...)是 ES6 中另一个非常强大的特性,它在数组操作中就像是一把万能钥匙,能轻松解决很多问题。
- 合并数组:
使用展开运算符可以非常简洁地合并多个数组。比如:
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combinedArr = [...arr1, ...arr2];
console.log(combinedArr);
这里arr1和arr2被合并成了一个新的数组[1, 2, 3, 4, 5, 6],代码简洁明了,就像把两个宝箱里的宝贝倒在一起,组成一个新的大宝箱。
- 复制数组:
展开运算符还可以用于复制数组,创建一个新的数组副本。比如:
const originalArr = [1, 2, 3];
const copiedArr = [...originalArr];
console.log(copiedArr);
这里copiedArr是originalArr的一个副本,它们虽然内容相同,但在内存中是两个不同的数组,就像用一个宝箱复制出了另一个一模一样的宝箱。
- 在指定位置插入元素:
利用展开运算符和slice方法,我们可以在数组的指定位置插入元素。比如,我们要在数组[1, 2, 4, 5]的索引 2 处插入3:
const arr = [1, 2, 4, 5];
const newArr = [...arr.slice(0, 2), 3, ...arr.slice(2)];
console.log(newArr);
这里先通过slice(0, 2)取出数组的前两个元素[1, 2],然后插入3,再通过slice(2)取出数组从索引 2 开始的剩余元素[4, 5],最后合并成新的数组[1, 2, 3, 4, 5],就像是在宝箱的指定位置插入了一件新宝贝。
flat/flatMap:扁平化与映射的新方式
flat和flatMap方法为我们处理数组的扁平化和映射提供了新的思路,让我们能更高效地处理复杂的数组结构。
- flat:数组扁平化:
flat方法用于将多维数组扁平化为一维数组,它可以接受一个参数depth,表示要扁平化的深度,默认值是 1。如果传入Infinity,则可以扁平化任意深度的数组。比如:
const nestedArr = [1, [2, 3], [4, [5, 6]]];
const flattenedArr = nestedArr.flat(Infinity);
console.log(flattenedArr);
这里nestedArr是一个多维数组,通过flat(Infinity)将其扁平化为一维数组[1, 2, 3, 4, 5, 6],就像是把一个嵌套的宝箱一层一层打开,把里面的宝贝都放在一个平面上。
- flatMap:映射并扁平化:
flatMap方法首先对数组中的每个元素执行一个映射函数,然后将结果扁平化为一个新数组。它相当于先执行map方法,再执行flat方法,但flatMap更高效。比如,我们有一个包含数字数组的数组,要对每个数字加 1 后再扁平化:
const nestedArr = [[1, 2], [3, 4]];
const result = nestedArr.flatMap((subArr) => subArr.map((num) => num + 1));
console.log(result);
这里先对每个子数组中的数字执行map方法,将其加 1,得到[[2, 3], [4, 5]],然后再通过flatMap的扁平化操作,得到最终结果[2, 3, 4, 5],就像是先给每个宝贝进行加工,然后再把它们整理到一个平面上。
性能优化与注意事项:让数组操作更高效稳定
性能优化建议
在操作数组时,性能优化是我们需要考虑的重要因素。就像在整理百宝箱时,合理的方法能让我们更快地找到和整理宝贝。
批量赋值优于频繁 push。当我们需要向数组中添加大量元素时,如果使用push方法一个一个地添加,就像是一次只往宝箱里放一件宝贝,效率比较低。比如:
const arr = [];
for (let i = 0; i < 10000; i++) {
arr.push(i);
}
这种方式会导致频繁的内存操作,影响性能。更好的做法是使用批量赋值,一次性分配足够的空间,然后再进行赋值操作,就像一次性把所有宝贝都放进宝箱:
const arr = new Array(10000);
for (let i = 0; i < 10000; i++) {
arr[i] = i;
}
这样可以减少内存操作的次数,提高效率。
另外,在大数据量操作时,建议使用原生方法。因为原生方法通常是经过优化的,执行效率更高。比如在进行数组排序时,使用数组的sort方法比自己实现一个排序算法要快得多:
const numbers = [3, 1, 4, 1, 5, 9];
numbers.sort((a, b) => a - b);
注意事项
在使用数组时,还有一些注意事项需要我们牢记,避免出现一些意想不到的问题。
数组是引用类型,这意味着当我们把一个数组赋值给另一个变量时,实际上传递的是数组的引用,而不是数组的副本。就像有两个标签指向同一个宝箱,无论通过哪个标签去操作宝箱里的宝贝,都会影响到另一个标签看到的结果。比如:
const arr1 = [1, 2, 3];
const arr2 = arr1;
arr2[0] = 99;
console.log(arr1);
这里arr1和arr2指向同一个数组,所以修改arr2的元素也会影响到arr1。
数组的length属性可以手动设置,这会影响数组的内容。当我们把length属性设置得比当前数组长度大时,数组会增加一些空元素;当把length属性设置得比当前数组长度小时,数组中索引大于或等于新length的元素会被删除。比如:
const arr = [1, 2, 3, 4, 5];
arr.length = 3;
console.log(arr);
arr.length = 7;
console.log(arr);
这里先把arr的length设置为 3,数组变为[1, 2, 3],后面又把length设置为 7,数组变为[1, 2, 3, empty × 4]。
对于稀疏数组,部分方法会跳过空槽。比如forEach、map、filter等方法在遍历稀疏数组时,会把空槽当作undefined来处理,但有些方法(如for...in)则不会遍历到空槽。在使用这些方法时,要注意它们对稀疏数组的处理方式。
在现代 JavaScript 开发中,推荐使用函数式编程的方式,避免直接修改原数组。因为直接修改原数组可能会带来一些难以调试的问题,而且不利于代码的维护和理解。比如,我们可以使用map方法创建一个新的数组,而不是直接修改原数组的元素:
const arr = [1, 2, 3];
const newArr = arr.map((num) => num * 2);
console.log(newArr);
console.log(arr);
这里map方法创建了一个新的数组newArr,原数组arr保持不变,这样代码的可读性和可维护性都更好。
总结:回顾数组的高级考点
到这里,我们已经深入探索了 JavaScript 中数组的高级考点,从数组的创建和初始化,到各种遍历、操作方法,再到 ES6 + 的新特性以及性能优化和注意事项,数组就像一个充满宝藏的百宝箱,里面的每一个方法和特性都能帮助我们更好地处理数据。
在实际开发中,数组是我们不可或缺的好帮手,无论是处理简单的数据列表,还是构建复杂的数据结构,数组都能发挥重要作用。希望大家通过这篇文章,能对数组有更深入的理解和掌握,在面对数组相关的问题时,能够游刃有余地解决。
当然,纸上得来终觉浅,绝知此事要躬行。大家一定要多动手实践,通过实际的代码编写来巩固所学的知识。可以尝试用不同的方法解决同一个问题,对比它们的优缺点,这样才能真正掌握数组的高级技巧。
如果你在学习过程中有任何疑问或者心得,欢迎在评论区留言分享,让我们一起在 JavaScript 的世界里继续探索,共同进步!