现代 JavaScript 与数组方法
上一节已经手写了筛选和转换函数。现在希望直接表达“保留正常学员”“生成姓名与成绩摘要”,同时减少反复读取字段和复制对象的代码。本节先认识短回调,再按“提取数据 → 组合数据 → 查询与转换”整理这些操作。
主线是箭头函数、解构、展开、可选链和常用数组方法;剩余参数、从键值对创建对象和排序作为补充练习。先能解释每一步的输入与输出,再考虑写得更短。
本章代码块默认分别运行;标明接续的除外。在 Console 中运行下一种写法时,如果遇到同名 const 重复声明,刷新页面后再试。
箭头函数
普通函数表达式:
const getLevel = function (score) {
return score >= 60 ? "合格" : "需要补考";
};
console.log(getLevel(86)); // "合格"箭头函数:
const getLevel = (score) => {
return score >= 60 ? "合格" : "需要补考";
};
console.log(getLevel(86)); // "合格"函数体只有一个返回表达式时可以简写:
const getLevel = (score) => (score >= 60 ? "合格" : "需要补考");
const getName = (student) => student.name;
console.log(getLevel(86)); // "合格"
console.log(getName({ name: "张三" })); // "张三"直接返回对象要用圆括号包裹:
const createSummary = (student) => ({
id: student.id,
name: student.name,
});
console.log(createSummary({ id: 1, name: "张三" }));箭头函数没有自己的 this。本课程主要把它用于短回调,不用它定义依赖动态 this 的对象方法。
数据提取与组合
对象解构
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,需要空值兜底时再使用 ??。
解构可以重命名并提供默认值。函数参数也可以解构:
const formatStudent = ({ name, score }) => `${name}:${score} 分`;
console.log(formatStudent({ name: "张三", score: 86 })); // "张三:86 分"参数结构复杂或字段很多时,先接收对象再在函数体中解构,错误更容易定位。
数组解构
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对象数组通常更常用对象解构;数组解构适合顺序含义稳定的数据。
展开语法
复制数组并追加:
const students = [{ id: 1, name: "张三" }];
const nextStudents = [...students, { id: 2, name: "李四" }];
console.log(nextStudents.map((student) => student.name)); // ["张三", "李四"]复制对象并覆盖字段:
const student = { id: 1, name: "张三", status: "pending" };
const activeStudent = { ...student, status: "active" };
console.log(student.status); // "pending"
console.log(activeStudent.status); // "active"这是浅复制。嵌套对象仍共享内部引用:
const student = {
name: "张三",
contact: { mobile: "13800000001" },
};
const copiedStudent = { ...student };
copiedStudent.contact.mobile = "13800000002";
console.log(student.contact.mobile); // 也发生变化本课程不把展开语法误称为通用深拷贝方案。
业务数据更新时,优先根据原对象创建新对象,而不是直接改写原对象:
const student = { id: 1, name: "张三", status: "pending" };
const nextStudent = {
...student,
status: "active",
};
console.log(student.status); // "pending"
console.log(nextStudent.status); // "active"剩余参数
const calculateTotal = (...scores) => {
let total = 0;
for (const score of scores) {
total += score;
}
return total;
};
console.log(calculateTotal(86, 72, 93));同样的 ... 在函数参数中表示收集剩余值,在数组或对象字面量中表示展开,含义由位置决定。
对象字段处理
可选链与空值合并
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。它改善缺失字段读取,但不能代替数据验证。
例如,手机号约定为字符串时,可选链不能把一个数字自动变成字符串。读取前先确定字段的允许类型,再决定哪些位置允许为空。
字段遍历与构建
const statusTextMap = {
active: "正常",
pending: "待审核",
disabled: "已停用",
};
console.log(Object.keys(statusTextMap));
console.log(Object.values(statusTextMap));
console.log(Object.entries(statusTextMap));接着上面的 statusTextMap,遍历键值对:
for (const [status, text] of Object.entries(statusTextMap)) {
console.log(`${status} -> ${text}`);
}补充:如果手中已有键值对数组,可以反向创建对象:
const pairs = [
["active", "正常"],
["disabled", "已停用"],
];
const statusTextMap = Object.fromEntries(pairs);
console.log(statusTextMap.active); // "正常"数组方法
下面各个方法共用这组数据。单独练习某个方法时,先把数据放到 app.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 遍历
students.forEach((student, index) => {
console.log(`${index + 1}. ${student.name}`);
});forEach() 适合逐项执行副作用,例如输出日志。它返回 undefined,不能用 break 提前结束,也不适合直接生成新数组。
这里的“副作用”指输出日志、修改外部状态等可观察操作。这个例子依次输出四名学员,原数组仍用于后续练习。
map 转换
上一节的手写转换函数,是遍历后把每个转换结果加入数组。针对当前数据,可以先写成:
const summaries = [];
for (const student of students) {
summaries.push({
id: student.id,
label: `${student.name} · ${student.score} 分`,
});
}
console.table(summaries);用 map() 表达同一个任务:对每项执行转换,并返回等长新数组。下面是替代写法,与上面选择一种运行:
const summaries = students.map((student) => ({
id: student.id,
label: `${student.name} · ${student.score} 分`,
}));
console.table(summaries);回调必须返回转换结果。漏写 return 会得到一组 undefined。
两种写法都得到四条摘要,第一条的 label 为“张三 · 86 分”。原来手动创建结果数组并 push() 的部分,由 map() 完成。
filter 筛选
filter() 保留回调结果为真的项:
const activeStudents = students.filter(
(student) => student.status === "active",
);
console.table(activeStudents);它返回新数组,不修改原数组。结果可能为空。
本例保留张三、李四和赵敏,共三人。它与上一节手写筛选函数的对应关系是:student.status === "active" 负责判断,filter() 负责遍历和收集满足条件的原始元素。
find 与 findIndex 查找
const targetStudent = students.find((student) => student.id === 2);
const targetIndex = students.findIndex((student) => student.id === 2);
console.log(targetStudent?.name); // "李四"
console.log(targetIndex); // 1find()返回第一项,找不到返回undefined。findIndex()返回第一项索引,找不到返回-1。
接着上面的 targetStudent 示例,访问 find() 结果前处理缺失:
const studentName = targetStudent?.name ?? "未找到学员";
console.log(studentName); // "李四"some 与 every 判断
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" 当作有效成绩。先验证数据,再用成绩做业务判断;下一节会把字段验证接入完整查询流程。
方法链组合
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() 每一步都会返回新数组,下一步继续处理;但数组中的对象元素仍可能是原对象的引用。链条过长时拆成命名变量:
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 之间的数字;字段验证会在下一节接入。下面是可单独运行的完整版本,不需要拼接前面的片段。
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,先复制数组,再按成绩降序排列:
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(),把这些结果继续归并为人数、平均分和及格率。
小结
- 箭头函数适合短回调,但不拥有自己的
this。 - 解构提取字段,展开语法完成浅复制和组合。
- 可选链与空值合并可安全处理缺失字段。
map转换、filter筛选、find查找、some/every判断。- 现代语法用于提升可读性,不是为了把所有逻辑压缩成一行。