数组概述
数组是JavaScript中用于存储有序数据集合的对象,是处理列表数据的核心数据结构。
数组的特点
- 有序集合 - 元素按索引顺序存储
- 动态大小 - 长度可以动态改变
- 类型灵活 - 可以包含任意类型的数据
- 零基索引 - 从0开始索引元素
- 丰富方法 - 提供大量内置操作方法
数组的应用场景
- 存储和处理数据列表
- 实现栈、队列等数据结构
- 数据排序和搜索
- 函数式编程操作
- DOM元素集合处理
数组创建
JavaScript提供了多种创建数组的方式,每种方式都有其适用场景:
1. 数组字面量
数组字面量
// 空数组
let emptyArray = [];
// 包含元素的数组
let fruits = ["苹果", "香蕉", "橙子"];
// 混合数据类型数组
let mixed = ["字符串", 42, true, null, { name: "对象" }];
// 多维数组
let matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
// 稀疏数组
let sparse = [1, , 3]; // 第二个元素为empty
console.log(fruits[0]); // "苹果"
console.log(matrix[1][1]); // 5
console.log(sparse.length); // 3
console.log(1 in sparse); // false (第二个元素不存在)
数组字面量特点:
- 语法简洁直观
- 性能最佳
- 适合创建已知元素的数组
- 可以创建稀疏数组
2. Array 构造函数
Array 构造函数
// 空数组
let arr1 = new Array();
// 指定长度的数组
let arr2 = new Array(5); // 创建长度为5的空数组
// 包含元素的数组
let arr3 = new Array("a", "b", "c");
// 单个数字参数的特殊情况
let arr4 = new Array(3); // [empty × 3] (长度为3的空数组)
let arr5 = new Array("3"); // ["3"] (包含一个元素"3")
console.log(arr2.length); // 5
console.log(arr3[1]); // "b"
注意: 使用
new Array(n)创建数组时,如果只有一个数字参数,会创建指定长度的空数组,而不是包含该数字的数组。
3. Array.of() 和 Array.from()
Array.of() 和 Array.from()
// Array.of() - 创建包含参数的数组
let arr1 = Array.of(5); // [5] (不是长度为5的空数组)
let arr2 = Array.of(1, 2, 3); // [1, 2, 3]
let arr3 = Array.of(); // [] (空数组)
// Array.from() - 从类数组或可迭代对象创建数组
let arr4 = Array.from("hello"); // ["h", "e", "l", "l", "o"]
let arr5 = Array.from([1, 2, 3], x => x * 2); // [2, 4, 6]
// 从Set创建数组
let set = new Set([1, 2, 3]);
let arr6 = Array.from(set); // [1, 2, 3]
// 从类数组对象创建数组
let arrayLike = {
0: "a",
1: "b",
2: "c",
length: 3
};
let arr7 = Array.from(arrayLike); // ["a", "b", "c"]
Array.from()应用:
- 将字符串转换为字符数组
- 将Set/Map转换为数组
- 将类数组对象转换为真实数组
- 创建指定范围和模式的数组
4. 其他创建方式
其他创建方式
// 使用fill()创建填充数组
let filledArray = new Array(5).fill(0); // [0, 0, 0, 0, 0]
// 使用keys()创建索引数组
let indexArray = [...Array(5).keys()]; // [0, 1, 2, 3, 4]
// 使用生成器创建数组
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
let rangeArray = [...range(1, 5)]; // [1, 2, 3, 4, 5]
// 使用映射创建数组
let mappedArray = Array.from({length: 5}, (_, i) => i * i); // [0, 1, 4, 9, 16]
数组基本操作
掌握数组的基本操作是使用数组的基础:
访问和修改元素
访问和修改元素
let colors = ["red", "green", "blue"];
// 访问元素
console.log(colors[0]); // "red"
console.log(colors[colors.length - 1]); // "blue" (最后一个元素)
// 修改元素
colors[1] = "yellow";
console.log(colors); // ["red", "yellow", "blue"]
// 添加新元素
colors[3] = "purple";
console.log(colors); // ["red", "yellow", "blue", "purple"]
// 访问不存在的索引
console.log(colors[10]); // undefined
// 负索引访问 (使用at()方法)
console.log(colors.at(-1)); // "purple" (最后一个元素)
console.log(colors.at(-2)); // "blue"
数组长度
数组长度
let arr = ["a", "b", "c"];
// 获取长度
console.log(arr.length); // 3
// 修改长度 (截断或扩展数组)
arr.length = 2;
console.log(arr); // ["a", "b"] (截断)
arr.length = 5;
console.log(arr); // ["a", "b", empty × 3] (扩展)
// 清空数组
arr.length = 0;
console.log(arr); // []
// 动态添加元素
arr[arr.length] = "new"; // 在末尾添加元素
console.log(arr); // ["new"]
数组检测
数组检测
let arr = [1, 2, 3];
let obj = {0: "a", 1: "b", length: 2};
// instanceof 操作符
console.log(arr instanceof Array); // true
console.log(obj instanceof Array); // false
// Array.isArray() (推荐)
console.log(Array.isArray(arr)); // true
console.log(Array.isArray(obj)); // false
// constructor 属性
console.log(arr.constructor === Array); // true
// Object.prototype.toString
console.log(Object.prototype.toString.call(arr)); // "[object Array]"
数组方法 - 添加/删除元素
这些方法会修改原数组,用于在数组开头或末尾添加/删除元素:
| 方法 | 描述 | 返回值 | 时间复杂度 | 是否修改原数组 |
|---|---|---|---|---|
push() |
在末尾添加元素 | 新长度 | O(1) | 是 |
pop() |
删除末尾元素 | 删除的元素 | O(1) | 是 |
unshift() |
在开头添加元素 | 新长度 | O(n) | 是 |
shift() |
删除开头元素 | 删除的元素 | O(n) | 是 |
splice() |
添加/删除/替换元素 | 删除的元素数组 | O(n) | 是 |
concat() |
合并数组 | 新数组 | O(n) | 否 |
添加/删除元素示例
let arr = ["a", "b", "c"];
// push() - 末尾添加
let newLength = arr.push("d", "e");
console.log(arr); // ["a", "b", "c", "d", "e"]
console.log(newLength); // 5
// pop() - 末尾删除
let lastElement = arr.pop();
console.log(arr); // ["a", "b", "c", "d"]
console.log(lastElement); // "e"
// unshift() - 开头添加
arr.unshift("z");
console.log(arr); // ["z", "a", "b", "c", "d"]
// shift() - 开头删除
let firstElement = arr.shift();
console.log(arr); // ["a", "b", "c", "d"]
console.log(firstElement); // "z"
// splice() - 多功能修改
let removed = arr.splice(1, 2, "x", "y"); // 从索引1开始删除2个元素,添加"x","y"
console.log(arr); // ["a", "x", "y", "d"]
console.log(removed); // ["b", "c"]
// concat() - 合并数组
let arr1 = [1, 2];
let arr2 = [3, 4];
let combined = arr1.concat(arr2, [5, 6]);
console.log(combined); // [1, 2, 3, 4, 5, 6]
性能提示:
push()和pop()操作很快 (O(1))unshift()和shift()操作较慢 (O(n))- 对于频繁在开头添加/删除的场景,考虑使用链表或其他数据结构
数组方法 - 搜索和判断
这些方法用于在数组中搜索元素或判断数组内容:
| 方法 | 描述 | 返回值 | 是否严格比较 |
|---|---|---|---|
indexOf() |
查找元素索引 | 索引或-1 | 是 (===) |
lastIndexOf() |
从后向前查找 | 索引或-1 | 是 (===) |
includes() |
检查是否包含元素 | 布尔值 | 是 (===) |
find() |
查找满足条件的元素 | 元素或undefined | 自定义 |
findIndex() |
查找满足条件的索引 | 索引或-1 | 自定义 |
findLast() |
从后向前查找满足条件的元素 | 元素或undefined | 自定义 |
findLastIndex() |
从后向前查找满足条件的索引 | 索引或-1 | 自定义 |
some() |
检查是否有元素满足条件 | 布尔值 | 自定义 |
every() |
检查所有元素是否满足条件 | 布尔值 | 自定义 |
搜索和判断示例
let numbers = [1, 2, 3, 4, 5, 2];
// indexOf() 和 lastIndexOf()
console.log(numbers.indexOf(2)); // 1
console.log(numbers.lastIndexOf(2)); // 5
console.log(numbers.indexOf(10)); // -1
// includes()
console.log(numbers.includes(3)); // true
console.log(numbers.includes(10)); // false
// find() 和 findIndex()
let firstEven = numbers.find(num => num % 2 === 0);
let firstEvenIndex = numbers.findIndex(num => num % 2 === 0);
console.log(firstEven); // 2
console.log(firstEvenIndex); // 1
// findLast() 和 findLastIndex() (ES2023)
let lastEven = numbers.findLast(num => num % 2 === 0);
let lastEvenIndex = numbers.findLastIndex(num => num % 2 === 0);
console.log(lastEven); // 2 (最后一个偶数)
console.log(lastEvenIndex); // 5
// some() 和 every()
let hasEven = numbers.some(num => num % 2 === 0);
let allEven = numbers.every(num => num % 2 === 0);
console.log(hasEven); // true
console.log(allEven); // false
// 对象数组搜索
let users = [
{ id: 1, name: "Alice", age: 25 },
{ id: 2, name: "Bob", age: 30 },
{ id: 3, name: "Charlie", age: 35 }
];
let user = users.find(u => u.age > 28);
console.log(user); // {id: 2, name: "Bob", age: 30}
let allAdults = users.every(u => u.age >= 18);
console.log(allAdults); // true
数组方法 - 转换和迭代
这些方法用于转换数组或对数组元素进行迭代操作:
| 方法 | 描述 | 返回值 | 是否修改原数组 |
|---|---|---|---|
map() |
对每个元素执行函数 | 新数组 | 否 |
filter() |
过滤满足条件的元素 | 新数组 | 否 |
reduce() |
从左到右累积计算 | 累积值 | 否 |
reduceRight() |
从右到左累积计算 | 累积值 | 否 |
forEach() |
遍历执行函数 | undefined | 否 |
sort() |
排序数组 | 排序后的数组 | 是 |
reverse() |
反转数组 | 反转后的数组 | 是 |
转换和迭代示例
let numbers = [1, 2, 3, 4, 5];
// map() - 转换每个元素
let doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
// filter() - 过滤元素
let evens = numbers.filter(num => num % 2 === 0);
console.log(evens); // [2, 4]
// reduce() - 累积计算
let sum = numbers.reduce((total, num) => total + num, 0);
let product = numbers.reduce((total, num) => total * num, 1);
console.log(sum); // 15
console.log(product); // 120
// reduceRight() - 从右向左累积
let subtraction = numbers.reduceRight((total, num) => total - num);
console.log(subtraction); // 5 - 4 - 3 - 2 - 1 = -5
// forEach() - 遍历执行
numbers.forEach((num, index) => {
console.log(`索引 ${index}: 值 ${num}`);
});
// sort() - 排序
let unsorted = [3, 1, 4, 2, 5];
let sorted = unsorted.sort((a, b) => a - b); // 数字排序
console.log(sorted); // [1, 2, 3, 4, 5]
// 字符串排序
let words = ["banana", "apple", "cherry"];
let sortedWords = words.sort();
console.log(sortedWords); // ["apple", "banana", "cherry"]
// reverse() - 反转
let reversed = numbers.reverse();
console.log(reversed); // [5, 4, 3, 2, 1]
reduce() 高级用法
reduce() 高级用法
let data = [
{ category: "fruit", name: "apple", price: 1.2 },
{ category: "fruit", name: "banana", price: 0.8 },
{ category: "vegetable", name: "carrot", price: 0.5 },
{ category: "fruit", name: "orange", price: 1.5 }
];
// 按类别分组
let grouped = data.reduce((acc, item) => {
if (!acc[item.category]) {
acc[item.category] = [];
}
acc[item.category].push(item);
return acc;
}, {});
console.log(grouped);
// {
// fruit: [
// {category: "fruit", name: "apple", price: 1.2},
// {category: "fruit", name: "banana", price: 0.8},
// {category: "fruit", name: "orange", price: 1.5}
// ],
// vegetable: [{category: "vegetable", name: "carrot", price: 0.5}]
// }
// 计算每个类别的总价
let categoryTotals = data.reduce((acc, item) => {
acc[item.category] = (acc[item.category] || 0) + item.price;
return acc;
}, {});
console.log(categoryTotals); // {fruit: 3.5, vegetable: 0.5}
// 找出最贵的商品
let mostExpensive = data.reduce((max, item) =>
item.price > max.price ? item : max, data[0]);
console.log(mostExpensive); // {category: "fruit", name: "orange", price: 1.5}
数组方法 - 其他实用方法
这些方法提供数组操作的其他常用功能:
其他实用方法
let arr = [1, 2, 3, 4, 5];
// slice() - 提取子数组
let slice1 = arr.slice(1, 3); // [2, 3] (索引1到3,不包括3)
let slice2 = arr.slice(2); // [3, 4, 5] (从索引2到末尾)
let slice3 = arr.slice(-2); // [4, 5] (最后2个元素)
let slice4 = arr.slice(1, -1); // [2, 3, 4] (从索引1到倒数第1个)
// join() - 数组转字符串
let str1 = arr.join(); // "1,2,3,4,5"
let str2 = arr.join(" - "); // "1 - 2 - 3 - 4 - 5"
let str3 = arr.join(""); // "12345"
// toString() - 数组转字符串
let str4 = arr.toString(); // "1,2,3,4,5"
// isArray() - 检查是否为数组
console.log(Array.isArray(arr)); // true
console.log(Array.isArray({})); // false
// fill() - 填充数组
let filled = new Array(3).fill(0);
console.log(filled); // [0, 0, 0]
// flat() - 扁平化数组 (ES2019)
let nested = [1, [2, [3, [4]]]];
console.log(nested.flat()); // [1, 2, [3, [4]]]
console.log(nested.flat(2)); // [1, 2, 3, [4]]
console.log(nested.flat(Infinity)); // [1, 2, 3, 4]
// flatMap() - 映射后扁平化
let phrases = ["hello world", "the quick brown fox"];
let words = phrases.flatMap(phrase => phrase.split(" "));
console.log(words); // ["hello", "world", "the", "quick", "brown", "fox"]
// copyWithin() - 复制数组元素
let copyArr = [1, 2, 3, 4, 5];
copyArr.copyWithin(0, 3); // 将索引3开始的元素复制到索引0
console.log(copyArr); // [4, 5, 3, 4, 5]
数组遍历
有多种方式可以遍历数组,每种方式都有其适用场景:
数组遍历
let fruits = ["苹果", "香蕉", "橙子"];
// for 循环
for (let i = 0; i < fruits.length; i++) {
console.log(fruits[i]);
}
// for...of 循环 (推荐)
for (let fruit of fruits) {
console.log(fruit);
}
// forEach() 方法
fruits.forEach(function(fruit, index) {
console.log(`${index}: ${fruit}`);
});
// entries() 方法
for (let [index, fruit] of fruits.entries()) {
console.log(`${index}: ${fruit}`);
}
// keys() 方法
for (let index of fruits.keys()) {
console.log(index, fruits[index]);
}
// values() 方法
for (let fruit of fruits.values()) {
console.log(fruit);
}
// for...in 循环 (不推荐用于数组)
// 会遍历所有可枚举属性,包括原型链上的
for (let index in fruits) {
console.log(fruits[index]);
}
遍历方法比较
| 方法 | 索引访问 | 值访问 | 可中断 | 性能 | 适用场景 |
|---|---|---|---|---|---|
for循环 |
是 | 是 | 是 | 最快 | 需要索引、性能要求高 |
for...of |
否 | 是 | 是 | 快 | 简单遍历、可读性要求高 |
forEach() |
是 | 是 | 否 | 中等 | 函数式编程、不需要中断 |
for...in |
是 | 是 | 是 | 慢 | 不推荐用于数组 |
数组排序和搜索算法
了解数组排序和搜索的原理对于处理大量数据很重要:
自定义排序
自定义排序
let users = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
{ name: "Charlie", age: 35 }
];
// 按年龄升序排序
users.sort((a, b) => a.age - b.age);
console.log(users);
// [{name: "Bob", age: 25}, {name: "Alice", age: 30}, {name: "Charlie", age: 35}]
// 按姓名排序
users.sort((a, b) => a.name.localeCompare(b.name));
console.log(users);
// [{name: "Alice", age: 30}, {name: "Bob", age: 25}, {name: "Charlie", age: 35}]
// 多条件排序
let products = [
{ name: "apple", category: "fruit", price: 1.2 },
{ name: "banana", category: "fruit", price: 0.8 },
{ name: "carrot", category: "vegetable", price: 0.5 },
{ name: "orange", category: "fruit", price: 1.5 }
];
// 先按类别,再按价格排序
products.sort((a, b) => {
if (a.category !== b.category) {
return a.category.localeCompare(b.category);
}
return a.price - b.price;
});
console.log(products);
// 先水果后蔬菜,水果内按价格排序
二分搜索
二分搜索
// 二分搜索实现
function binarySearch(arr, target) {
let left = 0;
let right = arr.length - 1;
while (left <= right) {
let mid = Math.floor((left + right) / 2);
if (arr[mid] === target) {
return mid; // 找到目标,返回索引
} else if (arr[mid] < target) {
left = mid + 1; // 目标在右侧
} else {
right = mid - 1; // 目标在左侧
}
}
return -1; // 未找到
}
let sortedNumbers = [1, 3, 5, 7, 9, 11, 13];
console.log(binarySearch(sortedNumbers, 7)); // 3
console.log(binarySearch(sortedNumbers, 10)); // -1
数组性能优化
了解数组性能优化技巧对于编写高效JavaScript代码很重要:
性能优化技巧
- 预分配数组大小 - 对于已知大小的数组,预先设置长度
- 避免在循环中修改数组长度 - 这会触发多次内存重新分配
- 使用TypedArray处理数值数据 - 对于纯数值数组,性能更好
- 避免使用delete删除元素 - 这会创建稀疏数组,影响性能
- 批量操作 - 使用splice()批量添加/删除元素
性能优化示例
// 预分配数组大小
function createArray(size) {
// 不好的方式 - 动态增长
let arr1 = [];
for (let i = 0; i < size; i++) {
arr1[i] = i; // 每次添加都可能触发内存重新分配
}
// 好的方式 - 预分配
let arr2 = new Array(size);
for (let i = 0; i < size; i++) {
arr2[i] = i; // 不会触发内存重新分配
}
return arr2;
}
// 使用TypedArray处理数值数据
let normalArray = [1, 2, 3, 4, 5]; // 普通数组
let typedArray = new Int32Array([1, 2, 3, 4, 5]); // 类型化数组
// 批量操作 vs 单个操作
let arr = [1, 2, 3, 4, 5];
// 不好的方式 - 多次push
arr.push(6);
arr.push(7);
arr.push(8);
// 好的方式 - 一次push多个元素
arr.push(6, 7, 8);
// 或者使用concat (不修改原数组)
arr = arr.concat([6, 7, 8]);
内存管理
内存管理
// 避免内存泄漏
function processLargeData() {
let data = new Array(1000000).fill("data");
// 处理数据...
// 处理完成后清除引用
data = null;
}
// 使用WeakMap处理大数组缓存
let cache = new WeakMap();
function getExpensiveData(key) {
if (cache.has(key)) {
return cache.get(key);
}
// 计算昂贵的数据
let data = /* 昂贵的计算 */ [];
cache.set(key, data);
return data;
}
实践练习
练习代码
// 练习1: 数组去重
function removeDuplicates(arr) {
// 方法1: 使用Set (最简单)
return [...new Set(arr)];
// 方法2: 使用filter
// return arr.filter((item, index) => arr.indexOf(item) === index);
// 方法3: 使用reduce
// return arr.reduce((unique, item) =>
// unique.includes(item) ? unique : [...unique, item], []);
}
let duplicates = [1, 2, 2, 3, 4, 4, 5];
console.log(removeDuplicates(duplicates)); // [1, 2, 3, 4, 5]
// 练习2: 查找数组中的最大值和最小值
function findMinMax(arr) {
// 方法1: 使用Math.min/max和扩展运算符
return {
min: Math.min(...arr),
max: Math.max(...arr)
};
// 方法2: 使用reduce
// return arr.reduce((acc, val) => ({
// min: Math.min(acc.min, val),
// max: Math.max(acc.max, val)
// }), {min: Infinity, max: -Infinity});
}
let numbers = [3, 7, 2, 9, 1, 5];
console.log(findMinMax(numbers)); // {min: 1, max: 9}
// 练习3: 数组分组
function groupBy(arr, key) {
return arr.reduce((groups, item) => {
let groupKey = item[key];
if (!groups[groupKey]) {
groups[groupKey] = [];
}
groups[groupKey].push(item);
return groups;
}, {});
}
let people = [
{ name: "张三", age: 25 },
{ name: "李四", age: 30 },
{ name: "王五", age: 25 }
];
console.log(groupBy(people, "age"));
// {25: [{name: "张三", age: 25}, {name: "王五", age: 25}], 30: [{name: "李四", age: 30}]}
// 练习4: 数组分块
function chunk(arr, size) {
const chunks = [];
for (let i = 0; i < arr.length; i += size) {
chunks.push(arr.slice(i, i + size));
}
return chunks;
}
let bigArray = [1, 2, 3, 4, 5, 6, 7, 8];
console.log(chunk(bigArray, 3)); // [[1, 2, 3], [4, 5, 6], [7, 8]]
// 练习5: 数组扁平化
function flatten(arr) {
// 方法1: 使用flat(Infinity)
// return arr.flat(Infinity);
// 方法2: 使用递归
return arr.reduce((flat, item) =>
flat.concat(Array.isArray(item) ? flatten(item) : item), []);
}
let nestedArray = [1, [2, [3, [4]], 5]];
console.log(flatten(nestedArray)); // [1, 2, 3, 4, 5]
最佳实践
- 使用
for...of或数组方法而不是for...in遍历数组 - 优先使用数组方法(
map,filter,reduce)而不是循环 - 使用
Array.isArray()检查数组类型 - 使用扩展运算符(
...)进行数组浅拷贝 - 使用
const声明不会重新赋值的数组 - 避免修改原数组的方法,除非确实需要
- 使用解构赋值交换数组元素或提取值
- 对于大型数组,考虑性能优化技巧
- 使用适当的数组方法而不是重新发明轮子
- 在适当的时候使用类型化数组处理数值数据
函数式编程实践
- 优先使用纯函数和不可变操作
- 使用
map、filter、reduce组合数据转换 - 避免在数组方法中使用副作用
- 使用函数组合创建可复用的数据转换管道