Skip to content

函数与回调 ​

上一节能遍历学员并计算结果。如果多个地方都需要成绩等级和学员摘要,复制判断代码会让修改规则变得困难。本节先把单个任务封装成函数,再把函数作为参数传入批量处理流程,为下一节的数组方法做准备。

函数通过参数接收输入,通过 return 返回结果。先明确“传入什么、得到什么”,再编写内部步骤。

函数基础 ​

声明、调用与参数 ​

先用最短的欢迎语练习声明和调用,再把重复的成绩处理写成函数。

js
function greetStudent(studentName) {
    console.log(`欢迎 ${studentName} 加入课程`);
}

greetStudent("张三");
greetStudent("李四");

函数声明本身不会执行函数体,使用函数名加括号才是调用。studentName 是形参,"张三" 是实参。

多个参数:

js
function formatStudent(studentName, courseName) {
    return `${studentName} · ${courseName}`;
}

console.log(formatStudent("张三", "FastAPI Web 全栈"));

参数过多时通常考虑传入对象,让字段含义更清楚。

返回值 ​

下面的成绩已按前两节的规则校验,getLevel() 只负责把有效成绩转换为等级。

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

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

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

    return "需要补考";
}

const level = getLevel(86);
console.log(level);

return 把结果交给调用位置,并立即结束当前函数。没有显式 return 的函数返回 undefined。

优先让计算函数返回结果,由调用者决定是输出、保存还是继续计算:

js
function calculateAverage(total, count) {
    if (count === 0) {
        return 0;
    }

    return total / count;
}

console.log(calculateAverage(251, 3));

参数约定 ​

默认参数 ​

js
function formatStatus(status = "pending") {
    if (status === "active") {
        return "正常";
    }

    if (status === "pending") {
        return "待审核";
    }

    if (status === "disabled") {
        return "已停用";
    }

    return "未知";
}

console.log(formatStatus());           // "待审核"
console.log(formatStatus("archived")); // "未知"
console.log(formatStatus(null));       // "未知"

只有实参为 undefined 或没有传入时才使用默认值;传入 null 不会触发默认参数。未知状态也不能当作“待审核”,应与综合案例一样返回“未知”。

输入校验 ​

下面把前面的 calculateAverage() 改为带校验的版本。两个代码块分别运行,避免同名函数混在一起。

js
function calculateAverage(total, count) {
    if (!Number.isFinite(total) || !Number.isInteger(count) || count < 0) {
        return null;
    }

    if (count === 0) {
        return 0;
    }

    return total / count;
}

console.log(calculateAverage(251, 3)); // 约 83.67
console.log(calculateAverage(0, 0));   // 0
console.log(calculateAverage(NaN, 2)); // null

这里约定总分必须是有限数字,人数必须是非负整数;无人时以 0 表示平均分,无效参数返回 null。typeof NaN 也是 "number",所以只检查类型不够。大型项目会有更完整的错误约定,本节先保持简单。

作用域与数据共享 ​

函数与块级作用域 ​

js
const courseName = "FastAPI Web 全栈";

function createSummary(studentName) {
    const message = `${studentName} · ${courseName}`;
    return message;
}

console.log(createSummary("张三"));
// console.log(message); // ReferenceError
  • 外层变量在函数内部可见。
  • 函数内部声明的变量只在函数内部可见。
  • 参数也是函数局部变量。

函数需要的数据优先通过参数传入,避免读取和修改大量全局变量。

除了函数,if、for 等语句的花括号也会形成代码块。let 和 const 声明的变量只在所在代码块及其内部可见:

js
const score = 86;

if (score >= 60) {
    const resultText = "合格";
    console.log(resultText); // 可以读取
}

// console.log(resultText); // ReferenceError:超出块级作用域

for (let index = 0; index < 2; index += 1) {
    console.log(index); // 依次输出 0、1
}

// console.log(index); // ReferenceError

如果分支结束后还要使用某个结果,应先在外层声明,再在分支中赋值。前面成绩分级示例把 let level 写在 if 之前,就是为了在判断结束后输出它。

内外层可以声明同名变量,但它们是不同的绑定:

js
const status = "active";

{
    const status = "pending";
    console.log(status); // "pending",优先读取当前层
}

console.log(status); // "active",外层变量没有被修改

这种同名遮蔽容易让初学者误以为外层数据被修改,业务代码优先使用清楚的名称。同一个作用域不能重复用 let、const 声明同名变量,也不能在它们初始化之前读取。

参数与原数据 ​

数字、字符串等值作为参数传入后,给形参重新赋值不会改变调用处的变量。对象参数仍指向原对象,直接修改其属性会影响调用者的数据。

js
function changeScore(score) {
    score = 90;
    return score;
}

const originalScore = 86;
console.log(changeScore(originalScore)); // 90
console.log(originalScore);             // 86

对象参数的属性修改则会留在原对象上:

js
function disableStudent(student) {
    student.status = "disabled";
}

const student = {name: "张三", status: "active"};
disableStudent(student);
console.log(student.status); // "disabled"

本章的摘要函数读取输入字段,再返回新对象;需要改变原数据的函数,应让名称和说明明确表达这个行为。下一节会用展开语法学习创建更新后的对象。

函数作为值 ​

函数表达式与赋值 ​

函数也可以作为值保存:

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

console.log(getLevel(86));

函数声明在当前作用域解析时即可使用;函数表达式需要等变量完成初始化后再调用。基础项目在普通命名函数中优先使用函数声明,回调中会常见函数表达式或箭头函数。

函数可以像数据一样赋给另一个变量;下一小节再把它作为参数传入:

js
function formatName(student) {
    return student.name;
}

const formatter = formatName;

console.log(formatter({name: "张三"}));

这里没有写 formatName(),因为我们要传递函数本身,不是立即调用后的结果。

回调与筛选 ​

传给另一个函数、由对方在合适时机调用的函数称为回调:

js
function processStudent(student, formatter) {
    const result = formatter(student);
    console.log(result);
}

function formatStudentName(student) {
    return `学员:${student.name}`;
}

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

processStudent(student, formatStudentName);

执行过程:

text
processStudent 接收数据和函数
  ↓
内部调用 formatter(student)
  ↓
formatStudentName 产生字符串
  ↓
processStudent 输出结果

回调让“处理流程”和“具体处理方式”分开。

回调不等于异步。上例在 processStudent() 执行过程中立即调用 formatter(),属于同步调用;后续事件和请求中的回调会结合具体运行时机再讲解。

案例:手写一个筛选函数

js
function filterStudents(students, predicate) {
    const result = [];

    for (const student of students) {
        if (predicate(student)) {
            result.push(student);
        }
    }

    return result;
}

function isActiveStudent(student) {
    return student.status === "active";
}

const students = [
    {name: "张三", status: "active"},
    {name: "李四", status: "disabled"},
    {name: "王五", status: "active"},
];

const activeStudents = filterStudents(students, isActiveStudent);
console.table(activeStudents);

下一节的 Array.prototype.filter() 就采用相同思想。先看懂手写版本,再学习内置方法会更自然。

学员摘要生成 ​

本例约定每条学员记录的成绩已校验为 0 到 100 之间的数字。先给单条记录生成摘要,再把这个函数作为参数交给遍历函数。

js
function getScoreLevel(score) {
    if (score >= 90) {
        return "优秀";
    }

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

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

    return "需要补考";
}

function getStatusText(status) {
    const statusTextMap = {
        active: "正常",
        pending: "待审核",
        disabled: "已停用",
    };

    return statusTextMap[status] ?? "未知";
}

function createStudentSummary(student) {
    return {
        id: student.id,
        name: student.name,
        level: getScoreLevel(student.score),
        statusText: getStatusText(student.status),
    };
}

function transformStudents(students, transformer) {
    const result = [];

    for (const student of students) {
        result.push(transformer(student));
    }

    return result;
}

const students = [
    {id: 1, name: "张三", score: 86, status: "active"},
    {id: 2, name: "李四", score: 72, status: "pending"},
];

const summaries = transformStudents(students, createStudentSummary);
console.table(summaries);
console.log(students[0].level); // undefined,原记录没有被添加 level

这段程序先用 transformStudents() 遍历,再把每条记录交给 createStudentSummary()。摘要函数继续调用成绩和状态函数,最终得到两条新摘要:张三为“良好/正常”,李四为“合格/待审核”。createStudentSummary() 返回新对象,没有给原学员记录添加等级字段。

练习与验收 ​

  1. 使用“返回值”小节中的分级函数,分别调用 getLevel(59)、getLevel(60) 和 getLevel(90),应得到“需要补考”“合格”“优秀”。
  2. 将 greetStudent("张三") 的调用结果保存到变量再输出。应先看到欢迎语,再看到 undefined;说明输出日志与返回结果的区别。
  3. 给摘要案例添加一名成绩为 95、状态为 disabled 的学员,应多得到一条“优秀/已停用”的摘要,无需修改遍历函数。
  4. 说明为什么传入 createStudentSummary 时不写括号,以及该函数最终在哪里被调用。
  5. 分别运行 formatStatus("archived")、calculateAverage(0, 0) 和 calculateAverage(Infinity, 2),结果应为“未知”、0 和 null;说明为什么后两种输入不能按同一规则处理。

下一节保留同样的“遍历 → 调用回调 → 收集结果”过程,用内置的 map()、filter() 表达它。

小结 ​

  1. 函数通过参数接收输入,通过 return 给出输出。
  2. 变量应声明在需要它的最小作用域,依赖优先通过参数传入。 let 和 const 还受代码块范围限制,分支内声明的变量不能随意在分支外读取。
  3. 函数可以作为值传递,回调由另一个函数在合适时机调用。