第2章 2.3 函数声明与表达式
🎯 开场:为什么你需要一个「 reusable recipe」?
上一章我们学了循环,能让电脑自动干重复的事。比如让电脑帮你打印 100 次「小明今天也要加油」,一句 for i in range(100): print("小明今天也要加油") 就搞定了。
但是问题来了——
如果你让电脑帮你算「小明的语文成绩 + 数学成绩」,第一次你写了一段代码,第二次算「小红」的成绩,你得把代码复制粘贴一遍。第三次算「老王」的成绩,又粘贴一遍。代码越写越长,改一个 bug 要改十几处。
说白了:重复的代码是万恶之源。
函数就是来解决这个问题的——它让你把一段代码「打包」成一个「菜谱」,下次想用直接喊名字就行,不用再抄一遍。
学完这一章,你就能:
- 写出自己的第一个「可复用代码块」
- 搞懂 function 声明和函数表达式到底有啥区别(面试常问!)
- 学会给函数传参数、打包返回值,像搭积木一样组合代码
🧱 基础:函数的两种写法
什么是函数?
\n\n
\n\n
\n\n函数就是一段可以反复使用的代码**。就像你点外卖的「收藏夹」,收藏的是一个完整的「下单流程」,而不是一道具体的菜。
JavaScript 里创建函数有两种方式,我们一个个说。
写法一:函数声明(Function Declaration)
最经典、最常用的写法:
function greet(name) {
return "你好," + name + "!";
}
console.log(greet("小明"));
// 输出:你好,小明!
function 关键字后面跟函数名 greet,括号里是参数 name,大括号里是要执行的代码。
生活类比:这就像在餐厅点菜时喊「服务员,来一份宫保鸡丁」——你报出菜名(函数名),厨房就知道该做什么。return 就是厨房做好菜端出来的动作。
注意! 函数声明会被「提升」(hoisting),意思是你可以先调用再声明:
// 先调用(写在前面)
console.log(add(3, 5));
// 后声明(写在后面)
function add(a, b) {
return a + b;
}
// 输出:8
这行得记住:函数声明可以提升,函数表达式不行。
写法二:函数表达式(Function Expression)
把函数赋值给一个变量,这个变量就变成了函数:
const greet = function(name) {
return "你好," + name + "!";
};
console.log(greet("小明"));
// 输出:你好,小明!
注意最后有个分号 ;,因为这是语句,不是声明。
生活类比:函数表达式就像「把菜谱写下来,夹进文件夹」——你有一张写满步骤的纸(函数本身),然后给它贴个标签(变量名)。菜谱本身不会动,得通过标签去找它。
关键区别:函数表达式不会提升!如果你先调用后定义,会报错:
console.log(add(3, 5)); // ReferenceError: Cannot access 'add' before initialization
const add = function(a, b) {
return a + b;
};
写法三:IIFE(立即调用函数表达式)
还有一个特殊写法,函数定义完立刻执行,用完就扔:
(function() {
console.log("我立刻就执行了!");
})();
// 输出:我立刻就执行了!
生活类比:IIFE 就像拆开一包一次性方便面——打开包装、倒调料、加热水,吃完扔掉,不会留到下一顿。为啥要这样?因为「作用域隔离」——函数内部的变量不会污染外面的代码。
const result = (function() {
const temp = "我是临时工";
return temp + ",干完就走";
})();
console.log(result);
// 输出:我是临时工,干完就走
console.log(temp); // ReferenceError: temp is not defined
参数默认值
有时候参数没传,我们想给个默认值:
function greet(name = "朋友") {
return "你好," + name + "!";
}
console.log(greet()); // 输出:你好,朋友!
console.log(greet("小明")); // 输出:你好,小明!
注意! 默认值参数必须在最后,或者给所有参数都设默认值:
// ✅ 正确:默认值放最后
function greet(name, age = 18) {
return name + "今年" + age + "岁";
}
// ❌ 错误:默认值不能在非默认值前面
// function greet(name = "朋友", age) { ... }
剩余参数(Rest Parameters)
有时候不确定会有多少个参数,用 ... 把它们收集成一个数组:
function sum(...numbers) {
let total = 0;
for (let n of numbers) {
total += n;
}
return total;
}
console.log(sum(1, 2, 3)); // 输出:6
console.log(sum(10, 20, 30, 40)); // 输出:100
生活类比:剩余参数就像「自助餐的取餐盘」——你不知道后面会有多少菜,先用一个大盘子接着,谁来都能放。
🔥 实战:3 个递进小项目
项目 1:成绩计算器(5 分钟)
跟着抄就能跑,理解函数的基本用法:
// 定义一个计算平均分的函数
function calculateAverage(scores) {
let sum = 0;
for (let i = 0; i < scores.length; i++) {
sum += scores[i];
}
return sum / scores.length;
}
// 小明的成绩单
const xiaomingScores = [85, 92, 78, 96, 88];
const average = calculateAverage(xiaomingScores);
console.log("小明的平均分是:" + average.toFixed(2));
// 输出:小明的平均分是:87.80
一句话解释:函数把「算平均分」这件事打包起来,以后给谁算成绩,传入数组就行。
项目 2:成绩管理系统(15 分钟)
从 JSON 数据读取多个学生的成绩,计算每个人的平均分并排名:
// 模拟从数据库读取的 JSON 数据
const studentData = `[
{"name": "小明", "scores": [85, 92, 78, 96, 88]},
{"name": "小红", "scores": [92, 90, 85, 88, 91]},
{"name": "老王", "scores": [78, 82, 80, 75, 79]},
{"name": "小李", "scores": [95, 98, 92, 96, 94]}
]`;
// 解析 JSON
const students = JSON.parse(studentData);
// 计算单个学生平均分的函数
function getAverage(scores) {
const sum = scores.reduce((acc, score) => acc + score, 0);
return sum / scores.length;
}
// 处理所有学生
function processStudents(data) {
return data.map(student => ({
name: student.name,
average: getAverage(student.scores).toFixed(2)
}));
}
// 按平均分排序
function sortByAverage(students) {
return students.sort((a, b) => b.average - a.average);
}
// 执行
const processed = processStudents(students);
const ranked = sortByAverage(processed);
console.log("成绩排名:");
ranked.forEach((student, index) => {
console.log(`${index + 1}. ${student.name} - ${student.average}分`);
});
预期输出:
成绩排名:
1. 小李 - 95.00分
2. 小红 - 89.20分
3. 小明 - 87.80分
4. 老王 - 78.80分
一句话解释:map 把每个学生处理一遍,sort 按平均分排序,函数各司其职,组合起来完成复杂任务。
项目 3:命令行待办清单(15 分钟)
组合前两个项目的能力,写一个有点真实用的小工具——添加任务、查看任务、标记完成:
// 待办清单数据
let todoList = [
{ id: 1, task: "完成数学作业", done: false },
{ id: 2, task: "背诵英语单词", done: true },
{ id: 3, task: "跑步30分钟", done: false }
];
let nextId = 4;
// 添加任务
function addTask(task) {
const newTask = {
id: nextId++,
task: task,
done: false
};
todoList.push(newTask);
return newTask;
}
// 查看所有任务
function showTasks() {
console.log("\n📋 当前待办清单:");
todoList.forEach(t => {
const status = t.done ? "✅" : "⬜";
console.log(`${status} [${t.id}] ${t.task}`);
});
}
// 标记任务完成
function completeTask(id) {
const task = todoList.find(t => t.id === id);
if (task) {
task.done = true;
console.log(`✅ 任务「${task.task}」已完成!`);
} else {
console.log(`❌ 未找到 ID 为 ${id} 的任务`);
}
}
// 筛选未完成任务
function showPendingTasks() {
const pending = todoList.filter(t => !t.done);
console.log(`\n⏳ 还有 ${pending.length} 个任务待完成:`);
pending.forEach(t => console.log(` - ${t.task}`));
}
// 测试
showTasks();
addTask("整理房间");
completeTask(1);
showPendingTasks();
预期输出:
📋 当前待办清单:
⬜ [1] 完成数学作业
✅ [2] 背诵英语单词
⬜ [3] 跑步30分钟
📋 当前待办清单:
⬜ [1] 完成数学作业
✅ [2] 背诵英语单词
⬜ [3] 跑步30分钟
✅ [4] 整理房间
✅ 任务「完成数学作业」已完成!
⏳ 还有 2 个任务待完成:
- 跑步30分钟
- 整理房间
一句话解释:每个功能都是一个函数,数据和逻辑分离,改其中一个不影响其他。
💪 进阶:常见坑 + 调试技巧
坑 1:函数表达式 vs 函数声明的 this
// ❌ 错误:函数表达式不能当作对象方法用
const calculator = {
name: "计算器",
// 这里不能用函数表达式!
add: function(a, b) {
return a + b;
}
};
// ✅ 正确:函数声明可以
const calculator = {
name: "计算器",
add(a, b) {
return a + b;
}
};
坑 2:默认参数引用问题
// ❌ 错误:不要用可变对象当默认值
function appendItem(item, list = []) {
list.push(item);
return list;
}
console.log(appendItem("a")); // ["a"]
console.log(appendItem("b")); // ["a", "b"] ← 保留上次的结果!
// ✅ 正确:用 undefined 触发默认值
function appendItem(item, list) {
if (list === undefined) {
list = [];
}
list.push(item);
return list;
}
坑 3:箭头函数没有 arguments
// ❌ 错误:箭头函数没有 arguments 对象
const sum = () => {
console.log(arguments); // ReferenceError: arguments is not defined
};
// ✅ 正确:用剩余参数代替
const sum = (...nums) => {
console.log(nums); // [1, 2, 3]
};
坑 4:return 换行问题
// ❌ 错误:JavaScript 会自动在 return 后加分号
function getName() {
return
"小明";
}
console.log(getName()); // undefined
// ✅ 正确:要么把值放同一行,要么加括号
function getName() {
return (
"小明"
);
}
// 或
function getName() {
return "小明";
}
调试技巧:console.log 打日志
function calculateTotal(price, quantity, discount) {
console.log("输入参数:", { price, quantity, discount });
const subtotal = price * quantity;
console.log("小计:", subtotal);
const total = subtotal * (1 - discount);
console.log("折后价:", total);
return total;
}
console.log("实付:" + calculateTotal(100, 3, 0.1));
输出:
输入参数: { price: 100, quantity: 3, discount: 0.1 }
小计: 300
折后价: 270
实付:270
✏️ 练习题
练习 1(2 分钟):改参数值
- 输入:把项目 1 里的
xiaomingScores改成[90, 85, 95] - 预期输出:
小明的平均分是:90.00 - 提示:直接把数组替换掉就行
练习 2(2 分钟):加判断
- 输入:在项目 1 的
calculateAverage函数里,加一个判断——如果数组为空,返回 0 - 预期输出:
calculateAverage([])返回0 - 提示:用
if判断scores.length === 0
练习 3(3 分钟):处理新数据
- 输入:用项目 2 的方法处理这组数据:
[{"name": "张三", "scores": [70, 75, 80]}] - 预期输出:
张三 - 75.00分 - 提示:复用
processStudents函数
练习 4(3 分钟):串起两个项目
- 输入:把项目 2 的排名功能集成到项目 3 的待办清单里,给任务按「紧急程度」排序
- 预期输出:能够按自定义规则排序任务
- 提示:参考
sortByAverage的写法
练习 5(5 分钟):分析报错
- 输入:运行下面代码,分析为什么报错
console.log(multiply(3, 4));
const multiply = function(a, b) {
return a * b;
};
- 预期输出:
ReferenceError: Cannot access 'multiply' before initialization - 提示:想想函数表达式会不会提升?
作业:做一个「个人财务小管家」
需求描述:做一个命令行工具,帮你记录收入和支出,计算余额。
功能点:
1. addRecord(type, amount, description) - 添加收支记录
2. showBalance() - 显示当前余额
3. showRecords() - 显示所有记录
4. getSummary() - 按类型统计总额
加分项:
1. 数据持久化到 LocalStorage(浏览器环境)
2. 支持按月份筛选记录
验收标准:
- 代码能跑起来
- 输出符合预期
- 每个函数有注释说明参数和返回值
📚 总结
这一章我们学了 3 个核心点:
- 函数声明
function fn(){}vs 函数表达式const fn = function(){}—— 前者会提升,后者不会 - 参数默认值 和 剩余参数
...args—— 让函数更灵活 - 函数组合 —— 把小函数组合成大功能,像搭积木一样
下一章我们要解决一个老大难问题:箭头函数里的 this 到底指向谁?写面向对象代码时,这个坑 99% 的人都踩过……
推荐资源:
- MDN 官方文档:Functions - JavaScript | MDN
- 视频:JavaScript 函数完全指南(B 站有搬运)
互动钩子:你在工作中有没有遇到过「函数提升」或者「this 绑定」导致的 bug?评论区聊聊,老粉优先回复!

评论(0)