购物清单里有十几件商品,成绩表里有几十个分数,搜索结果里有一批结构相同的记录。若给每一项都声明一个变量,代码很快就会变得难以维护。数组解决的正是这类问题:它把一组有顺序的值放进同一个容器,再让我们按位置读取、批量处理和重新排列。
真正掌握数组,不只是会写一对方括号。你还要知道 length 为什么不等于最后一个索引,哪些方法会改动原数组,怎样在 map、filter、find 和 reduce 之间做选择,以及空槽为何不等同于显式的 undefined。
本章从一份购物清单出发,逐步搭建一条完整的数据处理流水线。每个知识节都配有可以立即作答的小测,最后再用一个购物车项目把创建、增删改查、遍历、汇总与排序串在一起。
文中的控制台示例可直接粘贴到浏览器开发者工具或 Node.js 中运行。除特别说明外,两种环境的结果一致;控制台对数组的展示样式可能略有不同,但数组中的值不会因此改变。
完成本章后,你应该能够:
length 和 at() 安全读取或更新元素;push、pop、shift、unshift、splice 完成增删改;includes、indexOf、find 或 findIndex;forEach、some 和 every 检查一组数据;map、filter 和 reduce 建立清晰的数据处理流程;最常见、也最清楚的创建方式是数组字面量:在方括号里依次写入元素,并用逗号分隔。
const shoppingList = ["牛奶", "面包", "苹果"];
const scores = [72, 88, 95, 61];
const mixed = ["你好", 5, true];
const emptyList = [];
console.log(shoppingList);
console.log(emptyList.length);
console.log(typeof shoppingList);
console.log(Array.isArray(shoppingList));输出:
[ '牛奶', '面包', '苹果' ]
0
object
true数组中的每个值叫作元素。JavaScript 不要求所有元素具有相同类型,因此 mixed 是合法的。不过在真实项目里,同一个数组通常保存含义一致的数据,例如“全部是分数”或“全部是商品记录”,后续处理会更容易理解。
数组属于一种特殊对象,所以 typeof shoppingList 得到 "object"。要可靠判断一个值是不是数组,应使用 Array.isArray(value)。
Array 构造函数也能创建数组,但只传入一个非负整数时,它表达的是“数组长度”,不是“唯一元素的值”。
const slots = new Array(3);
const oneNumber = [3];
console.log(slots.length);
console.log(Object.keys(slots));
console.log(oneNumber);输出:
3
[]
[ 3 ]new Array(3) 创建了三个空槽,所以长度是 3,但并没有索引键;[3] 则包含一个真实元素 3。若只是列出已有数据,使用方括号不会产生这种歧义。
需要生成一组初始值时,可以使用 fill 或 Array.from:
const zeros = Array(3).fill(0);
const positions = Array.from({ length: 4 }, (_, index) => index);
console.log(zeros);
console.log(positions);输出:
[ 0, 0, 0 ]
[ 0, 1, 2, 3 ]fill 会修改调用它的数组;这里调用的是刚创建的新数组,因此不会影响其他数据。若初始元素需要彼此独立,尤其是对象或内层数组,应使用 Array.from 的映射函数逐项创建。
const 限制的是重新绑定,不是数组内部用 const 声明数组后,仍然可以更新它的元素,也可以调用会修改数组的方法;不能做的是让变量名指向另一个数组。
const words = ["旧值"];
words[0] = "新值";
words.push("第二项");
console.log(words);
// words = ["另一个数组"]; // TypeError: Assignment to constant variable.输出:
[ '新值', '第二项' ]业务代码中的数组通常可以先用 const 声明。这样既允许你修改数组内容,又能防止变量被意外改指向另一个数组。
数组是一组有顺序的数据。JavaScript 自动给每个元素分配索引,而且索引从 0 开始:第一项的索引是 0,第二项的索引是 1。

长度表示元素数量;最后一个有效索引通常比长度小 1。
const colors = ["黑色", "橙色", "粉色"];
console.log(colors[0]);
console.log(colors[1]);
console.log(colors.length);
console.log(colors[colors.length - 1]);
console.log(colors.at(-1));输出:
黑色
橙色
3
粉色
粉色colors.length 是 3,最后一个索引因此是 2。at(-1) 是读取倒数第一项的现代写法;at(-2) 则读取倒数第二项。对空数组来说,这两种最后一项写法都会得到 undefined。
使用赋值运算可以覆盖某个已有位置:
const cars = ["Toyota", "Renault", "Volkswagen"];
cars[0] = "Tesla";
console.log(cars);
console.log(cars[9]);输出:
[ 'Tesla', 'Renault', 'Volkswagen' ]
undefined读取不存在的索引不会抛错,而是得到 undefined。这并不代表访问一定正确;如果本应存在的数据意外变成 undefined,仍应检查索引计算是否有误。
普通方括号不会把负数理解成倒数索引。cars[-1] 实际是在数组对象上读取名为 "-1" 的普通属性:
cars[-1] = "Fiat";
console.log(cars[-1]);
console.log(cars.length);
console.log(cars.at(-1));输出:
Fiat
3
Volkswagen写入 -1 没有增加数组长度,也没有改变真正的最后一项。倒数读取要用 at(-1) 或 array[array.length - 1]。
length 可以写入,但要谨慎把 length 调小会永久删除尾部元素;再调大只会创建空槽,不会恢复已删除的数据。
const letters = ["A", "B", "C", "D"];
letters.length = 2;
console.log(letters);
letters.length = 5;
console.log(letters);
console.log(Object.keys(letters));输出:
[ 'A', 'B' ]
[ 'A', 'B', <3 empty items> ]
[ '0', '1' ]日常增删数据更适合使用数组方法。直接修改 length 往往会隐藏“删除了什么”或制造空槽,只有明确需要截断数组时才值得使用。
数组经常需要在头部、尾部或中间发生变化。先记住每个方法操作的位置和返回值,许多代码就不必靠猜。

const queue = ["小周", "小陈"];
const newLength = queue.push("小许");
const first = queue.shift();
queue.unshift("小林");
const last = queue.pop();
console.log(newLength);
console.log(first);
console.log(last);
输出:
3
小周
小许
[ '小林', '小陈' ]注意返回值:push 和 unshift 返回新长度;pop 和 shift 返回被删除的元素。如果数组原本为空,pop() 和 shift() 都返回 undefined。
splice 在中间插入、删除或替换splice(start, deleteCount, ...items) 可以承担三种任务,关键是读懂前两个参数:从哪个索引开始,以及删除多少项。
const list = ["牛奶", "面包", "苹果"];
list.splice(1, 0, "香蕉"); // 只插入
console.log(list);
const removed = list.splice(2, 1); // 只删除
console.log(removed);
console.log(list);
list.splice
输出:
[ '牛奶', '香蕉', '面包', '苹果' ]
[ '面包' ]
[ '牛奶', '香蕉', '苹果' ]
[ '牛奶', '燕麦', '鸡蛋', '苹果' ]替换时,删除数量和插入数量不必相等。上例删除一项却插入两项,因此数组长度增加了 1。
slice 与 splice 只差一个字母,副作用完全不同slice(start, end) 复制从 start 开始、到 end 之前结束的片段,不改变原数组。省略参数时,slice() 可以浅复制整个数组。
const names = ["小林", "小周", "小陈", "小许"];
const copied = names.slice(1, 3);
const merged = names.concat(["小何", "小吴"]);
console.log(copied);
console.log(names);
console.log(merged);输出:
[ '小周', '小陈' ]
[ '小林', '小周', '小陈', '小许' ]
[ '小林', '小周', '小陈', '小许', '小何', '小吴' ]concat 也返回新数组。现代代码还常用展开语法表达合并:const merged = [...names, "小何", "小吴"]。
copyWithin:用数组自身的一段覆盖另一段copyWithin(target, start, end) 会复制从 start 到 end 之前的元素,并从 target 开始覆盖原数组。它不会增加或减少长度,但会直接修改内容。
const letters = ["A", "B", "C", "D", "E"];
letters.copyWithin(0, 3, 5);
console.log(letters);
console.log(letters.length);输出:
[ 'D', 'E', 'C', 'D', 'E' ]
5源片段是索引 3 到索引 5 之前的 ["D", "E"],它从索引 0 开始覆盖了 "A" 和 "B"。这个方法适合原地移动固定长度的数据片段;普通业务替换通常仍以 splice 或新数组写法更直观。
不要用 delete list[index] 删除数组元素。它只移除该索引对应的属性,却保留数组长度并留下空槽。要让后续元素自然前移,使用 splice;要根据条件生成一份新数组,使用 filter。
面对“数组里有没有目标数据”这个需求,答案可能是布尔值、索引,也可能是元素本身。先确定期望的结果形态,再选择方法。
includes 与索引方法const tags = ["JavaScript", "CSS", "JavaScript", "HTML"];
console.log(tags.includes("CSS"));
console.log(tags.indexOf("JavaScript"));
console.log(tags.indexOf("JavaScript", 1));
console.log(tags.lastIndexOf("JavaScript"));
console.输出:
true
0
2
2
-1indexOf 的第二个参数表示从哪个索引开始搜索。不要直接把它的结果当作布尔值:索引 0 在条件判断中是假值,而找不到时返回的 -1 反而是真值。若只关心存在与否,直接用 includes 更清楚。
还有一个边界差异:includes(NaN) 能找到 NaN,indexOf(NaN) 则返回 -1。
find 与 findIndex当目标不是某个固定值,而是“第一件库存为 0 的商品”这类条件时,把检查规则写成回调函数。
const products = [
{ name: "键盘", stock: 8 },
{ name: "鼠标", stock: 0 },
{ name: "显示器", stock: 0 },
];
const soldOutProduct = products.find(product => product.stock === 0);
const soldOutIndex = products.findIndex(product => product.stock ===
输出:
{ name: '鼠标', stock: 0 }
1find 和 findIndex 都会在第一次匹配后停止。如果要取得所有符合条件的元素,应使用稍后介绍的 filter。
遍历就是按顺序访问数组中的元素。不同写法各有适用场景:需要精确控制索引和循环流程时用 for,只读取元素时用 for...of,要对每项执行一次回调时用 forEach。
for、for...of 与 forEachconst scores = [72, 88, 95];
for (let index = 0; index < scores.length; index += 1) {
console.log(index + ":" + scores[index]);
}输出:
0:72
1:88
2:95传统 for 循环能直接使用索引,也支持 break 和 continue。如果只关心元素,可以减少索引样板代码:
for (const score of scores) {
console.log(score);
}forEach 会把当前元素、索引和原数组依次传给回调:
scores.forEach((score, index) => {
console.log("第 " + (index + 1) + " 项:" + score);
});输出:
第 1 项:72
第 2 项:88
第 3 项:95forEach 适合“对每项做一次操作”,例如打印日志或更新界面。它的返回值始终是 undefined,也不能像普通循环那样用 break 提前结束。需要生成新数组时不要依赖 forEach 的返回值,应选择 map 或 filter。
some 与 every 回答真假问题some 检查“是否至少有一项满足条件”,every 检查“是否所有项都满足条件”。它们都会在答案已经确定时停止继续检查。
const scores = [72, 88, 95, 61];
const hasExcellentScore = scores.some(score => score >= 90);
const allPassed = scores.every(score => score >= 60);
console.log(hasExcellentScore);
console.log(allPassed);输出:
true
true这里至少有一个分数达到 90,并且所有分数都达到 60。如果要获得“哪些分数及格”,问题就不再只是真假判断,应该改用 filter。
这三个方法都会按顺序处理元素,但它们解决的问题不同:

三者都不会因为正常调用而修改原数组;但如果回调主动修改对象元素,对象本身仍可能发生变化。
map:一项对应一个新结果const scores = [72, 88, 95, 61];
const labels = scores.map(score => score + "分");
console.log(labels);
console.log(scores);输出:
[ '72分', '88分', '95分', '61分' ]
[ 72, 88, 95, 61 ]map 总会为每个被访问的元素安排一个对应位置。如果回调使用大括号,就要显式写 return:
const doubled = scores.map(score => {
return score * 2;
});忘记 return 会得到一组 undefined,这是使用 map 时最常见的错误之一。
filter:只保留通过条件的元素const scores = [72, 88, 95, 61];
const passed = scores.filter(score => score >= 80);
console.log(passed);
console.log(scores);输出:
[ 88, 95 ]
[ 72, 88, 95, 61 ]回调返回真值时保留当前元素,返回假值时跳过它。还可以结合 indexOf 只保留每个值第一次出现的位置:
const names = ["小林", "小周", "小林", "小陈", "小周"];
const uniqueNames = names.filter((name, index, array) => {
return array.indexOf(name) === index;
});
console.log(uniqueNames);输出:
[ '小林', '小周', '小陈' ]reduce:把多项归约成一个结果reduce 的回调通常接收“累计值”和“当前元素”。第二个参数是累计值的初始值,初学阶段建议始终明确提供。
const scores = [72, 88, 95, 61];
const total = scores.reduce((sum, score) => {
return sum + score;
}, 0);
console.log(total);
console.log(total / scores.length);输出:
316
79第一次执行回调时,sum 从初始值 0 开始;之后每次把当前分数加进去。提供初始值还能让空数组安全地归约为 0:
console.log([].reduce((sum, value) => sum + value, 0));输出:
0如果省略初始值,空数组调用 reduce 会抛出 TypeError。明确写出初始值不仅处理了空数组,也让累计值的类型一目了然。
下面的目标是:保留 80 分及以上的成绩,按从高到低排序,再生成显示标签。
const scores = [72, 88, 95, 61, 83];
const result = scores
.filter(score => score >= 80)
.toSorted((a, b) => b - a)
.map(score => score
输出:
[ '95分', '88分', '83分' ]
[ 72, 88, 95, 61, 83 ]每一步都返回新数组,所以下一步可以继续调用数组方法,原始 scores 保持不变。链式调用不是越长越好;当一步需要复杂条件或中间结果值得命名时,拆成多个变量会更清楚。
选择方法前先说出任务动词:每项“转换”用 map,按条件“保留”用 filter,把多项“汇总”用 reduce,找“第一项”用 find,只问“有没有”用 some 或 includes。
sort() 会直接改变原数组。更容易踩坑的是,它默认把元素转成字符串后比较,而不是按数值大小比较。
const values = [10, 2, 30, 4];
console.log([...values].sort());
console.log([...values].sort((a, b) => a - b));
console.log([...values].sort((a, b)
输出:
[ 10, 2, 30, 4 ]
[ 2, 4, 10, 30 ]
[ 30, 10, 4, 2 ]
[ 10, 2, 30, 4 ]比较函数的返回值决定顺序:
a 排在 b 前面;b 排在 a 前面;0:两者在排序规则下相等。因此数字升序常写 (a, b) => a - b,降序写 (a, b) => b - a。[...values] 先做浅复制,避免 sort 改动原数组。
现代 JavaScript 提供 toSorted() 和 toReversed(),它们直接返回新数组;传统的 sort() 与 reverse() 则原地修改。
const names = ["Maria", "Alicia", "Bert"];
const sorted = names.toSorted();
const reversed = names.toReversed();
console.log(sorted);
console.log(reversed);
console.log(names);输出:
[ 'Alicia', 'Bert', 'Maria' ]
[ 'Bert', 'Alicia', 'Maria' ]
[ 'Maria', 'Alicia', 'Bert' ]若运行环境不支持这些新方法,可以使用 [...names].sort() 和 [...names].reverse()。按中文、英文或其他自然语言规则排列字符串时,可在比较函数中使用 localeCompare;具体顺序会受区域设置影响,不要把某一台机器的显示结果当作跨环境固定顺序。
数组元素本身也可以是数组。最常见的场景是表格、棋盘或按分组保存的数据:外层数组表示行,内层数组表示列。
const grid = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
];
console.log(grid[0]);
console.log(grid[0][1]);
console.log(grid[2][输出:
[ 1, 2, 3 ]
2
9读取 grid[0][1] 可以拆成两步理解:先用 grid[0] 取得第一行 [1, 2, 3],再从这一行读取索引 1,得到 2。
嵌套数据通常配合嵌套循环:
for (const row of grid) {
console.log(row.join(" + "));
}输出:
1 + 2 + 3
4 + 5 + 6
7 + 8 + 9若要处理每个单元格,再加一层循环即可。数组层数越多,索引表达式越难读;当每个位置具有明确业务含义时,对象数组通常比三四层嵌套数组更清楚。
展开语法只复制最外层数组,里面的行仍与原数组共享:
const shallowCopy = [...grid];
shallowCopy[0][0] = 99;
console.log(grid[0][0]);输出:
99如果每一行都是一维基本值数组,可以逐行复制:
const independentGrid = grid.map(row => [...row]);
independentGrid[0][0] = 1;
console.log(independentGrid[0][0]);
console.log(grid[0][0]);输出:
1
99稀疏数组是指长度覆盖了某些索引,但这些索引上没有真正的元素。跳着写索引、扩大 length、使用 delete,或者写出多余逗号,都可能制造空槽。

const sparse = [];
sparse[2] = "C";
const explicit = [undefined, undefined, "C"];
console.log(sparse.length, Object.keys(sparse));
console.log(explicit.length, Object.keys(explicit));输出:
3 [ '2' ]
3 [ '0', '1', '2' ]两个数组长度都是 3,读取前两项也都会得到 undefined。区别在于:sparse 的索引 0 和 1 根本不存在,explicit 则真的保存了两个值为 undefined 的元素。
forEach、map、filter、some、every 和 reduce 的回调会跳过空槽;for...of 会在这些位置读到 undefined;includes(undefined) 也会把空槽当作 undefined 检查。
const sparseValues = [];
sparseValues[2] = "C";
const visits = [];
sparseValues.forEach((value, index) => {
visits.push(index + ":" + value);
});
console.log(visits);
console.log([...sparseValues]);
console.log输出:
[ '2:C' ]
[ undefined, undefined, 'C' ]
truemap 会跳过回调,却在结果数组里保留对应的空槽:
const explicitValues = [undefined, undefined, "C"];
console.log(sparseValues.map(value => value ?? "空").join("|"));
console.log(explicitValues.map(value => value ?? "空").join("|"));输出:
||C
空|空|Creduce 跳过空槽,却不会跳过显式的 undefined:
console.log([1, 2, , 4].reduce((sum, value) => sum + value, 0));
console.log([1, 2, undefined, 4].reduce((sum, value) => sum + value, 0));输出:
7
NaN第一行的空槽没有交给回调;第二行的 undefined 参与数字加法后产生 NaN。业务数据通常不应依赖这类差异。需要占位时明确选用 null 或合适的业务值;需要初始化固定长度数组时,可用 Array(length).fill(initialValue) 或 Array.from。
写入较大的有效索引会把 length 提升到“该索引加 1”,但中间位置不一定存在;length 也可以被直接调大。它只描述索引边界,不保证从索引 0 到末尾都有真实元素。
现在把本章方法组合起来。每个购物车元素都是一条商品记录,字段分别表示名称、单价、数量和是否选中。目标是支持新增、改数量、删除,并生成结算摘要。
const cart = [
{ name: "键盘", price: 299, quantity: 1, selected: true },
{ name: "鼠标", price: 99, quantity: 2, selected: true },
{ name: "支架", price: 129, quantity: 1, selected: false },
];
cart.push({ name: "鼠标垫", price: 39, quantity: 1, selected: true
这里用 push 新增,用 find 取得待修改对象,用 findIndex 配合 splice 删除。删除前必须判断索引不是 -1,否则 splice(-1, 1) 会误删最后一项。
const settlementLines = cart
.filter(item => item.selected && item.quantity > 0)
.map(item => ({
name: item.name,
quantity: item.quantity,
subtotal: item.price * item.quantity,
}));
const totalQuantity = settlementLines.reduce(
(sum, item) => sum
输出:
[
{ name: '键盘', quantity: 1, subtotal: 299 },
{ name: '鼠标', quantity: 3, subtotal: 297 },
{ name: '鼠标垫', quantity: 1, subtotal: 39 }
]
总件数:5
总金额:635rankedLines 按小计降序排列,因此 299 在 297 前面;toSorted 返回新数组,不会重排 settlementLines。
function summarizeCart(items) {
const lines = items
.filter(item => item.selected && item.quantity > 0)
.map(item => ({
...item,
subtotal: item.price * item.quantity,
}))
.toSorted((a, b) => b.subtotal -
完成自己的版本后,逐项检查:
cart.length 恰好增加 1;0 的商品不会进入结算明细;totalQuantity 为所有结算商品数量之和,totalPrice 为所有小计之和;cart 的顺序保持不变。category 字段,用 reduce 计算每个分类的总金额。removeItem(cart, name) 函数,找不到商品时返回 false,删除成功时返回 true。200 的商品,并检查所有商品数量是否都是正整数。sort() 修改原数组,生成一份按商品名称排列的新清单。0 开始的索引保存有顺序的数据,普通创建优先使用方括号字面量。length 表示长度,不是最后一个索引;最后一项可用 array.at(-1) 或 array[array.length - 1]。const 数组可以修改内部元素,但变量不能重新绑定到另一个数组。push、pop、shift、unshift、splice、sort 和 reverse 会修改原数组;slice、concat、map、filter、toSorted 返回新数组。includes 或索引方法,按条件找第一项用 find,找全部匹配项用 filter。map,真假检查用 some 或 every,汇总成单一结果用带初始值的 reduce。toSorted 或先复制再 sort。undefined 的遍历行为不同,业务数据应尽量保持连续明确。