在JavaScript开发中,数组是最常用的数据结构之一。理解哪些方法会改变原数组,哪些不会,是每个开发者必须掌握的基础知识。这直接关系到代码的可预测性和数据安全性。
数组方法的这种区分源于JavaScript的设计哲学。改变原数组的方法通常用于需要直接修改数据集合的场景,而不改变原数组的方法则更符合函数式编程的理念,强调数据的不可变性。
在实际开发中,这种区分至关重要:
这类方法我们称之为"变更方法"(Mutator Methods),它们会直接修改调用它们的数组。
push()方法在数组末尾添加一个或多个元素,并返回数组的新长度:
javascript复制const fruits = ['apple', 'banana'];
const newLength = fruits.push('orange');
// fruits现在是['apple', 'banana', 'orange']
// newLength值为3
pop()则相反,它移除并返回数组的最后一个元素:
javascript复制const last = fruits.pop();
// last是'orange'
// fruits现在是['apple', 'banana']
提示:push/pop操作的是数组尾部,因此时间复杂度是O(1),非常高效。
unshift()在数组开头添加元素:
javascript复制const newLength = fruits.unshift('strawberry');
// fruits现在是['strawberry', 'apple', 'banana']
shift()移除并返回第一个元素:
javascript复制const first = fruits.shift();
// first是'strawberry'
// fruits现在是['apple', 'banana']
注意:unshift/shift操作数组头部,需要移动所有后续元素,时间复杂度是O(n),在大数组上性能较差。
splice()是最强大的数组修改方法,可以删除、替换或插入元素:
javascript复制const months = ['Jan', 'March', 'April', 'June'];
months.splice(1, 0, 'Feb');
// 在索引1处插入'Feb'
// months现在是['Jan', 'Feb', 'March', 'April', 'June']
months.splice(4, 1, 'May');
// 替换索引4处的元素
// months现在是['Jan', 'Feb', 'March', 'April', 'May']
splice参数解析:
sort()对数组元素进行排序,默认按Unicode码点排序:
javascript复制const numbers = [1, 10, 2, 21];
numbers.sort();
// 结果是[1, 10, 2, 21](按字符串比较)
要正确排序数字,需要提供比较函数:
javascript复制numbers.sort((a, b) => a - b);
// 现在是[1, 2, 10, 21]
重要:sort()是原地排序,会改变原数组。如果需要保留原数组,应先创建副本:
javascript复制const sorted = [...numbers].sort();
reverse()简单地反转数组顺序:
javascript复制const arr = [1, 2, 3];
arr.reverse();
// arr现在是[3, 2, 1]
fill()用固定值填充数组:
javascript复制const arr = new Array(3).fill(0);
// arr是[0, 0, 0]
const arr2 = [1, 2, 3, 4];
arr2.fill(0, 1, 3);
// 从索引1到3(不包括3)填充0
// arr2现在是[1, 0, 0, 4]
copyWithin()在数组内部复制元素序列:
javascript复制const arr = [1, 2, 3, 4, 5];
arr.copyWithin(0, 3, 5);
// 将索引3到5(不包括5)的元素复制到索引0开始的位置
// arr现在是[4, 5, 3, 4, 5]
这类方法称为"访问方法"(Accessor Methods),它们返回新数组或某个值,而不修改原数组。
concat()合并数组,返回新数组:
javascript复制const arr1 = [1, 2];
const arr2 = [3, 4];
const combined = arr1.concat(arr2);
// combined是[1, 2, 3, 4]
// arr1和arr2保持不变
concat可以接受多个参数,包括值或其他数组:
javascript复制const newArr = [1].concat(2, [3, 4], [[5]]);
// newArr是[1, 2, 3, 4, [5]]
slice()返回数组的浅拷贝部分:
javascript复制const animals = ['ant', 'bison', 'camel', 'duck', 'elephant'];
animals.slice(2);
// 从索引2开始到结束:['camel', 'duck', 'elephant']
animals.slice(2, 4);
// 从索引2到4(不包括4):['camel', 'duck']
animals.slice(-2);
// 最后两个元素:['duck', 'elephant']
技巧:slice()不带参数可以创建数组的浅拷贝:
javascript复制const copy = original.slice();
join()将数组元素连接成字符串:
javascript复制const elements = ['Fire', 'Air', 'Water'];
elements.join();
// 默认用逗号分隔:"Fire,Air,Water"
elements.join('');
// 空分隔符:"FireAirWater"
elements.join('-');
// 自定义分隔符:"Fire-Air-Water"
toString()是join()的简化版,固定使用逗号分隔:
javascript复制const arr = [1, 2, 'a', '1a'];
arr.toString();
// "1,2,a,1a"
map()对每个元素执行函数,返回新数组:
javascript复制const numbers = [1, 4, 9];
const roots = numbers.map(num => Math.sqrt(num));
// roots是[1, 2, 3]
// numbers保持不变
filter()返回通过测试的元素组成的新数组:
javascript复制const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction'];
const result = words.filter(word => word.length > 6);
// result是["exuberant", "destruction"]
reduce()对数组执行reducer函数,返回单个值:
javascript复制const array1 = [1, 2, 3, 4];
const sum = array1.reduce(
(accumulator, currentValue) => accumulator + currentValue,
0
);
// sum是10
find()返回第一个满足条件的元素:
javascript复制const array1 = [5, 12, 8, 130, 44];
const found = array1.find(element => element > 10);
// found是12
indexOf()返回元素的第一个索引:
javascript复制const beasts = ['ant', 'bison', 'camel', 'duck', 'bison'];
beasts.indexOf('bison');
// 1
beasts.indexOf('bison', 2);
// 从索引2开始查找:4
beasts.indexOf('giraffe');
// -1(未找到)
虽然split()是字符串方法而非数组方法,但因其与数组转换相关,值得特别讨论。
split()将字符串分割为字符串数组:
javascript复制const str = 'The quick brown fox jumps over the lazy dog.';
const words = str.split(' ');
// words是["The", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog."]
当遇到连续分隔符时,split()会产生空字符串元素:
javascript复制const myString = 'Hello1World2';
const splits = myString.split(/\d/);
// splits是["Hello", "World", ""]
可以使用filter()移除空字符串:
javascript复制const splits = myString.split(/\d/).filter(s => s !== '');
// ["Hello", "World"]
split()支持正则表达式作为分隔符:
javascript复制const names = 'Harry Trump ;Fred Barney; Helen Rigby ; Bill Abel ;Chris Hand';
const re = /\s*;\s*/;
const nameList = names.split(re);
// ["Harry Trump", "Fred Barney", "Helen Rigby", "Bill Abel", "Chris Hand"]
split()的第二个参数限制返回数组的最大长度:
javascript复制const myString = 'Hello World. How are you doing?';
const splits = myString.split(' ', 3);
// ["Hello", "World.", "How"]
变更方法通常性能更好,因为它们不需要创建新数组。但在现代JavaScript引擎中,这种差异通常可以忽略,除非处理非常大的数组。
slice()和concat()只能创建浅拷贝。对于深拷贝:
javascript复制// 简单对象的深拷贝
const deepCopy = JSON.parse(JSON.stringify(originalArray));
// 复杂场景使用专用库如lodash的cloneDeep
import { cloneDeep } from 'lodash';
const deepCopy = cloneDeep(originalArray);
经验法则:
javascript复制// 安全的方式 - 全部使用非变更方法
const result = arr
.filter(x => x > 2)
.map(x => x * 2)
.reduce((sum, x) => sum + x, 0);
// 危险的方式 - 混合使用变更和非变更方法
const result = arr
.sort() // 改变了arr!
.map(x => x * 2);
javascript复制// 错误 - 直接修改状态
this.state.items.push(newItem);
this.setState({ items: this.state.items });
// 正确 - 使用非变更方法
this.setState(prevState => ({
items: [...prevState.items, newItem]
}));
现代JavaScript新增了一些有用的数组方法,了解它们是否改变原数组也很重要。
这两个方法返回新数组:
javascript复制const arr1 = [1, 2, [3, 4]];
arr1.flat();
// [1, 2, 3, 4]
const arr2 = [1, 2, 3];
arr2.flatMap(x => [x * 2]);
// [2, 4, 6]
检查数组是否包含某元素:
javascript复制const pets = ['cat', 'dog', 'bat'];
pets.includes('cat');
// true
ES2023新增的查找方法:
javascript复制const array = [5, 12, 8, 130, 44];
array.findIndex(element => element > 13);
// 3
// ES2023新增
array.findLast(element => element > 45);
// 130
array.findLastIndex(element => element > 45);
// 3
对于Int8Array等类型化数组,大多数方法的行为与普通数组一致,但有一些例外:
javascript复制const typedArray = new Int8Array([3, 1, 2]);
typedArray.sort();
// Int8Array [1, 2, 3]
在实际项目中,我经常看到开发者因为混淆了数组方法的特性而引入bug。特别是在React等框架中,直接修改状态数组会导致组件不更新,因为React的浅比较无法检测到数组的变化。因此,我总是建议团队成员在处理数组时,先明确是否需要保留原数组,再选择合适的方法。