1. 为什么数组是前端开发的基石?
在JavaScript的世界里,数组就像是一个万能工具箱,几乎每个项目都离不开它。想象一下你要处理用户列表、商品数据或者页面元素集合,数组都能完美胜任。根据2023年Stack Overflow开发者调查,数组操作是JavaScript中使用频率第二高的特性,仅次于变量声明。
数组的强大之处在于它既能存储有序数据,又能快速进行查找和修改。比如电商网站的商品列表、社交媒体的动态消息流,甚至是游戏中的道具栏,底层都是数组在支撑。这也是为什么几乎所有前端面试都会考察数组操作的原因。
2. 数组的创建与初始化
2.1 基础创建方式
最直接的数组创建方式是使用字面量语法:
javascript复制let fruits = ['苹果', '香蕉', '橙子'];
let numbers = [1, 2, 3, 4, 5];
let mixed = [1, '文本', true, {name: '对象'}];
这种方式的优点是直观且执行效率高。在V8引擎中,字面量创建的数组会被优化为连续内存空间,访问速度更快。
2.2 使用构造函数
虽然不推荐,但了解构造函数方式也很重要:
javascript复制let arr1 = new Array(); // 空数组
let arr2 = new Array(5); // 长度为5的空数组
let arr3 = new Array(1, 2, 3); // [1, 2, 3]
注意:当只传一个数字参数时,创建的是指定长度的空数组,而不是包含该数字的数组。这是新手常犯的错误。
2.3 特殊初始化技巧
实际开发中我们经常需要初始化特定值的数组:
javascript复制// 创建长度为5且全部填充0的数组
let zeros = new Array(5).fill(0);
// 创建1-100的数字数组
let range = Array.from({length: 100}, (_, i) => i + 1);
// 生成随机数数组
let randomArray = Array.from({length: 10}, () => Math.random());
3. 数组元素的增删改查
3.1 添加元素
尾部添加是最常见的操作:
javascript复制let fruits = ['苹果'];
fruits.push('香蕉'); // ['苹果', '香蕉']
头部添加使用unshift:
javascript复制fruits.unshift('橙子'); // ['橙子', '苹果', '香蕉']
中间插入可以用splice:
javascript复制fruits.splice(1, 0, '葡萄');
// 参数:起始位置、删除数量、插入元素
// 结果:['橙子', '葡萄', '苹果', '香蕉']
3.2 删除元素
尾部删除:
javascript复制let last = fruits.pop(); // 返回'香蕉',数组变为['橙子', '葡萄', '苹果']
头部删除:
javascript复制let first = fruits.shift(); // 返回'橙子',数组变为['葡萄', '苹果']
指定位置删除:
javascript复制let removed = fruits.splice(0, 1); // 从索引0开始删除1个元素
3.3 修改元素
直接通过索引修改:
javascript复制fruits[1] = '火龙果'; // ['葡萄', '火龙果']
批量修改可以用splice:
javascript复制fruits.splice(0, 2, '西瓜', '芒果');
// 从0开始删除2个元素,并插入新元素
// 结果:['西瓜', '芒果']
3.4 查询元素
基本查询:
javascript复制let first = fruits[0]; // '西瓜'
let last = fruits[fruits.length - 1]; // '芒果'
查找索引:
javascript复制let index = fruits.indexOf('芒果'); // 1
let lastIndex = fruits.lastIndexOf('芒果'); // 从后往前找
现代查找方法:
javascript复制// 找出第一个符合条件的元素
let result = fruits.find(item => item.length > 1);
// 找出符合条件的索引
let index = fruits.findIndex(item => item === '芒果');
4. 数组的高级操作技巧
4.1 遍历数组的7种方式
- 经典for循环:
javascript复制for (let i = 0; i < fruits.length; i++) {
console.log(fruits[i]);
}
- for...of循环:
javascript复制for (let fruit of fruits) {
console.log(fruit);
}
- forEach方法:
javascript复制fruits.forEach((fruit, index) => {
console.log(index, fruit);
});
- map方法(返回新数组):
javascript复制let upperFruits = fruits.map(f => f.toUpperCase());
- filter方法:
javascript复制let longFruits = fruits.filter(f => f.length > 2);
- reduce方法:
javascript复制let totalLength = fruits.reduce((sum, f) => sum + f.length, 0);
- entries方法:
javascript复制for (let [index, fruit] of fruits.entries()) {
console.log(index, fruit);
}
4.2 数组排序与搜索
基本排序:
javascript复制let numbers = [3, 1, 4, 1, 5, 9];
numbers.sort(); // [1, 1, 3, 4, 5, 9]
自定义排序:
javascript复制let users = [
{name: 'John', age: 25},
{name: 'Alice', age: 20}
];
users.sort((a, b) => a.age - b.age);
二分查找(必须先排序):
javascript复制numbers.sort((a, b) => a - b);
let index = numbers.findIndex(n => n >= 5); // 类似二分查找
4.3 多维数组操作
创建二维数组:
javascript复制let matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
访问元素:
javascript复制let center = matrix[1][1]; // 5
扁平化数组:
javascript复制let flat = matrix.flat(); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
5. 性能优化与常见陷阱
5.1 数组操作的时间复杂度
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 访问元素 | O(1) | 直接通过索引访问 |
| 搜索元素 | O(n) | 需要遍历整个数组 |
| 插入/删除(末尾) | O(1) | push/pop操作很快 |
| 插入/删除(开头) | O(n) | shift/unshift需要移动所有元素 |
| splice | O(n) | 取决于操作位置和元素数量 |
5.2 常见性能陷阱
- 连续使用shift/unshift:
javascript复制// 不好:每次操作都是O(n)
while (arr.length) {
let item = arr.shift(); // 每次都要移动所有元素
process(item);
}
// 更好:反转后使用pop
arr.reverse();
while (arr.length) {
let item = arr.pop(); // O(1)操作
process(item);
}
- 在循环中修改数组:
javascript复制// 可能导致意外行为
for (let i = 0; i < arr.length; i++) {
if (arr[i] === 'delete') {
arr.splice(i, 1); // 修改了数组长度
i--; // 必须调整索引
}
}
- 浅拷贝问题:
javascript复制let original = [{name: 'obj'}];
let copy = original.slice();
copy[0].name = 'changed';
console.log(original[0].name); // 'changed' 原数组也被修改了
5.3 性能优化技巧
- 预分配大数组:
javascript复制// 创建时指定长度比动态扩展更快
let bigArray = new Array(1000000);
- 批量操作代替循环:
javascript复制// 不好
for (let i = 0; i < 1000; i++) {
arr.push(i);
}
// 更好
arr.push(...Array.from({length: 1000}, (_, i) => i));
- 使用TypedArray处理数值数据:
javascript复制// 比普通数组更快且内存占用更少
let buffer = new ArrayBuffer(16);
let int32View = new Int32Array(buffer);
6. 现代JavaScript中的数组新特性
6.1 ES6+新增方法
- Array.from():
javascript复制// 将类数组转为真实数组
let nodeList = document.querySelectorAll('div');
let divArray = Array.from(nodeList);
- Array.of():
javascript复制// 解决new Array的单参数问题
let arr = Array.of(5); // [5] 而不是长度为5的空数组
- find/findIndex:
javascript复制// 查找符合条件的元素
let user = users.find(u => u.age > 18);
- includes:
javascript复制// 比indexOf更语义化
if (fruits.includes('苹果')) {
// ...
}
6.2 ES2023新增方法
- findLast/findLastIndex:
javascript复制// 从后往前查找
let lastEven = [1, 2, 3, 4].findLast(n => n % 2 === 0); // 4
- toReversed/toSorted/toSpliced:
javascript复制// 不改变原数组的新方法
let arr = [3, 1, 2];
let sorted = arr.toSorted(); // [1, 2, 3], arr保持不变
- with方法:
javascript复制// 不可变方式修改指定索引的值
let newArr = arr.with(1, '新值');
7. 实战案例:构建购物车功能
让我们用数组实现一个完整的购物车功能:
javascript复制class ShoppingCart {
constructor() {
this.items = [];
}
addItem(product, quantity = 1) {
const existing = this.items.find(item => item.id === product.id);
if (existing) {
existing.quantity += quantity;
} else {
this.items.push({
...product,
quantity
});
}
}
removeItem(productId) {
this.items = this.items.filter(item => item.id !== productId);
}
updateQuantity(productId, newQuantity) {
const item = this.items.find(item => item.id === productId);
if (item) {
item.quantity = newQuantity;
}
}
getTotal() {
return this.items.reduce((total, item) => {
return total + (item.price * item.quantity);
}, 0);
}
clear() {
this.items = [];
}
get itemCount() {
return this.items.reduce((count, item) => count + item.quantity, 0);
}
}
// 使用示例
const cart = new ShoppingCart();
cart.addItem({id: 1, name: '手机', price: 5999}, 2);
cart.addItem({id: 2, name: '耳机', price: 299});
console.log(cart.getTotal()); // 12297
console.log(cart.itemCount); // 3
这个实现展示了数组在实际业务中的应用,包含了:
- 添加商品(处理重复商品)
- 删除商品
- 修改数量
- 计算总价
- 清空购物车
- 获取商品总数
8. 数组与其他数据结构的转换
8.1 数组 ↔ 字符串
javascript复制// 数组转字符串
let str = fruits.join(', '); // "西瓜, 芒果"
// 字符串转数组
let newArr = str.split(', '); // ["西瓜", "芒果"]
8.2 数组 ↔ Set(去重)
javascript复制let dupArr = [1, 2, 2, 3];
let uniqueSet = new Set(dupArr); // Set {1, 2, 3}
let uniqueArr = Array.from(uniqueSet); // [1, 2, 3]
8.3 数组 ↔ 对象
javascript复制// 数组转对象
let obj = fruits.reduce((acc, fruit, index) => {
acc[`fruit${index}`] = fruit;
return acc;
}, {});
// 对象转数组
let arr = Object.entries(obj).map(([key, value]) => value);
9. 数组的边界情况处理
9.1 稀疏数组
javascript复制let sparse = [1, , 3]; // 中间有空位
console.log(sparse.length); // 3
console.log(sparse[1]); // undefined
// 检测空位
sparse.hasOwnProperty('1'); // false
9.2 数组类型检测
javascript复制// typeof不行
typeof []; // "object"
// 正确方式
Array.isArray([]); // true
[] instanceof Array; // true
Object.prototype.toString.call([]) === '[object Array]'; // true
9.3 超大数组处理
当处理超过10万条数据的数组时:
- 考虑分页或懒加载
- 使用Web Worker避免阻塞UI
- 对于纯数值数据,使用TypedArray
- 避免在渲染层直接操作大数组
10. 测试你的数组知识
10.1 基础题
- 如何创建一个包含1-100的数字数组?
- 如何快速复制一个数组?
- 如何判断一个变量是数组?
10.2 应用题
- 实现一个函数,统计数组中每个元素出现的次数
- 实现数组扁平化函数(支持多层)
- 实现数组去重函数(考虑对象元素)
10.3 挑战题
- 不使用循环,实现数组求和
- 实现一个支持链式调用的数组包装类
- 实现一个惰性求值的数组操作库
在实际项目中,我经常看到开发者忽视数组方法的返回值特性。比如:
javascript复制// 不好的写法
arr.map(item => {
process(item);
});
// 好的写法:要么使用返回值,要么用forEach
let newArr = arr.map(item => process(item));
// 或
arr.forEach(item => {
process(item);
});
另一个常见误区是过度依赖数组索引。现代JavaScript提供了更语义化的方法如find、some、every等,代码可读性更高:
javascript复制// 不够直观
let found;
for (let i = 0; i < users.length; i++) {
if (users[i].age > 18) {
found = users[i];
break;
}
}
// 更好的写法
let found = users.find(user => user.age > 18);
记住,数组操作是前端开发的基石,掌握它们能让你写出更简洁、高效的代码。建议定期复习数组方法,并在实际项目中尝试使用新特性。
