Skip to content

数组、对象、Map 与 Set ​

上一节已经逐条处理学员,但还需要解决三个数据组织问题:一名学员的姓名和成绩怎样放在一起,多名学员怎样组成列表,怎样快速按编号查找并得到不重复的课程?本节依次使用数组、对象、Map 和 Set 完成这些任务。

先从上一节用过的数组开始,再把每一项从姓名字符串扩展为完整学员记录。各小节代码块默认单独运行,标明接续的代码块按顺序运行;在 Console 中重复声明同名 const 时,刷新页面后再运行。

数组 ​

创建与访问 ​

js
const studentNames = ["张三", "李四", "王五"];
const scores = [86, 72, 93];

数组按顺序保存多个值,索引从 0 开始。

接着上面的 studentNames 定义运行:

js
console.log(studentNames[0]); // "张三"
console.log(studentNames[2]); // "王五"
console.log(studentNames.length); // 3

访问不存在的索引会得到 undefined。

修改与查询 ​

报名名单中的姓名可能需要修正,也可能增加新学员:

js
const studentNames = ["张三", "李四"];

studentNames[1] = "李小四";
studentNames.push("王五");

console.log(studentNames);

常用方法:

方法作用是否修改原数组
push(value)末尾追加,返回新长度是
pop()删除并返回最后一项是
unshift(value)开头追加是
shift()删除并返回第一项是
slice(start, end)截取并返回新数组否
splice(start, count, ...)删除、替换或插入是
js
const names = ["张三", "李四", "王五"];
const firstTwo = names.slice(0, 2);

console.log(firstTwo); // ["张三", "李四"]
console.log(names);    // 原数组不变

使用 splice() 前先确认它会直接修改原数组:

js
const names = ["张三", "李四", "王五"];
const removed = names.splice(1, 1, "赵敏");

console.log(removed); // ["李四"]
console.log(names);   // ["张三", "赵敏", "王五"]

这些方法展示了修改原数组与创建新结果的区别。处理业务数据时,如果不希望影响原数组,可以使用展开语法、map() 或 filter() 创建新结果;不要因为变量使用了 const,就误以为数组内容不会变化。

名单维护之外,还可以检查某门课程是否在列表中:

js
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() 查找对象数组。

遍历 ​

js
const studentNames = ["张三", "李四", "王五"];

for (const studentName of studentNames) {
  console.log(studentName);
}

需要索引时,接着上面的 studentNames 示例运行:

js
for (let index = 0; index < studentNames.length; index += 1) {
  console.log(`${index + 1}. ${studentNames[index]}`);
}

对象 ​

创建与读取 ​

js
const student = {
  id: 1,
  name: "张三",
  score: 86,
  status: "active",
  course: "web",
};

属性名描述字段,属性值保存当前记录的数据。

接着上面的 student 对象,先用点语法读取:

js
console.log(student.name);
console.log(student.score);

再用方括号语法读取同一个对象:

js
console.log(student["status"]);

const fieldName = "course";
console.log(student[fieldName]);

属性名是变量、包含特殊字符或来自动态配置时使用方括号。固定且合法的属性名优先点语法。

属性修改与检查 ​

js
const student = {
  id: 1,
  name: "张三",
};

student.status = "pending";
student.status = "active";
delete student.status;

delete 会删除属性,真实业务中是否允许删除字段要看数据模型约定。很多场景更适合保留字段并设置为 null。

判断一个对象是否有某个属性,可使用 Object.hasOwn():

js
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() 调用:

js
const student = {
  name: "张三",
  score: 86,
  getSummary() {
    return `${this.name}:${this.score} 分`;
  },
};

console.log(student.getSummary());

对象方法中的 this 会受到调用方式影响。基础课程只在直接调用对象方法的场景中使用,不把它作为函数主线;大部分数据处理会写成接收参数的独立函数。

对象数组 ​

后台数据最常见的结构:

js
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 数组,逐条读取:

js
for (const student of students) {
  console.log(`${student.id} - ${student.name} - ${student.score}`);
}

引用与复制 ​

对象和数组赋值时保存的是引用:

js
const originalStudent = { name: "张三", status: "pending" };
const sameStudent = originalStudent;

sameStudent.status = "active";

console.log(originalStudent.status); // "active"

两个变量指向同一个对象。接着上面的 originalStudent,用展开语法创建浅复制并修改副本;c07 会正式练习展开语法:

js
const copiedStudent = { ...originalStudent };
copiedStudent.status = "disabled";

console.log(originalStudent.status); // "active"
console.log(copiedStudent.status);   // "disabled"

这里修改副本的 status 不会改变原对象的 status。嵌套对象的浅复制不会递归复制所有层级,本课程不展开深拷贝方案。

即使两条记录的字段内容相同,也可能不是同一个对象。严格相等比较对象时,判断的是是否引用同一个对象:

js
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 不是普通对象的无条件替代品,应该根据数据形态选择。

js
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() 会更新值,不会保留两条记录:

js
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,字符串键与数字键不同

因此,用学员编号建立索引前,要约定编号类型和唯一性。重复编号会使前面的记录被覆盖。

从数组建立映射:

js
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 课程去重 ​

js
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 属性

数组去重:

js
const courseList = ["web", "python", "web", "data"];
const uniqueCourses = [...new Set(courseList)];

console.log(uniqueCourses);

Set 判断对象是否重复时比较对象引用,不会根据对象内容自动去重:

js
const values = [...new Set([{ id: 1 }, { id: 1 }])];
console.log(values.length); // 2

业务对象通常按 id 等稳定字段去重。

学员数据查询与统计 ​

本例约定至少有一名学员,且成绩已经是有效数字。若列表为空,直接用总分除以 students.length 会得到 NaN;下一节会把空列表处理放进计算函数。

js
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 收集不重复课程,三者各有职责。

练习与验收 ​

  1. 在综合案例中增加一名成绩为 81、课程为 web 的学员,使用新编号。人数应为 5,平均分为 84,courseCodes.size 仍为 3。
  2. 在独立的 Map 示例中,先保存编号 1,再更新同一编号,确认读取到的是新值;再用不存在的编号查找,应得到 undefined。
  3. 用两个变量引用同一名学员,修改其中一个变量读取到的姓名,解释另一个变量为什么也看到变化。

下一节把这些重复的数据处理过程封装成函数,通过参数提供数据,通过返回值取得结果。

小结 ​

  1. 数组保存有顺序的一组值,对象表示一条有字段的记录。
  2. 对象数组是前端业务数据的常见结构。
  3. 对象和数组是引用类型,赋值不会自动复制内容。
  4. Map 适合动态键值映射,常用于按 ID 建立索引。
  5. Set 适合不重复值集合,不能按对象内容自动去重。