前方高能!JS中Array的隐藏高级考点大揭秘🤩

120 阅读31分钟

数组初印象:从基础到进阶

image.png

在 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 ()

image.png

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,得到了一个新的数组。

数组遍历大赏:不同场景下的遍历策略

image.png

遍历数组是我们在日常开发中经常要做的事情,就像在百宝箱里翻找宝贝一样,我们需要把数组里的元素一个一个地拿出来处理。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。

特殊数组与对象:稀疏数组和类数组对象

image.png

稀疏数组:神秘的空槽

在 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()方法进行转换。

常用方法大盘点:数组操作的得力助手

image.png

掌握数组的各种常用方法,是我们用好数组这个百宝箱的关键。这些方法就像是数组的十八般武艺,能帮我们实现各种复杂的数据操作。接下来,我们就来详细了解一下数组的增删改查、排序反转等常用方法。

增删改查方法

  • 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 + 新特性:为数组带来新活力

image.png

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],就像是先给每个宝贝进行加工,然后再把它们整理到一个平面上。

性能优化与注意事项:让数组操作更高效稳定

image.png

性能优化建议

在操作数组时,性能优化是我们需要考虑的重要因素。就像在整理百宝箱时,合理的方法能让我们更快地找到和整理宝贝。

批量赋值优于频繁 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保持不变,这样代码的可读性和可维护性都更好。

总结:回顾数组的高级考点

image.png

到这里,我们已经深入探索了 JavaScript 中数组的高级考点,从数组的创建和初始化,到各种遍历、操作方法,再到 ES6 + 的新特性以及性能优化和注意事项,数组就像一个充满宝藏的百宝箱,里面的每一个方法和特性都能帮助我们更好地处理数据。

在实际开发中,数组是我们不可或缺的好帮手,无论是处理简单的数据列表,还是构建复杂的数据结构,数组都能发挥重要作用。希望大家通过这篇文章,能对数组有更深入的理解和掌握,在面对数组相关的问题时,能够游刃有余地解决。

当然,纸上得来终觉浅,绝知此事要躬行。大家一定要多动手实践,通过实际的代码编写来巩固所学的知识。可以尝试用不同的方法解决同一个问题,对比它们的优缺点,这样才能真正掌握数组的高级技巧。

如果你在学习过程中有任何疑问或者心得,欢迎在评论区留言分享,让我们一起在 JavaScript 的世界里继续探索,共同进步!