Skip to content

现代 JavaScript 与数组方法 ​

上一节已经手写了筛选和转换函数。现在希望直接表达“保留正常学员”“生成姓名与成绩摘要”,同时减少反复读取字段和复制对象的代码。本节先认识短回调,再按“提取数据 → 组合数据 → 查询与转换”整理这些操作。

主线是箭头函数、解构、展开、可选链和常用数组方法;剩余参数、从键值对创建对象和排序作为补充练习。先能解释每一步的输入与输出,再考虑写得更短。

本章代码块默认分别运行;标明接续的除外。在 Console 中运行下一种写法时,如果遇到同名 const 重复声明,刷新页面后再试。

箭头函数 ​

普通函数表达式:

js
const getLevel = function (score) {
  return score >= 60 ? "合格" : "需要补考";
};

console.log(getLevel(86)); // "合格"

箭头函数:

js
const getLevel = (score) => {
  return score >= 60 ? "合格" : "需要补考";
};

console.log(getLevel(86)); // "合格"

函数体只有一个返回表达式时可以简写:

js
const getLevel = (score) => (score >= 60 ? "合格" : "需要补考");
const getName = (student) => student.name;

console.log(getLevel(86));                // "合格"
console.log(getName({ name: "张三" })); // "张三"

直接返回对象要用圆括号包裹:

js
const createSummary = (student) => ({
  id: student.id,
  name: student.name,
});

console.log(createSummary({ id: 1, name: "张三" }));

箭头函数没有自己的 this。本课程主要把它用于短回调,不用它定义依赖动态 this 的对象方法。

数据提取与组合 ​

对象解构 ​

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

const { id, name, score } = student;
const { name: studentName, remark = "暂无备注" } = student;

console.log(id, name, score);      // 1 张三 86
console.log(studentName, remark);  // 张三 暂无备注

studentName 得到“张三”,remark 得到“暂无备注”。解构的默认值只在对应字段为 undefined 或不存在时生效;字段值为 null 时仍保留 null,需要空值兜底时再使用 ??。

解构可以重命名并提供默认值。函数参数也可以解构:

js
const formatStudent = ({ name, score }) => `${name}:${score} 分`;

console.log(formatStudent({ name: "张三", score: 86 })); // "张三:86 分"

参数结构复杂或字段很多时,先接收对象再在函数体中解构,错误更容易定位。

数组解构 ​

js
const scores = [86, 72, 93];
const [firstScore, secondScore] = scores;
const [first, , third] = scores;

console.log(firstScore, secondScore); // 86 72
console.log(first, third);            // 86 93

对象数组通常更常用对象解构;数组解构适合顺序含义稳定的数据。

展开语法 ​

复制数组并追加:

js
const students = [{ id: 1, name: "张三" }];
const nextStudents = [...students, { id: 2, name: "李四" }];

console.log(nextStudents.map((student) => student.name)); // ["张三", "李四"]

复制对象并覆盖字段:

js
const student = { id: 1, name: "张三", status: "pending" };
const activeStudent = { ...student, status: "active" };

console.log(student.status);       // "pending"
console.log(activeStudent.status); // "active"

这是浅复制。嵌套对象仍共享内部引用:

js
const student = {
  name: "张三",
  contact: { mobile: "13800000001" },
};

const copiedStudent = { ...student };
copiedStudent.contact.mobile = "13800000002";

console.log(student.contact.mobile); // 也发生变化

本课程不把展开语法误称为通用深拷贝方案。

业务数据更新时,优先根据原对象创建新对象,而不是直接改写原对象:

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

const nextStudent = {
  ...student,
  status: "active",
};

console.log(student.status);     // "pending"
console.log(nextStudent.status); // "active"

剩余参数 ​

js
const calculateTotal = (...scores) => {
  let total = 0;

  for (const score of scores) {
    total += score;
  }

  return total;
};

console.log(calculateTotal(86, 72, 93));

同样的 ... 在函数参数中表示收集剩余值,在数组或对象字面量中表示展开,含义由位置决定。

对象字段处理 ​

可选链与空值合并 ​

js
const student = {
  name: "张三",
  contact: null,
};

const mobile = student.contact?.mobile;
const displayMobile = student.contact?.mobile ?? "暂无手机号";

console.log(mobile);        // undefined
console.log(displayMobile); // "暂无手机号"

?. 在左侧为 null 或 undefined 时停止访问并返回 undefined。它改善缺失字段读取,但不能代替数据验证。

例如,手机号约定为字符串时,可选链不能把一个数字自动变成字符串。读取前先确定字段的允许类型,再决定哪些位置允许为空。

字段遍历与构建 ​

js
const statusTextMap = {
  active: "正常",
  pending: "待审核",
  disabled: "已停用",
};

console.log(Object.keys(statusTextMap));
console.log(Object.values(statusTextMap));
console.log(Object.entries(statusTextMap));

接着上面的 statusTextMap,遍历键值对:

js
for (const [status, text] of Object.entries(statusTextMap)) {
  console.log(`${status} -> ${text}`);
}

补充:如果手中已有键值对数组,可以反向创建对象:

js
const pairs = [
  ["active", "正常"],
  ["disabled", "已停用"],
];

const statusTextMap = Object.fromEntries(pairs);

console.log(statusTextMap.active); // "正常"

数组方法 ​

下面各个方法共用这组数据。单独练习某个方法时,先把数据放到 app.js 开头,再接上该方法的片段;不同方案中同名的变量不要重复粘贴到一起。

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: "web" },
];

先根据任务选择方法,不要为了使用新语法而强行改写所有循环:

需求推荐写法说明
逐项执行副作用forEach()不能使用 break,不返回新数组
需要 break、continue 或复杂控制for...of控制流程更清楚
转换每一项map()返回等长新数组
筛选多项filter()返回可能为空的新数组
查找第一项find()找不到返回 undefined
判断是否存在some()返回布尔值
判断是否全部满足every()返回布尔值
下一节汇总统计reduce()下一节结合统计案例学习

forEach 遍历 ​

js
students.forEach((student, index) => {
  console.log(`${index + 1}. ${student.name}`);
});

forEach() 适合逐项执行副作用,例如输出日志。它返回 undefined,不能用 break 提前结束,也不适合直接生成新数组。

这里的“副作用”指输出日志、修改外部状态等可观察操作。这个例子依次输出四名学员,原数组仍用于后续练习。

map 转换 ​

上一节的手写转换函数,是遍历后把每个转换结果加入数组。针对当前数据,可以先写成:

js
const summaries = [];

for (const student of students) {
  summaries.push({
    id: student.id,
    label: `${student.name} · ${student.score} 分`,
  });
}

console.table(summaries);

用 map() 表达同一个任务:对每项执行转换,并返回等长新数组。下面是替代写法,与上面选择一种运行:

js
const summaries = students.map((student) => ({
  id: student.id,
  label: `${student.name} · ${student.score} 分`,
}));

console.table(summaries);

回调必须返回转换结果。漏写 return 会得到一组 undefined。

两种写法都得到四条摘要,第一条的 label 为“张三 · 86 分”。原来手动创建结果数组并 push() 的部分,由 map() 完成。

filter 筛选 ​

filter() 保留回调结果为真的项:

js
const activeStudents = students.filter(
  (student) => student.status === "active",
);

console.table(activeStudents);

它返回新数组,不修改原数组。结果可能为空。

本例保留张三、李四和赵敏,共三人。它与上一节手写筛选函数的对应关系是:student.status === "active" 负责判断,filter() 负责遍历和收集满足条件的原始元素。

find 与 findIndex 查找 ​

js
const targetStudent = students.find((student) => student.id === 2);
const targetIndex = students.findIndex((student) => student.id === 2);

console.log(targetStudent?.name); // "李四"
console.log(targetIndex);         // 1
  • find() 返回第一项,找不到返回 undefined。
  • findIndex() 返回第一项索引,找不到返回 -1。

接着上面的 targetStudent 示例,访问 find() 结果前处理缺失:

js
const studentName = targetStudent?.name ?? "未找到学员";
console.log(studentName); // "李四"

some 与 every 判断 ​

js
const isValidScore = (score) =>
  Number.isFinite(score) && score >= 0 && score <= 100;

const hasFailedStudent = students.some((student) => student.score < 60);
const allScoresValid = students.every(
  (student) => isValidScore(student.score),
);

console.log(hasFailedStudent);    // false
console.log(allScoresValid);      // true
console.log(isValidScore("86")); // false,字符串不是有效成绩
  • some():是否至少一项满足条件。
  • every():是否所有项都满足条件。

空数组调用 some() 返回 false,调用 every() 返回 true。业务上是否接受空数组要另外判断。

Number.isFinite() 不会把字符串转为数字,因此这里不会把 "86" 当作有效成绩。先验证数据,再用成绩做业务判断;下一节会把字段验证接入完整查询流程。

方法链组合 ​

js
const activeWebSummaries = students
  .filter(
    (student) =>
      student.status === "active" && student.course === "web",
  )
  .map((student) => ({
    id: student.id,
    name: student.name,
    level: student.score >= 60 ? "合格" : "需要补考",
  }));

console.table(activeWebSummaries);

filter() 和 map() 每一步都会返回新数组,下一步继续处理;但数组中的对象元素仍可能是原对象的引用。链条过长时拆成命名变量:

js
const createStudentSummary = (student) => ({
  id: student.id,
  name: student.name,
  level: student.score >= 60 ? "合格" : "需要补考",
});

const activeStudents = students.filter(
  (student) => student.status === "active",
);

const webStudents = activeStudents.filter(
  (student) => student.course === "web",
);

const summaries = webStudents.map(createStudentSummary);
console.table(summaries);

命名的中间结果更适合教学、调试和复用。

学员数据查询与转换 ​

现在把“筛选正常的 Web 学员 → 生成成绩摘要 → 判断是否存在优秀学员”组合起来,并加入按编号查找。以下数据约定成绩已经是 0 到 100 之间的数字;字段验证会在下一节接入。下面是可单独运行的完整版本,不需要拼接前面的片段。

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: "web" },
];

const getLevel = (score) => {
  if (score >= 90) {
    return "优秀";
  }

  if (score >= 80) {
    return "良好";
  }

  if (score >= 60) {
    return "合格";
  }

  return "需要补考";
};

const activeWebStudents = students.filter(
  ({ status, course }) => status === "active" && course === "web",
);

const summaries = activeWebStudents.map(({ id, name, score }) => ({
  id,
  name,
  score,
  level: getLevel(score),
}));

const hasExcellentStudent = summaries.some(
  ({ score }) => score >= 90,
);

const targetId = 99;
const targetStudent = students.find(({ id }) => id === targetId);
const targetName = targetStudent?.name ?? "未找到学员";

console.table(summaries);
console.log("存在优秀学员:", hasExcellentStudent);
console.log(`编号 ${targetId}:`, targetName);

结果应包含张三和赵敏,成绩分别为 86 和 88,等级都为“良好”;“存在优秀学员”为 false,编号 99 显示“未找到学员”。虽然原数组中有一名 93 分学员,但他不在当前筛选结果中。

成绩排序 ​

需要按成绩查看先后顺序时,可以使用 sort()。继续使用上面的 students,先复制数组,再按成绩降序排列:

js
const rankedStudents = [...students].sort(
  (first, second) => second.score - first.score,
);

console.table(rankedStudents);
console.log(rankedStudents.map((student) => student.id)); // [3, 4, 1, 2]
console.log(students.map((student) => student.id)); // [1, 2, 3, 4]

排序后的编号为 [3, 4, 1, 2],原数组顺序保持不变。sort() 会修改调用它的数组,因此这里先用展开语法复制;对象本身仍是共享引用。

比较函数返回负数时,first 应排在 second 前面;返回正数时排在后面;返回 0 表示两者排序权重相同。数字升序使用 first.score - second.score,降序反过来。

不传比较函数时,默认按字符串形式排序,不能直接用于成绩等数字的大小排列。排序属于补充操作,不影响本节筛选与转换主线。

练习与验收 ​

基于完整案例,每次恢复原始数据后只修改一个条件:

任务预期结果
筛选全部 active 学员张三、李四、赵敏,共 3 人
筛选 active 且 course === "web"张三、赵敏,共 2 人
把 targetId 改为 2,再恢复为 99先显示“李四”,再显示“未找到学员”
把赵敏成绩改为 95 后重新查询当前结果中存在优秀学员
复制后按成绩升序排列原始数据编号顺序为 [2, 1, 4, 3],原数组顺序不变

说明为什么摘要使用 map(),筛选使用 filter(),而输出日志可使用 forEach()。下一节将学习 reduce(),把这些结果继续归并为人数、平均分和及格率。

小结 ​

  1. 箭头函数适合短回调,但不拥有自己的 this。
  2. 解构提取字段,展开语法完成浅复制和组合。
  3. 可选链与空值合并可安全处理缺失字段。
  4. map 转换、filter 筛选、find 查找、some/every 判断。
  5. 现代语法用于提升可读性,不是为了把所有逻辑压缩成一行。