第2章 2.3 函数声明与表达式


🎯 开场:为什么你需要一个「 reusable recipe」?

上一章我们学了循环,能让电脑自动干重复的事。比如让电脑帮你打印 100 次「小明今天也要加油」,一句 for i in range(100): print("小明今天也要加油") 就搞定了。

但是问题来了——

如果你让电脑帮你算「小明的语文成绩 + 数学成绩」,第一次你写了一段代码,第二次算「小红」的成绩,你得把代码复制粘贴一遍。第三次算「老王」的成绩,又粘贴一遍。代码越写越长,改一个 bug 要改十几处。

说白了:重复的代码是万恶之源。

函数就是来解决这个问题的——它让你把一段代码「打包」成一个「菜谱」,下次想用直接喊名字就行,不用再抄一遍。

学完这一章,你就能:
- 写出自己的第一个「可复用代码块」
- 搞懂 function 声明和函数表达式到底有啥区别(面试常问!)
- 学会给函数传参数、打包返回值,像搭积木一样组合代码


🧱 基础:函数的两种写法

什么是函数?

\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\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 个核心点:

  1. 函数声明 function fn(){} vs 函数表达式 const fn = function(){} —— 前者会提升,后者不会
  2. 参数默认值剩余参数 ...args —— 让函数更灵活
  3. 函数组合 —— 把小函数组合成大功能,像搭积木一样

下一章我们要解决一个老大难问题:箭头函数里的 this 到底指向谁?写面向对象代码时,这个坑 99% 的人都踩过……


推荐资源
- MDN 官方文档:Functions - JavaScript | MDN
- 视频:JavaScript 函数完全指南(B 站有搬运)

互动钩子:你在工作中有没有遇到过「函数提升」或者「this 绑定」导致的 bug?评论区聊聊,老粉优先回复!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。