数组、对象、Map 与 Set
上一节已经逐条处理学员,但还需要解决三个数据组织问题:一名学员的姓名和成绩怎样放在一起,多名学员怎样组成列表,怎样快速按编号查找并得到不重复的课程?本节依次使用数组、对象、Map 和 Set 完成这些任务。
先从上一节用过的数组开始,再把每一项从姓名字符串扩展为完整学员记录。各小节代码块默认单独运行,标明接续的代码块按顺序运行;在 Console 中重复声明同名 const 时,刷新页面后再运行。
数组
创建与访问
const studentNames = ["张三", "李四", "王五"];
const scores = [86, 72, 93];数组按顺序保存多个值,索引从 0 开始。
接着上面的 studentNames 定义运行:
console.log(studentNames[0]); // "张三"
console.log(studentNames[2]); // "王五"
console.log(studentNames.length); // 3访问不存在的索引会得到 undefined。
修改与查询
报名名单中的姓名可能需要修正,也可能增加新学员:
const studentNames = ["张三", "李四"];
studentNames[1] = "李小四";
studentNames.push("王五");
console.log(studentNames);常用方法:
| 方法 | 作用 | 是否修改原数组 |
|---|---|---|
push(value) | 末尾追加,返回新长度 | 是 |
pop() | 删除并返回最后一项 | 是 |
unshift(value) | 开头追加 | 是 |
shift() | 删除并返回第一项 | 是 |
slice(start, end) | 截取并返回新数组 | 否 |
splice(start, count, ...) | 删除、替换或插入 | 是 |
const names = ["张三", "李四", "王五"];
const firstTwo = names.slice(0, 2);
console.log(firstTwo); // ["张三", "李四"]
console.log(names); // 原数组不变使用 splice() 前先确认它会直接修改原数组:
const names = ["张三", "李四", "王五"];
const removed = names.splice(1, 1, "赵敏");
console.log(removed); // ["李四"]
console.log(names); // ["张三", "赵敏", "王五"]这些方法展示了修改原数组与创建新结果的区别。处理业务数据时,如果不希望影响原数组,可以使用展开语法、map() 或 filter() 创建新结果;不要因为变量使用了 const,就误以为数组内容不会变化。
名单维护之外,还可以检查某门课程是否在列表中:
const courses = ["html", "css", "javascript"];
console.log(courses.includes("css")); // true
console.log(courses.indexOf("javascript")); // 2
console.log(courses.indexOf("python")); // -1,未找到后面会用 find() 和 findIndex() 查找对象数组。
遍历
const studentNames = ["张三", "李四", "王五"];
for (const studentName of studentNames) {
console.log(studentName);
}需要索引时,接着上面的 studentNames 示例运行:
for (let index = 0; index < studentNames.length; index += 1) {
console.log(`${index + 1}. ${studentNames[index]}`);
}对象
创建与读取
const student = {
id: 1,
name: "张三",
score: 86,
status: "active",
course: "web",
};属性名描述字段,属性值保存当前记录的数据。
接着上面的 student 对象,先用点语法读取:
console.log(student.name);
console.log(student.score);再用方括号语法读取同一个对象:
console.log(student["status"]);
const fieldName = "course";
console.log(student[fieldName]);属性名是变量、包含特殊字符或来自动态配置时使用方括号。固定且合法的属性名优先点语法。
属性修改与检查
const student = {
id: 1,
name: "张三",
};
student.status = "pending";
student.status = "active";
delete student.status;delete 会删除属性,真实业务中是否允许删除字段要看数据模型约定。很多场景更适合保留字段并设置为 null。
判断一个对象是否有某个属性,可使用 Object.hasOwn():
const student = { id: 1, name: "张三" };
console.log(Object.hasOwn(student, "name")); // true
console.log(Object.hasOwn(student, "score")); // false
const studentWithScore = { score: undefined };
console.log(Object.hasOwn(studentWithScore, "score")); // true最后一条记录虽然把 score 设为 undefined,但属性仍然存在;“是否有属性”和“属性值是什么”是两个问题。
对象方法
这一小节先认识写法,函数的参数、返回值和调用过程在下一节正式学习。对象里的函数称为方法,下面通过 student.getSummary() 调用:
const student = {
name: "张三",
score: 86,
getSummary() {
return `${this.name}:${this.score} 分`;
},
};
console.log(student.getSummary());对象方法中的 this 会受到调用方式影响。基础课程只在直接调用对象方法的场景中使用,不把它作为函数主线;大部分数据处理会写成接收参数的独立函数。
对象数组
后台数据最常见的结构:
const students = [
{
id: 1,
name: "张三",
score: 86,
status: "active",
course: "web",
},
{
id: 2,
name: "李四",
score: 72,
status: "active",
course: "python",
},
{
id: 3,
name: "王五",
score: 93,
status: "disabled",
course: "web",
},
];接着上面的 students 数组,逐条读取:
for (const student of students) {
console.log(`${student.id} - ${student.name} - ${student.score}`);
}引用与复制
对象和数组赋值时保存的是引用:
const originalStudent = { name: "张三", status: "pending" };
const sameStudent = originalStudent;
sameStudent.status = "active";
console.log(originalStudent.status); // "active"两个变量指向同一个对象。接着上面的 originalStudent,用展开语法创建浅复制并修改副本;c07 会正式练习展开语法:
const copiedStudent = { ...originalStudent };
copiedStudent.status = "disabled";
console.log(originalStudent.status); // "active"
console.log(copiedStudent.status); // "disabled"这里修改副本的 status 不会改变原对象的 status。嵌套对象的浅复制不会递归复制所有层级,本课程不展开深拷贝方案。
即使两条记录的字段内容相同,也可能不是同一个对象。严格相等比较对象时,判断的是是否引用同一个对象:
const firstStudent = { id: 1, name: "张三" };
const sameStudent = firstStudent;
const anotherStudent = { id: 1, name: "张三" };
console.log(firstStudent === sameStudent); // true
console.log(firstStudent === anotherStudent); // false
console.log(firstStudent.id === anotherStudent.id); // true业务上判断是否为同一名学员,通常比较约定唯一的 id。两个数组也按引用比较,不能用 [] === [] 判断数组是否为空,仍应检查 length。
索引与去重
Map 编号索引
普通对象适合表示一条有固定字段的记录、配置对象或 JSON 风格数据;Map 适合建立“键 → 值”的动态映射。Map 不是普通对象的无条件替代品,应该根据数据形态选择。
const studentById = new Map();
studentById.set(1, { id: 1, name: "张三" });
studentById.set(2, { id: 2, name: "李四" });
console.log(studentById.get(2));
console.log(studentById.has(3));
studentById.delete(2);
console.log(studentById.has(2)); // false核心 API:
| 操作 | 示例 |
|---|---|
| 新增或更新 | map.set(key, value) |
| 读取 | map.get(key) |
| 判断键 | map.has(key) |
| 删除 | map.delete(key) |
对同一个键再次调用 set() 会更新值,不会保留两条记录:
const studentById = new Map();
studentById.set(1, { id: 1, name: "张三" });
studentById.set(1, { id: 1, name: "张小三" });
console.log(studentById.get(1).name); // "张小三"
console.log(studentById.get("1")); // undefined,字符串键与数字键不同因此,用学员编号建立索引前,要约定编号类型和唯一性。重复编号会使前面的记录被覆盖。
从数组建立映射:
const students = [
{ id: 1, name: "张三" },
{ id: 2, name: "李四" },
];
const studentById = new Map();
for (const student of students) {
studentById.set(student.id, student);
}
console.log(studentById.get(1));Map 可使用数字等多种类型作为键,遍历顺序按插入顺序。普通 JSON 数据不会直接保存为 Map,接口传输时通常仍使用对象和数组。
Set 课程去重
const courseCodes = new Set();
courseCodes.add("web");
courseCodes.add("python");
courseCodes.add("web");
console.log(courseCodes.has("web")); // true
courseCodes.delete("python");
console.log(courseCodes.has("python")); // false
console.log(courseCodes.size); // 1,当前只有 web核心 API:
| 操作 | 示例 |
|---|---|
| 新增 | set.add(value) |
| 判断 | set.has(value) |
| 删除 | set.delete(value) |
| 数量 | set.size 属性 |
数组去重:
const courseList = ["web", "python", "web", "data"];
const uniqueCourses = [...new Set(courseList)];
console.log(uniqueCourses);Set 判断对象是否重复时比较对象引用,不会根据对象内容自动去重:
const values = [...new Set([{ id: 1 }, { id: 1 }])];
console.log(values.length); // 2业务对象通常按 id 等稳定字段去重。
学员数据查询与统计
本例约定至少有一名学员,且成绩已经是有效数字。若列表为空,直接用总分除以 students.length 会得到 NaN;下一节会把空列表处理放进计算函数。
const students = [
{ id: 1, name: "张三", score: 86, status: "active", course: "web" },
{ id: 2, name: "李四", score: 72, status: "active", course: "python" },
{ id: 3, name: "王五", score: 93, status: "disabled", course: "web" },
{ id: 4, name: "赵敏", score: 88, status: "active", course: "data" },
];
const studentById = new Map();
const courseCodes = new Set();
let totalScore = 0;
for (const student of students) {
studentById.set(student.id, student);
courseCodes.add(student.course);
totalScore += student.score;
}
const averageScore = totalScore / students.length;
console.log("编号为 3 的学员:", studentById.get(3));
console.log("不重复课程:", [...courseCodes]);
console.log("课程种类数:", courseCodes.size);
console.log("平均分:", averageScore);这组数据的平均分为 84.75;编号 3 对应王五,不重复课程为 web、python、data,courseCodes.size 得到种类数 3。原数组负责保留顺序,Map 提供按编号访问的入口,Set 收集不重复课程,三者各有职责。
练习与验收
- 在综合案例中增加一名成绩为
81、课程为web的学员,使用新编号。人数应为5,平均分为84,courseCodes.size仍为3。 - 在独立的
Map示例中,先保存编号1,再更新同一编号,确认读取到的是新值;再用不存在的编号查找,应得到undefined。 - 用两个变量引用同一名学员,修改其中一个变量读取到的姓名,解释另一个变量为什么也看到变化。
下一节把这些重复的数据处理过程封装成函数,通过参数提供数据,通过返回值取得结果。
小结
- 数组保存有顺序的一组值,对象表示一条有字段的记录。
- 对象数组是前端业务数据的常见结构。
- 对象和数组是引用类型,赋值不会自动复制内容。
Map适合动态键值映射,常用于按 ID 建立索引。Set适合不重复值集合,不能按对象内容自动去重。