B 币安 · 全球最大的加密货币交易所币安 注册 → AD OKX OKX · 全球领先的加密货币交易所OKX 注册 → AD
💻 编程基础 · 第6讲 / 10

数组与对象 — 装载和处理数据

数组是有顺序的列表,对象是按名字(键)查找值的集合。两者合在一起的“对象数组”就像一张表,可以用循环和 `map`、`filter`、`reduce` 求合计、挑出想要的行。

⏱ 约24分钟 ✍️ 练习题 5 道 🧪 代码练习 4 题 更新 2026-10-09
🎯 本讲学习目标
  • 能用数组的索引、length、push 读取和添加值
  • 能用点表示法和方括号表示法读取、修改对象的属性
  • 能把对象数组看作表格,用循环或 map、filter、reduce 加工
  • 能用 JSON.stringify 和 JSON.parse 把数据转成文字再还原

1.数组——有顺序的列表

到目前为止,我们都是一个变量装一个值。可是总不能把 30 个学生的分数装进 30 个变量里。数组是把多个值按顺序放在一个名字下的数据类型。在方括号里用逗号排列值,就能写出 [70, 85, 90] 这样的数组。第4讲用 for...of 逐个取出的那个列表,就是数组。

数组里的值用位置编号——索引——来取。索引不是从 1 而是从 0 开始,所以第一个值是 arr[0],第二个值是 arr[1]。值的个数是 arr.length,最后一个值的索引永远是 arr.length - 1。第4讲见过的 off-by-one 失误正是在这里产生的。

读取不存在的索引不会报错,而是得到 undefined。在长度为 3 的数组里读 arr[3] 或 arr[-1],结果都是 undefined。想从后往前数,可以用 arr.at(-1) 取得最后一个值。数组里也可以混放数字、字符串、布尔值,但实际上放同一类的值才便于处理。

索引从 0 开始,不存在的格子是 undefined
const fruits = ['苹果', '梨', '橘子'];
console.log(fruits[0], fruits[2]);
console.log(fruits.length);
console.log(fruits[fruits.length - 1]);
console.log(fruits[3]);
console.log(fruits.at(-1));
输出
苹果 橘子
3
橘子
undefined
橘子

2.修改与查找数组——`push`、`pop`、`includes`、`indexOf`

push 在数组末尾添加值,pop 取出末尾的值并返回,同时把它从数组中删掉。也可以像 arr[1] = '葡萄' 这样直接往索引里放值来修改。这里有个容易混淆的地方:用 const 创建的数组,内容也是可以改的。const 阻止的是“给这个名字重新放入另一个数组”,而不是修改数组里的格子。

要确认某个值在不在数组里,includes 返回真或假,indexOf 返回位置。indexOf 找不到时返回 -1,和第4讲用 -1 表示“没找到”是同一个约定。所以如果写成 if (arr.indexOf(x)),值在第一格(0)时为假,没找到(-1)反而为真(第3讲的真值与假值)。只想知道在不在,就用 includes。

把数组放进另一个变量,不是复制,而是多出一个“指向同一个数组的名字”。在下面的代码里往 b 里 push,a 也会变。就像两个人分别拿着同一栋房子的地址。真正需要一份独立的副本时,要像 [...a] 这样展开,创建新数组。对象的行为也完全一样。

push、pop、includes、indexOf
const cart = ['牛奶', '面包'];
cart.push('鸡蛋');
console.log(cart, cart.length);
const last = cart.pop();
console.log(last, cart);
console.log(cart.includes('面包'), cart.indexOf('面包'));
console.log(cart.indexOf('海苔'));
输出
[ '牛奶', '面包', '鸡蛋' ] 3
鸡蛋 [ '牛奶', '面包' ]
true 1
-1
指向同一个数组的两个名字 vs 展开后创建的副本
const a = [1, 2];
const b = a;
b.push(3);
console.log(a);
const c = [...a];
c.push(4);
console.log(a, c);
输出
[ 1, 2, 3 ]
[ 1, 2, 3 ] [ 1, 2, 3, 4 ]

3.对象——带名字标签的格子

如果说数组是按“第几个”找值,对象就是按“名字”找值。在花括号里用逗号排列 键: 值,就能写出 { name: '小明', age: 20 } 这样的对象。每一对键和值叫作属性。像一位会员、一件商品这样,多条信息合起来构成一个对象时,用对象最合适。

属性通常像 user.name 这样用点来读;键名存放在变量里时,就像 user[key] 这样用方括号来读。读取不存在的属性,和数组里不存在的格子一样会得到 undefined。可是如果再从这个 undefined 上读属性,就会出现 TypeError。下面的代码就是这种情况,原因是把键名错写成了 nmae。信息的具体措辞在不同浏览器中略有差异。

把 AI 写的代码用在真实数据上时,这是最常见的错误之一。通常是数据里的键名和代码期望的键名不同,或者只有某些条目缺了这个属性。错误信息括号里的 (reading 'length') 告诉你“读什么的时候失败了”,所以顺着往回找它前面那个值为什么是 undefined 就行(第8讲)。

读取、修改、添加,不存在的属性是 undefined
const user = { name: '小明', age: 20 };
console.log(user.name, user.age);
user.age = 21;
user.city = '上海';
console.log(user);
const key = 'name';
console.log(user[key]);
console.log(user.email);
输出
小明 20
{ name: '小明', age: 21, city: '上海' }
小明
undefined
从 undefined 上再读就是 TypeError
const user = { name: '小明' };
console.log(user.nmae);
console.log(user.nmae.length);
输出
undefined
TypeError: Cannot read properties of undefined (reading 'length')

4.对象数组 = 表格

真实数据大多是“对象数组”的样子。数组的每一格是表格的一行,对象的键就是列名。可以把它想成把 Excel 之类电子表格里的一张表搬进了代码。下面的表格和代码,是用两种方式表示同一份虚构的销售记录。

在这种结构里,把第4讲的循环和刚学的读取属性结合起来,几乎什么计算都能做。逐行取出(for...of),读取需要的列(row.qty),再累加或计数。第9讲会把文字形式的表格转成这种结构,亲手求合计、平均值和分组合计。

虚构的销售记录——一行就是一个对象
(索引)`item``price``qty`
0笔记本15004
1铅笔50010
2橡皮8003
例题上面这份虚构的销售记录,总销售额(价格 × 数量之和)是多少?
const sales = [
  { item: '笔记本', price: 1500, qty: 4 },
  { item: '铅笔', price: 500, qty: 10 },
  { item: '橡皮', price: 800, qty: 3 },
];
let revenue = 0;
for (const row of sales) {
  revenue += row.price * row.qty;
}
console.log(revenue);
输出
13400
  1. 第1步 累积什么:销售额合计。起始值 0。
  2. 第2步 每一行做的事:加上 row.price * row.qty。笔记本 6,000,铅笔 5,000,橡皮 2,400。
  3. 第3步 结果:6,000 + 5,000 + 2,400 = 13,400韩元。
  4. 验算:有 3 行,循环也应该转 3 圈。每行打印中间合计,会按 6000 → 11000 → 13400 的顺序增长。
答案13,400韩元——逐行累加价格 × 数量

5.初尝 `map`、`filter`、`reduce`

数组有可以代替循环的方法,最常用的三个是 map、filter、reduce。三者都接收第5讲的箭头函数,并对数组里的每个值调用这个函数。map 用每个值转换后的结果组成新数组(长度相同);filter 只挑出函数返回真的值组成新数组(长度相同或更短);reduce 把所有值折叠成一个,得出合计之类的单个值。

重要的一点是,map 和 filter 不修改原数组,而是返回新数组。这和第5讲的纯函数性质相同,结果容易预测。reduce 一定要在第二个实参里放起始值。reduce((s, x) => s + x, 0) 里的 0,相当于第4讲累加模式里的 let sum = 0。漏掉起始值的话,遇到空数组就会出现 TypeError。

一开始觉得循环更好读也没关系,这些方法只是把循环能做的事写得更短而已。不过 AI 写的代码里,这三个方法常常像链条一样连在一起出现,所以最好练习一步步拆开,打印中间结果来读。

map 是转换,filter 是挑选,reduce 是折叠——原数组保持不变
const prices = [1500, 500, 800, 12000];
const withTax = prices.map((p) => p * 1.1);
console.log(withTax.map((p) => Math.round(p)));
const cheap = prices.filter((p) => p < 1000);
console.log(cheap);
const total = prices.reduce((s, p) => s + p, 0);
console.log(total);
console.log(prices);
输出
[ 1650, 550, 880, 13200 ]
[ 500, 800 ]
14800
[ 1500, 500, 800, 12000 ]
三种方法对比
方法做什么返回什么换成循环就是
map逐个转换值长度相同的新数组把转换后的值 push 进新数组
filter挑出符合条件的值长度相同或更短的新数组只在条件为真时 push
reduce折叠成一个单个值(合计等)累加模式

6.JSON——交换数据用的文字格式

JSON 是把对象和数组写成文字(字符串)的格式。名字来自 JavaScript,但如今几乎所有编程语言和网络服务交换数据时都使用它。配置文件、服务器的响应、应用保存的数据,相当一部分都是 JSON。JSON.stringify 把值转成 JSON 文字,JSON.parse 把 JSON 文字还原成值。

JSON 和 JavaScript 的对象语法很像,但更严格。键必须用双引号括起来,字符串也只能用双引号。最后一项后面不允许有逗号,也不能写注释。函数和 undefined 放不进去,stringify 时,作为对象属性会被省略,在数组里则变成 null。只要违反任何一条规则,JSON.parse 就会报 SyntaxError。

JSON 挤成长长的一行时很难读。这时把它粘贴到 JSON 格式化工具里,就会自动缩进,有时还会指出违反规则的地方。不过,真实会员信息这类含有个人信息的数据,不要粘贴到外部工具或 AI 里,换成结构相同的虚构数据再检查(第10讲)。

值 → JSON 文字 → 再变回值
const item = { name: '笔记本', price: 1500, tags: ['文具'] };
const text = JSON.stringify(item);
console.log(text);
console.log(typeof text);
const back = JSON.parse(text);
console.log(back.price + 500);
输出
{"name":"笔记本","price":1500,"tags":["文具"]}
string
2000
键没有引号,还用了单引号——违反 JSON 规则
const bad = "{ name: '笔记本' }";
console.log(JSON.parse(bad));
输出
SyntaxError: Expected property name or '}' in JSON at position 2 (line 1 column 3)
像 JSON.stringify(值, null, 2) 这样在第三个实参里给 2,就会转成缩进两格的多行文字。本页的练习区也会原样显示多行内容,打印长数据查看结构时不妨试试。

📌 要点总结

  • 数组是有顺序的列表——索引从 0 开始,最后一个是 arr.length - 1,不存在的格子是 undefined
  • 对象是按名字(键)查找值的集合——从 undefined 上再读属性就是 TypeError
  • 把数组、对象放进另一个变量,不是复制,而是指向同一个东西
  • 对象数组就是表格——逐行取出、读取列再累加,或用 map、filter、reduce 加工
  • JSON 的键用双引号,禁止末尾逗号——用 stringify 转成文字,用 parse 还原成值

✍️ 练习题

请先自己作答,再展开“答案与解析”。

Q1. 在 const a = [10, 20, 30]; 中,a[a.length] 的值是?

⭕ 回答正确

❌ 再想一想

答案与解析
答案 ② undefined

a.length 是 3,索引只有 0~2,所以 a[3] 是 undefined。最后一个值是 a[a.length - 1]。

Q2. 关于 const list = ['a', 'b']; list.push('c'); 的结果,哪种说法正确?

⭕ 回答正确

❌ 再想一想

答案与解析
答案 ② list 变成 ['a', 'b', 'c']

const 只阻止给名字重新放入别的值。修改数组里的格子、用 push 添加都是可以的。push 加在末尾。

Q3. [3, 8, 1, 6].filter((x) => x > 4) 的结果是?

⭕ 回答正确

❌ 再想一想

答案与解析
答案 ① [8, 6]

filter 只挑出函数返回真的值组成新数组。得到真假数组,是把同样的函数放进 map 的时候。

Q4. 下面哪一个是正确的 JSON?

⭕ 回答正确

❌ 再想一想

答案与解析
答案 ④ { "name": "小明", "age": 20 }

JSON 里的键和字符串都必须用双引号,最后一项后面不允许有逗号。第一项的问题是键没有引号,第二项用了单引号,第三项是末尾逗号。

Q5. 出现了 TypeError: Cannot read properties of undefined (reading 'price')。应该先检查什么?

答案与解析
参考答案 检查 .price 前面那个值为什么是 undefined——是不是键名拼错、索引不存在,或者有缺少该属性的条目。

这个错误的意思是“试图从 undefined 上读取 price”。问题不在 price,而在它前面的值(例如 items[i] 或 row.info)。

🧪 代码练习

读取数组和对象,编写在对象数组(表格)中累积值、挑选值的函数。空数组之类的边界值也会一起评分。

代码只在本浏览器内的隔离环境中运行,不会发送到服务器。无法访问网络,超过2秒会自动停止。改过的代码只保存在此浏览器中。按 Ctrl+Enter(⌘+Enter)运行,Tab 键输入两个空格(先按 Esc 再按 Tab 可跳到下一项)。

浏览器脚本未开启,无法运行代码,但题目、初始代码、自动检查项和参考答案都可以直接阅读。

1取出最后一个值

编写函数 lastItem(arr):返回数组 arr 的最后一个值。空数组时返回 null。

自动检查
  • lastItem([1,2,3])期望值 3
  • lastItem(["苹果"])期望值 "苹果"
  • lastItem([])期望值 null
  • lastItem([0,false])期望值 false
💡 提示

空数组里的 arr[arr.length - 1] 就是 arr[-1],结果是 undefined。所以要先单独处理空数组。

查看参考答案
function lastItem(arr) {
  if (arr.length === 0) {
    return null;
  }
  return arr[arr.length - 1];
}

2数量合计

编写函数 totalQty(rows):接收 { item, qty } 形式的对象数组 rows,返回 qty 的合计。空数组返回 0。

自动检查
  • totalQty([{"item":"笔记本","qty":4},{"item":"铅笔","qty":10}])期望值 14
  • totalQty([])期望值 0
  • totalQty([{"item":"橡皮","qty":3}])期望值 3
  • totalQty([{"item":"a","qty":0},{"item":"b","qty":0}])期望值 0
💡 提示

每一行是一个对象。用点表示法读取 row.qty。也可以用 reduce 写成一行。

查看参考答案
function totalQty(rows) {
  let total = 0;
  for (const row of rows) {
    total += row.qty;
  }
  return total;
}

3只挑出符合条件的名字

编写函数 namesFrom(rows, min):接收 { item, price } 形式的对象数组 rows 和标准价格 min,按原顺序返回价格大于等于 min 的行的 item 组成的数组。没有符合的行时返回空数组 []。

自动检查
  • namesFrom([{"item":"笔记本","price":1500},{"item":"铅笔","price":500},{"item":"书包","price":20000}], 1000)期望值 ["笔记本","书包"]
  • namesFrom([], 100)期望值 []
  • namesFrom([{"item":"铅笔","price":500}], 500)期望值 ["铅笔"]
  • namesFrom([{"item":"铅笔","price":500}], 501)期望值 []
💡 提示

分两步:先用 filter 挑出行,再用 map 只取名字。用循环加 if 和 push 也可以。

查看参考答案
function namesFrom(rows, min) {
  return rows
    .filter((r) => r.price >= min)
    .map((r) => r.item);
}

4按项目计数

编写函数 countBy(words):接收字符串数组 words,返回记录每个词出现次数的对象。例:['苹果', '梨', '苹果'] → { 苹果: 2, 梨: 1 }。空数组返回空对象 {}。

自动检查
  • countBy(["苹果","梨","苹果"])期望值 {"梨":1,"苹果":2}
  • countBy([])期望值 {}
  • countBy(["a"])期望值 {"a":1}
  • countBy(["x","x","x","y"])期望值 {"x":3,"y":1}
💡 提示

键名存放在变量 w 里,所以不用点表示法,而用方括号 counts[w]。第一次出现的词是 undefined。

查看参考答案
function countBy(words) {
  const counts = {};
  for (const w of words) {
    if (counts[w] === undefined) {
      counts[w] = 1;
    } else {
      counts[w] += 1;
    }
  }
  return counts;
}

🤖 可以这样问 AI

复制后把 [ ] 部分换成你的情况。不要直接相信回答,请与正文对照核实。

拿不准该怎样把数据装进数组、对象时

请把下面的虚构数据转换成 JavaScript 的“对象数组”。键名用英文小写,数字用数字而不是字符串。然后用这份数据可以做的 3 种计算(求合计、按条件挑选、分组计数),把循环版本和 `map`/`filter`/`reduce` 版本并排展示。数据:[粘贴虚构的表格]

看不懂串在一起的 `map`、`filter`、`reduce` 代码时

请把下面代码里的 `map`/`filter`/`reduce` 链条一步步拆开,用小的示例输入展示每一步的中间结果是什么数组或值。也请写出功能相同的 `for...of` 循环版本。代码:[粘贴]

`JSON.parse` 报错时

请找出下面这段 JSON 字符串在 `JSON.parse` 时报错的所有违规之处(引号、末尾逗号、注释等),并给出修正后的 JSON。内容是虚构的示例数据:[粘贴]

🧰 相关工具

可以当场检查、整理本讲涉及的 JSON、正则表达式和文本的工具。请不要粘贴真实的个人信息或密码。

参考
  • MDN 文档 — JavaScript 指南:索引集合、使用对象
  • MDN 文档 — 数组的 map / filter / reduce 方法、JSON.stringify / JSON.parse 参考
  • JSON 数据交换格式国际标准的一般规则

达成全部学习目标后,请点击完成。

💻 编程基础

  1. 1什么是程序:把事情拆成顺序、条件和循环的思维
  2. 2值、变量与数据类型:给值贴上名字标签
  3. 3条件语句:根据情况走不同的路
  4. 4循环 — 让同一件事准确地重复多次
  5. 5函数 — 拆成一个个有名字的小机器
  6. 6数组与对象 — 装载和处理数据
  7. 7字符串与文本处理——截取、查找、替换文字
  8. 8调试与读懂错误——把红字变成线索
  9. 9简单的自动化示例——用代码做表格计算和文本整理
  10. 10阅读并检验 AI 写的代码——测试、安全、许可证与个人信息
📚 推荐阅读
📊Using AI for Spreadsheets, and Checking the Formulas→ 💻AI Coding Assistants: Security and Licence Risks→ 💬Practising Conversation in a Foreign Language with AI→ ⚙️Finding Work Tasks Worth Automating with AI→
← AI时代基础学力课程