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) 取得最后一个值。数组里也可以混放数字、字符串、布尔值,但实际上放同一类的值才便于处理。
undefinedconst 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、indexOfconst 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
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讲)。
undefinedconst 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: '上海' }
小明
undefinedundefined 上再读就是 TypeErrorconst 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 | 笔记本 | 1500 | 4 |
| 1 | 铅笔 | 500 | 10 |
| 2 | 橡皮 | 800 | 3 |
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步 累积什么:销售额合计。起始值
0。 - 第2步 每一行做的事:加上
row.price * row.qty。笔记本 6,000,铅笔 5,000,橡皮 2,400。 - 第3步 结果:6,000 + 5,000 + 2,400 = 13,400韩元。
- 验算:有 3 行,循环也应该转 3 圈。每行打印中间合计,会按 6000 → 11000 → 13400 的顺序增长。
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讲)。
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
2000const 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还原成值
🧪 代码练习
读取数组和对象,编写在对象数组(表格)中累积值、挑选值的函数。空数组之类的边界值也会一起评分。
代码只在本浏览器内的隔离环境中运行,不会发送到服务器。无法访问网络,超过2秒会自动停止。改过的代码只保存在此浏览器中。按 Ctrl+Enter(⌘+Enter)运行,Tab 键输入两个空格(先按 Esc 再按 Tab 可跳到下一项)。
浏览器脚本未开启,无法运行代码,但题目、初始代码、自动检查项和参考答案都可以直接阅读。
1取出最后一个值
编写函数 lastItem(arr):返回数组 arr 的最后一个值。空数组时返回 null。
lastItem([1,2,3])期望值3lastItem(["苹果"])期望值"苹果"lastItem([])期望值nulllastItem([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}])期望值14totalQty([])期望值0totalQty([{"item":"橡皮","qty":3}])期望值3totalQty([{"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 数据交换格式国际标准的一般规则
达成全部学习目标后,请点击完成。
此浏览器无法保存记录,仅在当前页面显示。