作为前端开发者,JavaScript数组操作是日常开发中最频繁使用的技能之一。在面试中,面试官往往会通过数组方法的考察来评估候选人的基础功底和实际编码能力。本文将系统梳理JS数组的核心方法,从底层原理到实际应用场景,帮助你彻底掌握这一关键知识点。
数组的基础操作可以归纳为CRUD(Create, Read, Update, Delete)四大类。理解这些方法的特性是高效使用数组的关键。
增操作(Create):
push():在数组末尾添加元素,返回新数组长度unshift():在数组开头插入元素,返回新数组长度splice():在指定位置插入元素,返回空数组(因为删除0项)concat():合并数组,返回新数组关键区别:前三个方法会修改原数组(变异方法),而concat不会改变原数组
javascript复制// 增操作示例
let fruits = ['apple', 'banana'];
fruits.push('orange'); // 返回3,fruits变为['apple','banana','orange']
fruits.unshift('pear'); // 返回4,fruits变为['pear','apple','banana','orange']
fruits.splice(2, 0, 'grape'); // 在索引2处插入'grape',fruits变为['pear','apple','grape','banana','orange']
let newFruits = fruits.concat(['melon']); // 新数组,fruits保持不变
删操作(Delete):
pop():删除并返回最后一个元素shift():删除并返回第一个元素splice():删除指定位置元素,返回被删除元素的数组slice():返回子数组的浅拷贝javascript复制// 删操作示例
let numbers = [1, 2, 3, 4, 5];
numbers.pop(); // 返回5,numbers变为[1,2,3,4]
numbers.shift(); // 返回1,numbers变为[2,3,4]
numbers.splice(1, 2); // 返回[3,4],numbers变为[2]
let subArray = numbers.slice(0, 1); // 返回[2],numbers仍为[2]
排序是数据处理中的常见需求,JS提供了两种原生的排序方法:
reverse():反转数组顺序sort():按指定规则排序javascript复制// 排序示例
let nums = [3, 1, 4, 1, 5, 9];
nums.reverse(); // [9,5,1,4,1,3]
nums.sort(); // [1,1,3,4,5,9] - 默认按字符串Unicode排序
// 正确数字排序
nums.sort((a, b) => a - b); // 升序 [1,1,3,4,5,9]
nums.sort((a, b) => b - a); // 降序 [9,5,4,3,1,1]
// 对象数组排序
let users = [
{name: 'John', age: 25},
{name: 'Alice', age: 22},
{name: 'Bob', age: 30}
];
users.sort((a, b) => a.age - b.age); // 按年龄升序
注意事项:sort()方法默认按字符串Unicode码点排序,对数字排序会产生意外结果,必须提供比较函数
join():将数组元素连接成字符串toString():将数组转换为字符串(逗号分隔)toLocaleString():本地化字符串表示javascript复制// 转换方法示例
let date = new Date();
let arr = [1, 'a', date];
arr.join('|'); // "1|a|Thu Jul 20 2023 12:00:00 GMT+0800"
arr.toString(); // "1,a,Thu Jul 20 2023 12:00:00 GMT+0800"
arr.toLocaleString(); // "1,a,2023/7/20 12:00:00"(根据地区设置)
ES5引入的迭代方法极大提升了JS处理数组的能力,也是函数式编程风格的基础。
forEach():遍历数组,无返回值map():映射新数组filter():过滤符合条件的元素reduce():归约计算some()/every():条件判断javascript复制// 迭代方法示例
let nums = [1, 2, 3, 4, 5];
// forEach - 单纯遍历
nums.forEach((num, index) => {
console.log(`索引${index}的值是${num}`);
});
// map - 数据转换
let squares = nums.map(n => n * n); // [1, 4, 9, 16, 25]
// filter - 数据筛选
let evens = nums.filter(n => n % 2 === 0); // [2, 4]
// reduce - 数据聚合
let sum = nums.reduce((acc, curr) => acc + curr, 0); // 15
// some/every - 条件检查
let hasEven = nums.some(n => n % 2 === 0); // true
let allEven = nums.every(n => n % 2 === 0); // false
ES6新增的查找方法为数组操作提供了更多便利:
find():返回第一个符合条件的元素findIndex():返回第一个符合条件的索引includes():检查是否包含某元素javascript复制// 查找方法示例
let users = [
{id: 1, name: 'John'},
{id: 2, name: 'Alice'},
{id: 3, name: 'Bob'}
];
let user = users.find(u => u.id === 2); // {id: 2, name: 'Alice'}
let index = users.findIndex(u => u.id === 2); // 1
let exists = users.includes(users[1]); // true
这是面试中最常被问到的区别点。理解哪些方法会改变原数组(变异方法),哪些不会,对于写出可预测的代码至关重要。
会改变原数组的方法:
不改变原数组的方法:
记忆技巧:增删改排序相关的方法通常会改变原数组,而查询和转换类方法通常返回新数组
场景1:删除数组中的假值
javascript复制let mixed = [0, 1, false, 2, '', 3];
let truthy = mixed.filter(Boolean); // [1, 2, 3]
场景2:数组去重
javascript复制let dupes = [1, 2, 2, 3, 4, 4, 5];
let unique = [...new Set(dupes)]; // [1, 2, 3, 4, 5]
// 或
let unique = dupes.filter((item, index) => dupes.indexOf(item) === index);
场景3:数组扁平化
javascript复制let nested = [1, [2, [3, [4]]]];
let flat = nested.flat(Infinity); // [1, 2, 3, 4]
虽然数组方法很强大,但在处理大数据量时需要注意性能:
array.map().filter().reduce()会创建中间数组,可能影响性能javascript复制// 性能优化示例
// 不好的写法:创建了中间数组
let result = bigArray
.map(x => x * 2)
.filter(x => x > 10)
.reduce((a, b) => a + b, 0);
// 更好的写法:单次遍历
let result = bigArray.reduce((acc, curr) => {
let doubled = curr * 2;
if (doubled > 10) {
return acc + doubled;
}
return acc;
}, 0);
javascript复制// 错误写法
let nums = [10, 5, 40, 25];
nums.sort(); // [10, 25, 40, 5] - 按字符串排序!
// 正确写法
nums.sort((a, b) => a - b); // [5, 10, 25, 40]
javascript复制let sparse = [1, , 3]; // 注意中间的"空洞"
sparse.forEach(x => console.log(x)); // 只输出1和3
sparse.map(x => x * 2); // [2, empty, 6]
javascript复制let objects = [{x:1}, {x:2}];
let copy = objects.slice();
copy[0].x = 10; // 原数组也会被修改!
javascript复制// 添加元素
let newArr = [...arr, newItem];
// 合并数组
let combined = [...arr1, ...arr2];
javascript复制let [first, ...rest] = [1, 2, 3, 4];
// first = 1, rest = [2, 3, 4]
现代JavaScript新增了一些强大的数组方法:
javascript复制// 扁平化数组
let nested = [1, [2, [3]]];
nested.flat(); // [1, 2, [3]]
nested.flat(2); // [1, 2, 3]
// flatMap = map + flat(1)
let phrases = ["hello world", "the quick brown fox"];
let words = phrases.flatMap(phrase => phrase.split(" "));
// ["hello", "world", "the", "quick", "brown", "fox"]
javascript复制// 从类数组对象创建数组
let nodeList = document.querySelectorAll('div');
let divArray = Array.from(nodeList);
// 创建包含单个数字的数组
Array.of(7); // [7]
Array.of(1, 2, 3); // [1, 2, 3]
javascript复制// fill - 填充数组
let arr = [1, 2, 3];
arr.fill(0, 1); // [1, 0, 0]
// copyWithin - 内部拷贝
let nums = [1, 2, 3, 4, 5];
nums.copyWithin(0, 3); // [4, 5, 3, 4, 5]
基于对数组方法的深入理解,我们可以封装一些实用的工具函数:
javascript复制class ArrayUtils {
// 分组函数
static groupBy(array, keyFunc) {
return array.reduce((acc, item) => {
const key = keyFunc(item);
(acc[key] = acc[key] || []).push(item);
return acc;
}, {});
}
// 分页函数
static paginate(array, pageSize, pageNum) {
return array.slice((pageNum - 1) * pageSize, pageNum * pageSize);
}
// 加权随机选择
static weightedRandom(items, weights) {
let cumulativeWeights = [];
weights.reduce((acc, curr, i) => {
let sum = acc + curr;
cumulativeWeights[i] = sum;
return sum;
}, 0);
const random = Math.random() * cumulativeWeights.at(-1);
return items[cumulativeWeights.findIndex(w => w >= random)];
}
}
// 使用示例
let data = [
{category: 'fruit', name: 'apple'},
{category: 'fruit', name: 'banana'},
{category: 'vegetable', name: 'carrot'}
];
let grouped = ArrayUtils.groupBy(data, item => item.category);
/*
{
fruit: [
{category: 'fruit', name: 'apple'},
{category: 'fruit', name: 'banana'}
],
vegetable: [
{category: 'vegetable', name: 'carrot'}
]
}
*/
了解不同数组方法的性能特点对于编写高效代码很重要。以下是常见操作的性能对比:
| 操作 | 方法 | 时间复杂度 | 备注 |
|---|---|---|---|
| 添加元素 | push() | O(1) | 最佳选择 |
| 删除元素 | pop() | O(1) | 最佳选择 |
| 开头添加 | unshift() | O(n) | 避免在大数组使用 |
| 开头删除 | shift() | O(n) | 避免在大数组使用 |
| 查找元素 | indexOf() | O(n) | 线性搜索 |
| 排序 | sort() | O(n log n) | 通常使用快速排序 |
| 遍历 | for循环 | O(n) | 通常比forEach快 |
| 映射 | map() | O(n) | 创建新数组 |
| 过滤 | filter() | O(n) | 创建新数组 |
实际编码中,应根据具体场景选择最合适的方法。例如,在需要频繁在数组开头增删元素的场景,考虑使用链表等其他数据结构替代数组。
题目1:实现一个函数,将二维数组扁平化并去重
javascript复制function flattenAndUnique(arr) {
// 扁平化
const flattened = arr.flat(Infinity);
// 去重
return [...new Set(flattened)];
}
// 测试
let testArr = [[1, 2, 2], [3, 4, 5, 5], [6, 7, 8, [9, 10]]];
console.log(flattenAndUnique(testArr)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
题目2:实现数组的乱序排列(洗牌算法)
javascript复制function shuffleArray(arr) {
// Fisher-Yates洗牌算法
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;
}
// 测试
let ordered = [1, 2, 3, 4, 5];
console.log(shuffleArray(ordered)); // 随机顺序
要深入掌握JS数组,建议进一步学习:
在实际项目中,熟练运用数组方法可以显著提高代码质量和开发效率。建议多练习LeetCode等平台上的数组相关题目,将理论知识转化为实际编码能力。