JavaScript 数组实战:筛选、转换、排序与不可变更新
本文目录
原始发布日期保留为 2018-02-10。本文于 2026-10-19 重整,补全现代 JavaScript 用法、不可变写法和常见边界。
先判断:会不会修改原数组
| 需求 | 会修改原数组 | 返回新数组或结果 |
|---|---|---|
| 末尾增删 | push()、pop() | toSpliced() 可复制后修改 |
| 开头增删 | unshift()、shift() | toSpliced() 可复制后修改 |
| 任意位置增删 | splice() | toSpliced() |
| 排序、反转 | sort()、reverse() | toSorted()、toReversed() |
| 按索引覆盖 | 赋值、fill()、copyWithin() | with(index, value) |
| 转换、筛选 | 不修改(但回调可产生副作用) | map()、filter()、flatMap() |
| 截取 | 不修改 | slice()、concat() |
在 React、Vue 等状态管理场景中,优先使用返回新数组的方法,避免原地修改后界面无法可靠感知变化。需要原地更新时,应明确这是有意的副作用。
添加与删除
push()、pop()、unshift()、shift()
这四个方法都会修改原数组:
- push(...items) 将元素加到末尾,返回修改后的数组长度。
- pop() 移除并返回末尾元素;空数组返回 undefined。
- unshift(...items) 将元素加到开头,返回修改后的数组长度。
- shift() 移除并返回开头元素;空数组返回 undefined。
const queue = ['b', 'c'];
const length = queue.unshift('a'); // 3
const first = queue.shift(); // 'a'
const last = queue.pop(); // 'c'
console.log(queue); // ['b']
旧浏览器兼容性笔记不应当作现代 JavaScript 行为依据。若目标包含旧版内嵌浏览器或 WebView,应按实际支持范围验证。
splice() 与 toSpliced()
splice(start, deleteCount, ...items) 从指定位置删除元素并可插入新元素,会修改原数组;返回被删除元素组成的数组。start 可以是负数,表示从末尾倒数。省略 deleteCount 时会删除从 start 到末尾的所有元素。
const source = ['a', 'b', 'c', 'd'];
const removed = source.splice(1, 2, 'x', 'y');
console.log(source); // ['a', 'x', 'y', 'd']
console.log(removed); // ['b', 'c']
toSpliced() 是复制版本,不改变原数组:
const source = ['a', 'b', 'c'];
const next = source.toSpliced(1, 1, 'x');
console.log(source); // ['a', 'b', 'c']
console.log(next); // ['a', 'x', 'c']
复制与截取
- slice(start, end) 返回从 start 到 end 前一位的浅拷贝,不修改原数组。
- concat(...values) 合并并返回新数组,不修改原数组。
- 展开语法创建浅拷贝,也可用于合并数组。
const numbers = [1, 2, 3, 4];
console.log(numbers.slice(1, 3)); // [2, 3]
console.log(numbers.concat([5, 6])); // [1, 2, 3, 4, 5, 6]
console.log([...numbers, 5, 6]); // [1, 2, 3, 4, 5, 6]
console.log(numbers); // [1, 2, 3, 4]
浅拷贝只复制数组这一层,嵌套对象仍指向同一个对象:
const original = [{ name: 'Ada' }];
const copy = [...original];
copy[0].name = 'Grace';
console.log(original[0].name); // 'Grace'
需要不可变地修改对象数组中的一项,通常要同时复制数组和目标对象:
const updated = original.map((item, index) =>
index === 0 ? { ...item, name: 'Lin' } : item,
);
映射、筛选与遍历
map()、filter()、flatMap()
- map(callback) 为每个被访问的元素计算新值,返回新数组;适合一对一转换。
- filter(callback) 返回所有通过条件的元素;适合筛选。
- flatMap(callback) 先映射,再把每个返回数组展开一层;适合一项变成零项、一项或多项的转换。
const prices = [10, 20, 30];
const withTax = prices.map((price) => price * 1.13);
const expensive = prices.filter((price) => price >= 20);
const labels = prices.flatMap((price) =>
price >= 20 ? [price, String(price) + ' 元'] : [],
);
console.log(withTax); // [11.3, 22.6, 33.9]
console.log(expensive); // [20, 30]
console.log(labels); // [20, '20 元', 30, '30 元']
console.log(prices); // [10, 20, 30]
不要用 map() 做没有返回值的副作用遍历;这种情况用 forEach() 或循环更清晰。
forEach()
forEach(callback) 对数组元素执行回调,返回 undefined。回调可接收 element、index、array。它不能用 break 提前退出,也不会等待异步回调完成。
const numbers = [1, 2, 3];
let total = 0;
numbers.forEach((number) => {
total += number;
});
console.log(total); // 6
以下写法不会等待每个异步任务结束:
await numbers.forEach(async (number) => {
await saveNumber(number);
});
需要按顺序等待时使用 for...of;允许并行时使用 Promise.all():
for (const number of numbers) {
await saveNumber(number);
}
await Promise.all(numbers.map((number) => saveNumber(number)));
查找与条件判断
| 方法 | 用途 | 未找到时 |
|---|---|---|
| find() | 返回第一个满足条件的元素 | undefined |
| findIndex() | 返回第一个满足条件的索引 | -1 |
| findLast() / findLastIndex() | 从末尾开始查找 | undefined / -1 |
| some() | 是否至少一个元素满足条件 | false |
| every() | 是否所有元素都满足条件 | 空数组返回 true |
| includes(value) | 是否包含一个值 | false |
| indexOf(value) / lastIndexOf(value) | 返回值对应的索引 | -1 |
includes() 使用 SameValueZero 比较,可找到 NaN;indexOf() 使用严格相等比较,NaN 不等于自身。
const values = [1, NaN, 3, 3];
console.log(values.includes(NaN)); // true
console.log(values.indexOf(NaN)); // -1
console.log(values.find((value) => value > 2)); // 3
console.log(values.findLast((value) => value === 3)); // 3
console.log(values.some((value) => value < 0)); // false
console.log(values.every(Number.isFinite)); // false
搜索对象时通常用 find():
const users = [
{ id: 'u1', name: 'Ada' },
{ id: 'u2', name: 'Lin' },
];
const user = users.find((item) => item.id === 'u2');
console.log(user); // { id: 'u2', name: 'Lin' }
汇总与归约
reduce(callback, initialValue) 将数组累积为一个值。回调参数依次是 accumulator、currentValue、index、array。总是考虑提供初始值:没有初始值时,空数组会抛出 TypeError;非空数组会把首个元素作为初始累加值。
const orders = [
{ amount: 12 },
{ amount: 8 },
{ amount: 15 },
];
const total = orders.reduce((sum, order) => sum + order.amount, 0);
console.log(total); // 35
reduce() 很灵活,但不应为了少写几行把筛选、查找或简单映射塞进一个回调。能用 map()、filter()、find() 或 some() 清楚表达时,优先选择语义更直接的方法。
排序与反转
sort() 会修改原数组
不传比较函数时,元素会先转成字符串,再按 UTF-16 码元顺序比较。因此数字数组不应省略比较函数:
const values = [1, 10, 2];
console.log([...values].sort()); // [1, 10, 2],字符串顺序
console.log([...values].sort((a, b) => a - b)); // [1, 2, 10]
console.log(values); // [1, 10, 2]
降序可交换减法顺序;字符串排序可用 localeCompare()。需要固定语种规则或大量比较时,可复用 Intl.Collator:
const scores = [3, 10, 2];
const descending = scores.toSorted((a, b) => b - a);
const names = ['张三', '李四', '王五'];
const collator = new Intl.Collator('zh-CN');
const sortedNames = names.toSorted((a, b) => collator.compare(a, b));
reverse() 与非变更版本
reverse() 会原地反转并返回同一个数组。要保留原数组,用 toReversed();较旧运行环境可先浅拷贝再调用 reverse()。
const original = [1, 2, 3];
console.log(original.toReversed()); // [3, 2, 1]
console.log(original); // [1, 2, 3]
现代非变更方法
较新的 ECMAScript 提供一组返回新数组、不修改接收数组的方法:
| 原地修改方法 | 非变更版本 | 行为 |
|---|---|---|
| sort() | toSorted(compareFn) | 排序 |
| reverse() | toReversed() | 反转 |
| splice() | toSpliced(start, deleteCount, ...items) | 删除和插入 |
| arr[index] = value | with(index, value) | 替换指定索引 |
const values = [3, 1, 2];
const sorted = values.toSorted((a, b) => a - b);
const reversed = values.toReversed();
const changed = values.with(1, 9);
console.log(values); // [3, 1, 2]
console.log(sorted); // [1, 2, 3]
console.log(reversed); // [2, 1, 3]
console.log(changed); // [3, 9, 2]
这些方法属于较新的运行时能力。发布到旧版浏览器、内嵌 WebView 或旧 Node.js 前,应核对项目支持范围;需要兼容时使用合适的 polyfill,或复制后再操作。
空槽、undefined 与 delete
delete array[index] 不会收缩数组长度,而是留下空槽;通常要删除元素应使用 splice() 或 toSpliced()。
const items = ['a', 'b', 'c'];
delete items[1];
console.log(items.length); // 3
console.log(1 in items); // false,索引处是空槽
console.log(items[1]); // undefined
空槽和显式的 undefined 不是同一回事。大多数业务数据应避免稀疏数组。不同方法对空槽的处理也不同:map()、filter()、forEach() 会跳过空槽;find()、includes() 会把空槽按 undefined 处理。
const sparse = ['a', , 'c'];
console.log(sparse.map((value) => value ?? '?')); // 索引 1 仍是空槽
console.log(sparse.includes(undefined)); // true
console.log(1 in sparse); // false
数组与对象查找
按数组元素值搜索,使用 includes()、indexOf();按对象属性找一项,使用 find();判断普通对象是否自身拥有一个属性,使用 Object.hasOwn()。
const settings = Object.create({ inherited: true });
settings.theme = 'dark';
console.log(Object.hasOwn(settings, 'theme')); // true
console.log(Object.hasOwn(settings, 'inherited')); // false
如果要用任意字符串或对象作为键并频繁查询,考虑 Map。字典场景也应注意原型属性与键冲突;对外部输入做键值查找时,优先使用 Map 或 Object.create(null),并根据业务校验键和值。
常见小模式
去重
Set 保留首次插入顺序;对象去重按引用身份,不会自动按对象字段去重。
const values = [1, 2, 2, NaN, NaN];
console.log([...new Set(values)]); // [1, 2, NaN]
从右侧取最后一个元素
at() 支持负索引,-1 表示末尾元素:
const items = ['first', 'middle', 'last'];
console.log(items.at(-1)); // 'last'
console.log(items.at(-2)); // 'middle'
快速选择
- 想得到转换后的新数组:map()。
- 想留下满足条件的元素:filter()。
- 想取一个匹配项:find();想取末尾的匹配项:findLast()。
- 只想知道是否匹配:some() / every()。
- 想改原数组:明确使用 push()、splice()、sort() 等变更方法。
- 想保留原数组并排序、反转、删除或替换:使用 toSorted()、toReversed()、toSpliced()、with()。
- 想汇总:reduce();如果逻辑不直观,拆成更清楚的步骤。
- 想按值去重:new Set(array);对象需要按字段去重时,明确选择唯一键。
参考资料
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。