MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript数组方法的深入剖析

2024-10-297.9k 阅读

数组简介

在JavaScript中,数组是一种非常重要的数据结构,它可以用来存储多个值。数组的元素可以是不同的数据类型,例如数字、字符串、对象甚至其他数组。以下是创建数组的几种常见方式:

// 字面量方式
let arr1 = [1, 2, 3];
// 构造函数方式
let arr2 = new Array(1, 2, 3);

数组的索引从0开始,我们可以通过索引来访问数组中的元素。例如:

let numbers = [10, 20, 30];
console.log(numbers[0]); // 输出 10

改变原数组的方法

push()

push() 方法用于在数组的末尾添加一个或多个元素,并返回新数组的长度。它会改变原数组。

let fruits = ['apple', 'banana'];
let newLength = fruits.push('cherry');
console.log(fruits); // 输出 ['apple', 'banana', 'cherry']
console.log(newLength); // 输出 3

在内部实现上,push 方法首先计算数组的新长度,然后将新元素按顺序赋值到数组末尾的新位置。它通过修改数组的 length 属性来实现元素的添加。

pop()

pop() 方法用于删除并返回数组的最后一个元素,同样会改变原数组。

let numbers = [1, 2, 3];
let removed = numbers.pop();
console.log(numbers); // 输出 [1, 2]
console.log(removed); // 输出 3

pop 方法会先获取数组的最后一个元素,然后将数组的 length 属性减1,从而达到删除最后一个元素的目的。

shift()

shift() 方法用于删除并返回数组的第一个元素,改变原数组。

let colors = ['red', 'green', 'blue'];
let firstColor = colors.shift();
console.log(colors); // 输出 ['green', 'blue']
console.log(firstColor); // 输出'red'

shift 方法在删除第一个元素后,会将后面的元素依次向前移动一个位置,并将 length 属性减1。

unshift()

unshift() 方法用于在数组的开头添加一个或多个元素,并返回新数组的长度,改变原数组。

let animals = ['cat', 'dog'];
let newLength = animals.unshift('bird');
console.log(animals); // 输出 ['bird', 'cat', 'dog']
console.log(newLength); // 输出 3

unshift 方法会先增加数组的 length 属性,然后将原数组元素依次向后移动,再将新元素添加到数组开头。

splice()

splice() 方法可以用于在指定位置添加、删除或替换元素,会改变原数组。

  • 删除元素
let fruits = ['apple', 'banana', 'cherry', 'date'];
let removed = fruits.splice(1, 2);
console.log(fruits); // 输出 ['apple', 'date']
console.log(removed); // 输出 ['banana', 'cherry']

这里从索引1开始删除2个元素。

  • 添加元素
let fruits = ['apple', 'date'];
fruits.splice(1, 0, 'banana', 'cherry');
console.log(fruits); // 输出 ['apple', 'banana', 'cherry', 'date']

从索引1开始,删除0个元素(即不删除),然后添加 'banana''cherry'

  • 替换元素
let fruits = ['apple', 'banana', 'cherry'];
fruits.splice(1, 1, 'date', 'fig');
console.log(fruits); // 输出 ['apple', 'date', 'fig', 'cherry']

从索引1开始,删除1个元素,然后添加 'date''fig'

splice 方法的实现较为复杂,它需要根据参数计算需要移动的元素数量,然后进行相应的插入、删除或替换操作,同时更新数组的 length 属性。

reverse()

reverse() 方法用于颠倒数组中元素的顺序,改变原数组。

let numbers = [1, 2, 3, 4];
numbers.reverse();
console.log(numbers); // 输出 [4, 3, 2, 1]

reverse 方法通过交换数组两端的元素,逐步向中间靠拢,实现数组的颠倒。

sort()

sort() 方法用于对数组的元素进行排序,改变原数组。默认情况下,它按照字符串Unicode码点进行排序。

let numbers = [3, 1, 4, 2];
numbers.sort();
console.log(numbers); // 输出 [1, 2, 3, 4]

如果要进行数值排序,可以提供一个比较函数:

let numbers = [3, 1, 4, 2];
numbers.sort((a, b) => a - b);
console.log(numbers); // 输出 [1, 2, 3, 4]

sort 方法的底层实现通常使用快速排序或归并排序等算法,对于复杂数据类型的排序,比较函数起到了关键作用。

不改变原数组的方法

concat()

concat() 方法用于合并两个或多个数组,返回一个新数组,原数组不变。

let arr1 = [1, 2];
let arr2 = [3, 4];
let newArr = arr1.concat(arr2);
console.log(newArr); // 输出 [1, 2, 3, 4]
console.log(arr1); // 输出 [1, 2],原数组不变

如果 concat 的参数是数组,它会将这些数组的元素展开并合并到新数组中。如果参数不是数组,则直接添加到新数组。

slice()

slice() 方法用于提取数组的一部分,返回一个新数组,原数组不变。

let fruits = ['apple', 'banana', 'cherry', 'date'];
let newFruits = fruits.slice(1, 3);
console.log(newFruits); // 输出 ['banana', 'cherry']
console.log(fruits); // 输出 ['apple', 'banana', 'cherry', 'date'],原数组不变

slice 方法从起始索引开始提取元素,直到结束索引(不包括结束索引)。如果省略结束索引,则提取到数组末尾。

join()

join() 方法用于将数组的所有元素连接成一个字符串,返回这个字符串,原数组不变。

let fruits = ['apple', 'banana', 'cherry'];
let str = fruits.join(', ');
console.log(str); // 输出 'apple, banana, cherry'
console.log(fruits); // 输出 ['apple', 'banana', 'cherry'],原数组不变

join 方法可以接受一个分隔符作为参数,默认分隔符是逗号。

toString()

toString() 方法返回一个表示数组元素的字符串,原数组不变。它实际上是调用了 join(',') 方法。

let numbers = [1, 2, 3];
let str = numbers.toString();
console.log(str); // 输出 '1,2,3'
console.log(numbers); // 输出 [1, 2, 3],原数组不变

indexOf()

indexOf() 方法返回数组中第一个满足条件的元素的索引,如果不存在则返回 -1,原数组不变。

let fruits = ['apple', 'banana', 'cherry', 'banana'];
let index = fruits.indexOf('banana');
console.log(index); // 输出 1

indexOf 方法从数组开头开始查找,使用严格相等(===)进行比较。

lastIndexOf()

lastIndexOf() 方法返回数组中最后一个满足条件的元素的索引,如果不存在则返回 -1,原数组不变。

let fruits = ['apple', 'banana', 'cherry', 'banana'];
let index = fruits.lastIndexOf('banana');
console.log(index); // 输出 3

lastIndexOf 方法从数组末尾开始查找,同样使用严格相等(===)进行比较。

includes()

includes() 方法用于判断数组是否包含某个元素,返回布尔值,原数组不变。

let numbers = [1, 2, 3];
let hasTwo = numbers.includes(2);
console.log(hasTwo); // 输出 true

includes 方法内部也是使用严格相等(===)进行比较,但它对NaN的判断与 indexOf 有所不同,includes 能正确识别数组中的NaN。

迭代方法

forEach()

forEach() 方法用于对数组的每个元素执行一次提供的函数,没有返回值,原数组不变。

let numbers = [1, 2, 3];
numbers.forEach((num, index, array) => {
    console.log(`${num} 是数组中的第 ${index + 1} 个元素,数组本身是 ${array}`);
});

forEach 方法会按照数组索引顺序依次调用回调函数,回调函数接受三个参数:当前元素、当前元素索引和数组本身。

map()

map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果,原数组不变。

let numbers = [1, 2, 3];
let squared = numbers.map(num => num * num);
console.log(squared); // 输出 [1, 4, 9]
console.log(numbers); // 输出 [1, 2, 3],原数组不变

map 方法内部会遍历数组,对每个元素执行回调函数,并将返回值收集到新数组中。

filter()

filter() 方法创建一个新数组,其中包含通过所提供函数实现的测试的所有元素,原数组不变。

let numbers = [1, 2, 3, 4, 5];
let evens = numbers.filter(num => num % 2 === 0);
console.log(evens); // 输出 [2, 4]
console.log(numbers); // 输出 [1, 2, 3, 4, 5],原数组不变

filter 方法遍历数组,对每个元素执行回调函数,如果回调函数返回 true,则将该元素添加到新数组。

reduce()

reduce() 方法对数组中的每个元素执行一个由您提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。

let numbers = [1, 2, 3];
let sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum); // 输出 6

reduce 方法接受两个参数,第一个是 reducer 函数,reducer 函数接受四个参数:累加器(acc)、当前值(cur)、当前索引(index)和数组本身;第二个参数是初始值。如果没有提供初始值,则将数组的第一个元素作为初始值,从第二个元素开始遍历。

reduceRight()

reduceRight() 方法与 reduce() 方法类似,不同之处在于它从数组的末尾开始,以降序顺序将数组元素迭代处理为一个值。

let numbers = [1, 2, 3];
let product = numbers.reduceRight((acc, num) => acc * num, 1);
console.log(product); // 输出 6

reduceRight 同样接受 reducer 函数和初始值,执行逻辑与 reduce 相似,但遍历顺序相反。

every()

every() 方法测试数组的所有元素是否都通过了指定函数的测试,返回一个布尔值,原数组不变。

let numbers = [2, 4, 6];
let allEven = numbers.every(num => num % 2 === 0);
console.log(allEven); // 输出 true

every 方法一旦遇到回调函数返回 false,就会停止遍历并返回 false

some()

some() 方法测试数组中是不是至少有1个元素通过了被提供的函数测试,返回一个布尔值,原数组不变。

let numbers = [1, 2, 3];
let hasEven = numbers.some(num => num % 2 === 0);
console.log(hasEven); // 输出 true

some 方法一旦遇到回调函数返回 true,就会停止遍历并返回 true

数组方法的性能考量

不同的数组方法在性能上存在差异。例如,改变原数组的方法(如 pushpop 等)通常在操作简单数组时性能较好,因为它们直接修改数组内部结构。而不改变原数组的方法(如 concatslice 等)会创建新数组,这在处理大数据量时可能会消耗更多内存。

迭代方法中,forEachmapfilter 等在现代JavaScript引擎中经过了优化,性能表现较为出色。但如果需要提前终止迭代,everysome 会更合适,因为它们在满足条件时会提前停止遍历。

reducereduceRight 虽然功能强大,但由于其需要累积计算,在大数据量下性能可能不如专门用于特定操作(如求和、求积)的简单循环。

在实际应用中,应根据具体需求和数据量来选择合适的数组方法,以达到最佳的性能和内存使用效率。例如,在频繁添加元素的场景下,push 会比 concat 更高效;而在需要根据条件筛选元素并创建新数组时,filter 是一个很好的选择。

同时,对于复杂的数组操作,可以考虑结合多种方法来实现,在保证代码可读性的前提下,尽量优化性能。例如,先使用 filter 筛选出符合条件的元素,再使用 map 对这些元素进行转换,最后使用 reduce 进行汇总计算。

数组方法的兼容性

虽然现代JavaScript引擎对数组方法的支持已经非常广泛,但在实际开发中,仍需要考虑兼容性问题。例如,一些较老的浏览器可能不支持某些新的数组方法,如 includesfindfindIndex 等。

为了确保代码在不同环境下的兼容性,可以使用垫片(polyfill)。垫片是一段代码,用于在不支持某个功能的环境中模拟该功能。例如,对于 Array.prototype.includes,可以这样实现垫片:

if (!Array.prototype.includes) {
    Array.prototype.includes = function (searchElement, fromIndex = 0) {
        if (fromIndex < 0) {
            fromIndex = Math.max(0, this.length + fromIndex);
        }
        for (let i = fromIndex; i < this.length; i++) {
            if (this[i] === searchElement) {
                return true;
            }
        }
        return false;
    };
}

通过这样的垫片代码,即使在不支持 includes 方法的环境中,也能使用该方法的功能。

同样,对于其他新的数组方法,也可以找到相应的垫片实现。在开发中,可以使用一些工具(如Babel)来自动为代码添加必要的垫片,以确保代码在各种目标环境中的兼容性。

总结数组方法的应用场景

  1. 数据添加与删除
    • 当需要在数组末尾添加元素时,使用 push 方法;在开头添加元素则用 unshift
    • 如果要删除数组末尾元素,pop 是首选;删除开头元素则用 shift
    • 对于在数组中间添加、删除或替换元素,splice 方法非常灵活。
  2. 数据合并与提取
    • 合并多个数组,concat 方法能方便地实现,且不改变原数组。
    • 提取数组部分元素形成新数组,slice 方法能满足需求。
  3. 数据转换与筛选
    • 对数组中每个元素进行转换操作,如数据格式化、类型转换等,map 方法是最佳选择。
    • 筛选出符合特定条件的元素,filter 方法能轻松实现。
  4. 数据汇总与判断
    • 对数组元素进行累加、累乘等汇总计算,reducereduceRight 方法提供了强大的功能。
    • 判断数组元素是否全部满足或部分满足某个条件,everysome 方法可以完成。
  5. 数据查找与判断存在性
    • 查找元素的索引,indexOflastIndexOf 分别从开头和末尾查找。
    • 判断数组是否包含某个元素,includes 方法简洁高效。

通过深入理解JavaScript数组方法的特性、性能和兼容性,开发者能够更加高效地处理数组数据,编写出健壮、高性能的JavaScript代码。无论是简单的数据处理,还是复杂的业务逻辑实现,合理运用数组方法都能使代码更加简洁、可读且高效。在实际项目中,根据具体需求灵活选择和组合数组方法,是提升开发效率和代码质量的关键。