JavaScript数组

掌握JavaScript数组的创建、操作、遍历和常用方法

数组概述

数组是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声明不会重新赋值的数组
  • 避免修改原数组的方法,除非确实需要
  • 使用解构赋值交换数组元素或提取值
  • 对于大型数组,考虑性能优化技巧
  • 使用适当的数组方法而不是重新发明轮子
  • 在适当的时候使用类型化数组处理数值数据

函数式编程实践

  • 优先使用纯函数和不可变操作
  • 使用mapfilterreduce组合数据转换
  • 避免在数组方法中使用副作用
  • 使用函数组合创建可复用的数据转换管道

下一步学习

掌握了数组后,接下来可以学习: