主题
第 2 章:函数和作用域
在本章中,你将深入学习 JavaScript 中最重要的概念之一——函数。你将理解函数的多种定义方式、作用域规则、闭包原理以及 this 关键字的绑定机制。
📖 本章内容
1. 函数基础
1.1 为什么需要函数?
函数是可重用的代码块,它帮助我们:
- 避免重复代码 - 写一次,到处使用
- 组织代码 - 将相关逻辑组合在一起
- 提高可维护性 - 修改一处即可影响所有调用
- 抽象复杂性 - 隐藏实现细节,暴露简洁接口
1.2 函数声明
👑 The Metaphor: First-Class Citizens (一等公民) 在 JavaScript 的国度里,函数是一等公民。 这意味着函数和数字、字符串一样,拥有“人权”:
- 可以被赋值给变量(有了名字)。
- 可以作为参数传递给别的函数(可以去旅行)。
- 可以作为返回值被返回(可以生孩子)。
在 Java (pre-8) 等语言里,函数是二等公民,必须依附于类(Class)才能生存。 JS 的这种特性赋予了它极大的灵活性,也是函数式编程 (Functional Programming) 的基石。
javascript
/**
* 基本函数声明
* 功能: 问候用户
*/
// 日志: 定义函数
console.log('=== 函数声明示例 ===\n');
// 无参数函数
function sayHello() {
console.log('Hello, World!');
}
// 调用函数
sayHello(); // 输出: Hello, World!
// 带参数的函数
function greet(name) {
console.log(`Hello, ${name}!`);
}
greet('Alice'); // 输出: Hello, Alice!
greet('Bob'); // 输出: Hello, Bob!
// 带返回值的函数
function add(a, b) {
return a + b;
}
const sum = add(5, 3);
console.log(`5 + 3 = ${sum}`); // 输出: 5 + 3 = 8
// 多个参数的函数
function calculateRectangleArea(width, height) {
console.log(`计算矩形面积: 宽=${width}, 高=${height}`);
const area = width * height;
console.log(`面积 = ${area}`);
return area;
}
const area = calculateRectangleArea(10, 5);
// 输出:
// 计算矩形面积: 宽=10, 高=5
// 面积 = 501.3 函数的重要特性
🕰️ The Sci-Fi: Hoisting as Time Travel 再次见到我们的老朋友 "Hoisting"。函数声明会被完整提升到顶部。 这就像是时间旅行:你可以在函数出生(定义)之前就调用它。
- JS 引擎: "我不仅活在当下,我还拥有未来的记忆。我知道你在第 100 行定义了
doSomething,所以在第 1 行我就能执行它。"- C++ 编译器: "Undeclared identifier? 滚去排队!"
这虽然方便了代码组织(把杂乱的辅助函数扔到文件底部),但也破坏了线性阅读的逻辑。能力越大,责任越大。
javascript
/**
* 函数的重要特性演示
*/
console.log('=== 函数特性 ===\n');
// 1. 函数可以在声明前调用(函数提升)
console.log('调用 hoistedFunction:');
hoistedFunction(); // 输出: 我被提升了!
function hoistedFunction() {
console.log('我被提升了!');
}
// 2. 函数可以接收默认参数(ES6+)
function greetWithDefault(name = 'Guest') {
console.log(`Hello, ${name}!`);
}
greetWithDefault('Alice'); // 输出: Hello, Alice!
greetWithDefault(); // 输出: Hello, Guest!
// 3. 函数可以返回多个值(通过数组或对象)
function getMinMax(numbers) {
const min = Math.min(...numbers);
const max = Math.max(...numbers);
// 返回对象
return { min, max };
}
const result = getMinMax([3, 7, 2, 9, 1]);
console.log(`最小值: ${result.min}, 最大值: ${result.max}`);
// 输出: 最小值: 1, 最大值: 9
// 4. 函数可以不返回值(返回 undefined)
function logMessage(message) {
console.log(`日志: ${message}`);
// 没有 return 语句
}
const returnValue = logMessage('测试消息');
console.log(`返回值: ${returnValue}`);
// 输出:
// 日志: 测试消息
// 返回值: undefined2. 函数的多种形式
2.1 函数表达式
javascript
/**
* 函数表达式
* 将函数赋值给变量
*/
console.log('=== 函数表达式 ===\n');
// 函数表达式
const multiply = function(a, b) {
return a * b;
};
console.log(`3 * 4 = ${multiply(3, 4)}`); // 输出: 3 * 4 = 12
// ⚠️ 函数表达式不会被提升
// console.log(notHoisted()); // ❌ 错误: notHoisted is not defined
const notHoisted = function() {
return '我不会被提升';
};
// 命名函数表达式(可用于递归和调试)
const factorial = function fact(n) {
console.log(`计算阶乘: ${n}`);
if (n <= 1) return 1;
return n * fact(n - 1); // 递归调用自己
};
console.log(`5! = ${factorial(5)}`); // 输出: 5! = 1202.2 箭头函数
javascript
/**
* 箭头函数 (ES6+)
* 更简洁的函数语法
*/
console.log('=== 箭头函数 ===\n');
// 传统函数
const addTraditional = function(a, b) {
return a + b;
};
// 箭头函数 - 完整形式
const addArrow = (a, b) => {
return a + b;
};
// 箭头函数 - 简化形式(单行返回)
const addSimple = (a, b) => a + b;
console.log(`传统函数: ${addTraditional(5, 3)}`); // 8
console.log(`箭头函数: ${addArrow(5, 3)}`); // 8
console.log(`简化箭头: ${addSimple(5, 3)}`); // 8
// 单个参数可以省略括号
const square = n => n * n;
console.log(`5 的平方: ${square(5)}`); // 输出: 5 的平方: 25
// 无参数需要空括号
const getRandomNumber = () => Math.random();
console.log(`随机数: ${getRandomNumber()}`);
// 返回对象需要用括号包裹
const createPerson = (name, age) => ({ name, age });
const person = createPerson('Alice', 25);
console.log('创建的对象:', person);
// 输出: 创建的对象: { name: 'Alice', age: 25 }
// 箭头函数在数组方法中的应用
const numbers = [1, 2, 3, 4, 5];
// 传统写法
const doubled1 = numbers.map(function(n) {
return n * 2;
});
// 箭头函数写法
const doubled2 = numbers.map(n => n * 2);
console.log('加倍后的数组:', doubled2);
// 输出: 加倍后的数组: [ 2, 4, 6, 8, 10 ]2.3 函数声明 vs 表达式 vs 箭头函数
javascript
/**
* 三种函数形式的对比
*/
console.log('=== 函数形式对比 ===\n');
// 1. 函数声明 - Function Declaration
function declarationFunc() {
return '函数声明';
}
// 2. 函数表达式 - Function Expression
const expressionFunc = function() {
return '函数表达式';
};
// 3. 箭头函数 - Arrow Function
const arrowFunc = () => '箭头函数';
console.log('调用结果:');
console.log('- 声明:', declarationFunc());
console.log('- 表达式:', expressionFunc());
console.log('- 箭头:', arrowFunc());
// 关键区别汇总
console.log('\n区别汇总:');
console.log('1. 提升: 只有函数声明会被提升');
console.log('2. this: 箭头函数没有自己的 this');
console.log('3. arguments: 箭头函数没有 arguments 对象');
console.log('4. 构造函数: 箭头函数不能用作构造函数');3. 作用域
3.1 什么是作用域?
🏰 The Metaphor: The Castle Walls 作用域(Scope)就是代码世界里的城墙。
- 全局作用域: 荒野。谁都可以在这里跑,但也最危险(变量污染、命名冲突)。
- 函数作用域: 城堡。外面的人(外部代码)看不见城堡里有什么(私有变量),但城堡里的人可以看见外面的荒野。
- 块级作用域: 房间。
let和const让你可以在城堡里再建一个个带锁的小房间。最小权限原则 (Least Privilege): 尽量把变量关在最小的房间里。不要让它们在荒野上裸奔。
作用域(Scope)决定了变量的可访问范围。JavaScript 有三种作用域:
- 全局作用域 - 在代码任何地方都可以访问
- 函数作用域 - 只在函数内部可以访问
- 块级作用域 - 只在 {} 块内部可以访问(let/const)
3.2 全局作用域
javascript
/**
* 全局作用域
* 在任何地方都可以访问的变量
*/
console.log('=== 全局作用域 ===\n');
// 全局变量
const globalVar = '我是全局变量';
function accessGlobal() {
console.log(`函数内访问: ${globalVar}`);
}
accessGlobal(); // 输出: 函数内访问: 我是全局变量
console.log(`函数外访问: ${globalVar}`); // 输出: 函数外访问: 我是全局变量
// ⚠️ 避免过多使用全局变量,容易造成命名冲突3.3 函数作用域
javascript
/**
* 函数作用域
* 变量只在函数内部可见
*/
console.log('=== 函数作用域 ===\n');
function myFunction() {
const localVar = '我是局部变量';
console.log(`函数内访问: ${localVar}`);
}
myFunction(); // 输出: 函数内访问: 我是局部变量
// console.log(localVar); // ❌ 错误: localVar is not defined
// 内部函数可以访问外部函数的变量
function outerFunction() {
const outerVar = '外部变量';
function innerFunction() {
console.log(`内部函数访问外部变量: ${outerVar}`);
}
innerFunction(); // 输出: 内部函数访问外部变量: 外部变量
}
outerFunction();3.4 块级作用域
javascript
/**
* 块级作用域
* let 和 const 具有块级作用域
*/
console.log('=== 块级作用域 ===\n');
// if 块
if (true) {
const blockVar = '块级变量';
let blockLet = '块级 let';
var functionVar = '函数级 var';
console.log(`块内访问 const: ${blockVar}`);
console.log(`块内访问 let: ${blockLet}`);
}
// console.log(blockVar); // ❌ 错误: blockVar is not defined
// console.log(blockLet); // ❌ 错误: blockLet is not defined
console.log(`块外访问 var: ${functionVar}`); // ✅ 可以访问(var 没有块级作用域)
// for 循环块
console.log('\nfor 循环作用域:');
for (let i = 0; i < 3; i++) {
console.log(`循环内 i = ${i}`);
}
// console.log(i); // ❌ 错误: i is not defined
// ⚠️ var 在循环中的问题
console.log('\nvar 在循环中的问题:');
for (var j = 0; j < 3; j++) {
// j 是函数作用域,不是块级作用域
}
console.log(`循环外 j = ${j}`); // ✅ 可以访问,j = 33.5 作用域链 (The Scope Chain)
🎭 The Metaphor: The Russian Matryoshka Dolls 作用域链就像是一套俄罗斯套娃。
- 最里面的娃娃(当前函数)想找一个变量。
- 如果自己肚子里没有,它就打开外面一层的娃娃(父级函数)去找。
- 如果还没有,就再往外找...
- 直到最大的那个娃娃(全局作用域 Global Scope)。
- 如果连最大的娃娃里都没有,JS 引擎就会抛出一个
ReferenceError:"在这个宇宙里,我不认识这个名字。"
🎓 CS Master's Bridge: 词法环境链 (Lexical Environment Chain)
别被“作用域链”这个前端术语迷惑了。在编译原理中,这实际上是一个 Linked List of Lexical Environments。
- 每个执行上下文 (Execution Context) 都有一个
outer指针,指向其父级词法环境。- 变量查找过程本质上就是沿着这个链表进行 O(N) 的线性搜索。
- 全局作用域 (Global Scope) 是链表的尾节点 (Tail),其
outer指针为null。🔧 Under the Hood (V8 Context Allocation): 通常,局部变量存储在 Stack 上 (高效)。但闭包打破了这一点。 V8 在编译阶段进行 Scope Analysis (Escape Analysis)。
- 如果一个变量被内部函数引用(逃逸),V8 就不会把它放在栈帧上,而是分配到一个堆对象 Context 中。
- 这个 Context 对象通过
outer指针链连接。- 这就是为什么闭包比普通函数慢:变量访问从 Stack Offset 变成了 Heap Lookup (甚至是指针追踪)。
javascript
/**
* 作用域链
* JavaScript 查找变量的机制
*/
console.log('=== 作用域链 ===\n');
const globalVar = 'global';
function level1() {
const level1Var = 'level1';
console.log('level1 函数:');
console.log(` 访问 globalVar: ${globalVar}`); // ✅ 找到全局变量
console.log(` 访问 level1Var: ${level1Var}`); // ✅ 找到当前作用域
function level2() {
const level2Var = 'level2';
console.log('\nlevel2 函数:');
console.log(` 访问 globalVar: ${globalVar}`); // ✅ 向上查找到全局
console.log(` 访问 level1Var: ${level1Var}`); // ✅ 向上查找到 level1
console.log(` 访问 level2Var: ${level2Var}`); // ✅ 找到当前作用域
function level3() {
console.log('\nlevel3 函数:');
console.log(` 访问 globalVar: ${globalVar}`); // ✅ 向上查找到全局
console.log(` 访问 level1Var: ${level1Var}`); // ✅ 向上查找到 level1
console.log(` 访问 level2Var: ${level2Var}`); // ✅ 向上查找到 level2
}
level3();
}
level2();
}
level1();
// 查找规则: 当前作用域 → 上层作用域 → ... → 全局作用域
// 如果找不到,报错: ReferenceError4. 闭包
4.1 什么是闭包? (The "Magic" Revealed)
🎒 The Metaphor: The Backpacker 想象函数是一个背包客。 当函数被创建时,它不仅仅是代码,它还背了一个隐形的背包(Closure)。 这个背包里装满了它出生时周围环境里的所有变量。 无论这个背包客走到哪里(被传到哪里执行),它都可以随时打开背包,拿出那些原本应该已经消失的变量。 这就是闭包:函数 + 它随身携带的背包。
🎬 Pop Culture: Inception (盗梦空间) 闭包就像是《盗梦空间》里的层层梦境。 你在深层梦境(内部函数)里,依然记得上一层梦境(外部函数)里留下的图腾(变量)。 即使上一层梦境已经坍塌(外部函数执行完毕),那个图腾依然在你的口袋里,真实存在。
🎓 CS Master's Bridge: 栈帧持久化 (Stack Frame Persistence)
在 C/C++ 中,函数返回后,其栈帧 (Stack Frame) 会被立即销毁 (Popped off)。如果你试图返回一个指向栈局部变量的指针,你会得到一个悬垂指针 (Dangling Pointer) —— 这是未定义行为 (UB)。
但在 JavaScript 中,闭包打破了这个规则。
闭包本质上是:一个函数加上它被创建时的词法环境 (Lexical Environment)。 当一个内部函数引用了外部函数的变量时,JS 引擎(如 V8)检测到这种“逃逸” (Escape Analysis),它就不会把这些变量分配在栈上,而是分配在堆 (Heap) 上。
所以,闭包就是 Heap-allocated Scope。即使外部函数执行完毕,这个堆内存依然存在,直到内部函数被 GC 回收。
**闭包(Closure)**是指函数能够记住并访问其词法作用域,即使函数在其词法作用域之外执行。
简单说:内部函数引用了外部函数的变量,即使外部函数已经执行完毕。
4.2 基本闭包示例
javascript
/**
* 基本闭包
* 内部函数访问外部函数的变量
*/
console.log('=== 基本闭包 ===\n');
function createCounter() {
let count = 0; // 外部函数的变量
console.log('创建计数器,初始值:', count);
// 返回一个内部函数
return function() {
count++; // 访问外部函数的变量
console.log(`当前计数: ${count}`);
return count;
};
}
// 创建计数器实例
const counter1 = createCounter();
console.log('\n使用 counter1:');
counter1(); // 输出: 当前计数: 1
counter1(); // 输出: 当前计数: 2
counter1(); // 输出: 当前计数: 3
// 创建另一个独立的计数器
const counter2 = createCounter();
console.log('\n使用 counter2:');
counter2(); // 输出: 当前计数: 1
counter2(); // 输出: 当前计数: 2
// counter1 和 counter2 是独立的,各自维护自己的 count
console.log('\n再次使用 counter1:');
counter1(); // 输出: 当前计数: 44.3 闭包的实际应用
应用1: 数据私有化
javascript
/**
* 使用闭包实现数据私有化
* 外部无法直接访问内部变量
*/
console.log('=== 数据私有化 ===\n');
function createBankAccount(initialBalance) {
let balance = initialBalance; // 私有变量
console.log(`创建账户,初始余额: ¥${balance}`);
return {
// 存款方法
deposit(amount) {
if (amount > 0) {
balance += amount;
console.log(`存入 ¥${amount},当前余额: ¥${balance}`);
} else {
console.log('❌ 存款金额必须大于0');
}
},
// 取款方法
withdraw(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
console.log(`取出 ¥${amount},当前余额: ¥${balance}`);
} else {
console.log('❌ 余额不足或金额无效');
}
},
// 查询余额方法
getBalance() {
console.log(`当前余额: ¥${balance}`);
return balance;
}
};
}
const account = createBankAccount(1000);
console.log('');
account.deposit(500); // 存入 ¥500,当前余额: ¥1500
account.withdraw(200); // 取出 ¥200,当前余额: ¥1300
account.getBalance(); // 当前余额: ¥1300
// ⚠️ 无法直接访问 balance 变量
// console.log(account.balance); // undefined应用2: 函数工厂
javascript
/**
* 使用闭包创建函数工厂
* 根据参数生成定制化的函数
*/
console.log('\n=== 函数工厂 ===\n');
function createMultiplier(multiplier) {
console.log(`创建乘法器,倍数: ${multiplier}`);
return function(number) {
const result = number * multiplier;
console.log(`${number} × ${multiplier} = ${result}`);
return result;
};
}
// 创建不同的乘法器
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log('\n使用 double:');
double(5); // 5 × 2 = 10
double(10); // 10 × 2 = 20
console.log('\n使用 triple:');
triple(5); // 5 × 3 = 15
triple(10); // 10 × 3 = 30应用3: 事件处理器
javascript
/**
* 在事件处理器中使用闭包
* 保存事件发生时的上下文信息
*/
console.log('\n=== 事件处理器中的闭包 ===\n');
function createButtonHandler(buttonName) {
let clickCount = 0;
console.log(`为按钮 "${buttonName}" 创建处理器`);
return function() {
clickCount++;
console.log(`按钮 "${buttonName}" 被点击了 ${clickCount} 次`);
};
}
// 模拟按钮点击
const loginButton = createButtonHandler('登录');
const submitButton = createButtonHandler('提交');
console.log('\n模拟点击:');
loginButton(); // 按钮 "登录" 被点击了 1 次
loginButton(); // 按钮 "登录" 被点击了 2 次
submitButton(); // 按钮 "提交" 被点击了 1 次
loginButton(); // 按钮 "登录" 被点击了 3 次4.4 闭包的注意事项
javascript
/**
* 闭包的常见陷阱
*/
console.log('\n=== 闭包陷阱 ===\n');
// ⚠️ 陷阱1: 循环中的闭包
console.log('陷阱1: 使用 var 在循环中创建闭包');
const functions1 = [];
for (var i = 0; i < 3; i++) {
functions1.push(function() {
console.log(` i = ${i}`);
});
}
console.log('调用函数:');
functions1[0](); // i = 3 (不是 0!)
functions1[1](); // i = 3 (不是 1!)
functions1[2](); // i = 3 (不是 2!)
// ✅ 解决方案1: 使用 let
console.log('\n解决方案1: 使用 let');
const functions2 = [];
for (let j = 0; j < 3; j++) {
functions2.push(function() {
console.log(` j = ${j}`);
});
}
console.log('调用函数:');
functions2[0](); // j = 0 ✅
functions2[1](); // j = 1 ✅
functions2[2](); // j = 2 ✅
// ✅ 解决方案2: 使用 IIFE(立即执行函数)
console.log('\n解决方案2: 使用 IIFE');
const functions3 = [];
for (var k = 0; k < 3; k++) {
functions3.push((function(index) {
return function() {
console.log(` index = ${index}`);
};
})(k));
}
console.log('调用函数:');
functions3[0](); // index = 0 ✅
functions3[1](); // index = 1 ✅
functions3[2](); // index = 2 ✅5. this 关键字 (The Most Misunderstood Feature)
5.1 什么是 this?
🎭 The Drama: The Identity Crisis
this是 JavaScript 中患有严重身份认知障碍的角色。 在其他语言(如 Java/C++)中,this忠诚地指向实例本身。 但在 JS 中,this是一个墙头草。它不看自己出生在哪里,只看谁在调用它。
- "是
obj叫我吗?那我就是obj。"- "没人叫我?那我就是
Global(或者在严格模式下消失为undefined)。"- "是
new叫我吗?那我就是一个全新的对象。"这种设计虽然灵活(Dynamic Binding),但也导致了无数的 Bug 和脱发。它是 JS 最大的设计缺陷之一,也是设计特性之一。
🎭 WTFJS / CS Contrast:
- C++/Java:
this是一个 Instance Pointer,永远指向当前对象实例。它是在编译时确定的。- JavaScript:
this是一个 Runtime Binding (运行时绑定)。它不取决于函数在哪里定义,而完全取决于 函数是如何被调用的 (Call Site)。这就是为什么前端开发者经常会问:"Wait, what is
thisnow?" 把它想象成 C 语言中,每个函数都隐式接收一个void* context参数。你传什么,它就是什么。如果你不传,它可能就是全局对象(或者 undefined)。
this 关键字指向函数执行时的上下文对象。它的值取决于函数如何被调用,而不是在哪里定义。
5.2 this 的四种绑定规则
javascript
/**
* this 的绑定规则
*/
console.log('=== this 绑定规则 ===\n');
// 规则1: 默认绑定 - 独立函数调用
console.log('规则1: 默认绑定');
function showThis1() {
console.log(' this:', this); // 严格模式下是 undefined,非严格模式下是 global
}
showThis1();
// 规则2: 隐式绑定 - 对象方法调用
console.log('\n规则2: 隐式绑定');
const person = {
name: 'Alice',
greet: function() {
console.log(` Hello, I'm ${this.name}`);
}
};
person.greet(); // this 指向 person,输出: Hello, I'm Alice
// 规则3: 显式绑定 - 使用 call/apply/bind
console.log('\n规则3: 显式绑定');
function introduce(greeting) {
console.log(` ${greeting}, I'm ${this.name}`);
}
const user = { name: 'Bob' };
introduce.call(user, 'Hi'); // Hi, I'm Bob
introduce.apply(user, ['Hey']); // Hey, I'm Bob
const boundIntroduce = introduce.bind(user);
boundIntroduce('Hello'); // Hello, I'm Bob
// 规则4: new 绑定 - 构造函数调用
console.log('\n规则4: new 绑定');
function Person(name) {
this.name = name;
console.log(` 创建 Person,this.name = ${this.name}`);
}
const alice = new Person('Alice'); // this 指向新创建的对象5.3 箭头函数中的 this
🛡️ Engineering Mindset: 修复
this的设计缺陷箭头函数 (
=>) 不仅仅是简写。它的核心目的是修复this的动态绑定问题。 箭头函数没有自己的this,它通过 Lexical Scoping (词法作用域) 捕获外层的this。
- 普通函数:
this取决于调用方式 (Dynamic)。- 箭头函数:
this取决于定义位置 (Lexical)。最佳实践: 在回调函数(如
setTimeout,map,onClick)中,永远优先使用箭头函数,除非你真的需要动态this。
javascript
/**
* 箭头函数的 this
* 箭头函数没有自己的 this,它继承外层作用域的 this
*/
console.log('\n=== 箭头函数的 this ===\n');
const obj = {
name: 'Object',
// 普通函数
regularFunction: function() {
console.log('普通函数 this.name:', this.name); // 'Object'
},
// 箭头函数
arrowFunction: () => {
console.log('箭头函数 this.name:', this.name); // undefined(继承外层)
},
// 方法中的箭头函数
methodWithArrow: function() {
const arrow = () => {
console.log('方法内箭头函数 this.name:', this.name); // 'Object'
};
arrow();
}
};
obj.regularFunction(); // 普通函数 this.name: Object
obj.arrowFunction(); // 箭头函数 this.name: undefined
obj.methodWithArrow(); // 方法内箭头函数 this.name: Object5.4 this 的实际应用
javascript
/**
* this 在实际开发中的应用
*/
console.log('\n=== this 实际应用 ===\n');
// 示例: 倒计时器
function CountdownTimer(seconds) {
this.seconds = seconds;
console.log(`创建倒计时器: ${seconds} 秒\n`);
// ❌ 使用普通函数会导致 this 丢失
// setInterval(function() {
// this.seconds--; // this 不指向 CountdownTimer!
// console.log(this.seconds);
// }, 1000);
// ✅ 解决方案1: 使用箭头函数
this.start = function() {
console.log('开始倒计时(使用箭头函数):');
const interval = setInterval(() => {
this.seconds--;
console.log(` 剩余: ${this.seconds} 秒`);
if (this.seconds === 0) {
console.log(' 倒计时结束!\n');
clearInterval(interval);
}
}, 1000);
};
// ✅ 解决方案2: 保存 this 引用
this.startWithSelf = function() {
console.log('开始倒计时(保存 this):');
const self = this; // 保存 this 引用
const interval = setInterval(function() {
self.seconds--;
console.log(` 剩余: ${self.seconds} 秒`);
if (self.seconds === 0) {
console.log(' 倒计时结束!\n');
clearInterval(interval);
}
}, 1000);
};
}
// 创建倒计时器(示例,不实际运行)
// const timer = new CountdownTimer(5);
// timer.start();6. 高阶函数
6.1 什么是高阶函数?
高阶函数是指:
- 接收函数作为参数,或
- 返回一个函数
的函数。
6.2 接收函数作为参数
javascript
/**
* 接收函数作为参数的高阶函数
*/
console.log('=== 接收函数作为参数 ===\n');
// 示例: 通用的数组处理函数
function processArray(arr, callback) {
console.log(`处理数组: [${arr}]`);
const result = [];
for (const item of arr) {
result.push(callback(item));
}
return result;
}
const numbers = [1, 2, 3, 4, 5];
// 使用不同的回调函数
const doubled = processArray(numbers, n => n * 2);
console.log('加倍结果:', doubled);
// 输出: 加倍结果: [ 2, 4, 6, 8, 10 ]
const squared = processArray(numbers, n => n * n);
console.log('平方结果:', squared);
// 输出: 平方结果: [ 1, 4, 9, 16, 25 ]6.3 返回函数
javascript
/**
* 返回函数的高阶函数
*/
console.log('\n=== 返回函数 ===\n');
// 示例: 创建条件检查器
function createChecker(condition) {
console.log(`创建检查器: ${condition}`);
return function(value) {
const result = condition(value);
console.log(` 检查 ${value}: ${result ? '✅ 通过' : '❌ 失败'}`);
return result;
};
}
// 创建不同的检查器
const isEven = createChecker(n => n % 2 === 0);
const isPositive = createChecker(n => n > 0);
console.log('\n使用 isEven:');
isEven(4); // ✅ 通过
isEven(5); // ❌ 失败
console.log('\n使用 isPositive:');
isPositive(10); // ✅ 通过
isPositive(-5); // ❌ 失败6.4 常用的高阶函数
javascript
/**
* 数组的常用高阶函数
*/
console.log('\n=== 数组高阶函数 ===\n');
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log('原数组:', numbers);
console.log('');
// 1. map - 映射转换
const doubled = numbers.map(n => n * 2);
console.log('map(加倍):', doubled);
// 2. filter - 过滤筛选
const evens = numbers.filter(n => n % 2 === 0);
console.log('filter(偶数):', evens);
// 3. reduce - 归约计算
const sum = numbers.reduce((total, n) => {
console.log(` 累加: ${total} + ${n} = ${total + n}`);
return total + n;
}, 0);
console.log('reduce(求和):', sum);
// 4. find - 查找第一个符合条件的元素
const firstEven = numbers.find(n => n % 2 === 0);
console.log('\nfind(第一个偶数):', firstEven);
// 5. every - 检查是否所有元素都符合条件
const allPositive = numbers.every(n => n > 0);
console.log('every(都是正数):', allPositive);
// 6. some - 检查是否有元素符合条件
const hasEven = numbers.some(n => n % 2 === 0);
console.log('some(有偶数):', hasEven);
// 链式调用
console.log('\n链式调用示例:');
const result = numbers
.filter(n => n % 2 === 0) // 筛选偶数
.map(n => n * n) // 求平方
.reduce((sum, n) => sum + n, 0); // 求和
console.log('偶数的平方和:', result); // 2207. 最佳实践
7.1 函数设计原则
javascript
/**
* 函数设计最佳实践
*/
console.log('=== 函数设计原则 ===\n');
// ✅ 原则1: 单一职责 - 一个函数只做一件事
function validateEmail(email) {
return email.includes('@');
}
function sendEmail(email) {
if (validateEmail(email)) {
console.log(`发送邮件到: ${email}`);
}
}
// ❌ 避免: 一个函数做太多事
function badFunction(email) {
// 验证、发送、记录...太多职责
}
// ✅ 原则2: 函数应该有清晰的输入和输出
function calculateTotal(price, quantity) {
return price * quantity;
}
// ✅ 原则3: 使用描述性的函数名
function getUserFullName(firstName, lastName) {
return `${firstName} ${lastName}`;
}
// ❌ 避免: 使用模糊的名称
function process(a, b) {
return a + b;
}
// ✅ 原则4: 函数参数不要过多(建议 ≤ 3 个)
// 如果参数很多,使用对象
function createUser(options) {
const { name, age, email, phone } = options;
console.log(`创建用户: ${name}`);
return { name, age, email, phone };
}
createUser({
name: 'Alice',
age: 25,
email: 'alice@example.com',
phone: '123-456-7890'
});7.2 纯函数
💎 The Philosophy: The Platonic Ideal (柏拉图理念) 纯函数 (Pure Function) 是代码世界里的数学公式。
- 无副作用: 它不修改外部世界,不读写文件,不修改全局变量。它就像一个与世隔绝的隐士。
- 引用透明: 只要输入是
2,输出永远是4。它不依赖心情、天气或时间。在充满不确定性(网络延迟、用户输入、硬件故障)的现实世界中,纯函数是我们唯一能掌控的绝对真理。 尽可能多写纯函数,把副作用(脏活累活)推到系统的边缘。
javascript
/**
* 纯函数
* 相同输入总是产生相同输出,且没有副作用
*/
console.log('\n=== 纯函数 ===\n');
// ✅ 纯函数示例
function add(a, b) {
return a + b; // 相同输入,相同输出,无副作用
}
console.log('纯函数 add(2, 3):', add(2, 3)); // 总是 5
// ❌ 非纯函数示例
let counter = 0;
function impureIncrement() {
counter++; // 修改外部变量,有副作用
return counter;
}
console.log('非纯函数调用1:', impureIncrement()); // 1
console.log('非纯函数调用2:', impureIncrement()); // 2 (不同结果)
// ✅ 改进为纯函数
function pureIncrement(value) {
return value + 1; // 不修改外部状态
}
console.log('纯函数 pureIncrement(0):', pureIncrement(0)); // 1
console.log('纯函数 pureIncrement(0):', pureIncrement(0)); // 1 (相同结果)7.3 何时使用箭头函数
javascript
/**
* 箭头函数使用场景
*/
console.log('\n=== 箭头函数使用场景 ===\n');
// ✅ 适合使用箭头函数的场景
// 1. 数组方法
const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2);
console.log('数组方法:', doubled);
// 2. 简短的回调函数
setTimeout(() => console.log('定时器回调'), 0);
// 3. 需要保持 this 绑定
const obj = {
name: 'Object',
delayedGreet: function() {
setTimeout(() => {
console.log(`Hello from ${this.name}`); // this 指向 obj
}, 0);
}
};
obj.delayedGreet();
// ❌ 不适合使用箭头函数的场景
// 1. 对象方法(需要访问 this)
const person = {
name: 'Alice',
greet: function() {
console.log(`Hi, I'm ${this.name}`); // ✅ 正常工作
}
// greet: () => {
// console.log(`Hi, I'm ${this.name}`); // ❌ this 不指向 person
// }
};
person.greet();
// 2. 构造函数
// const Person = (name) => {
// this.name = name; // ❌ 箭头函数不能用作构造函数
// };
// 3. 需要 arguments 对象的函数
function regularFunc() {
console.log('arguments:', arguments); // ✅ 可以访问 arguments
}
regularFunc(1, 2, 3);8. 常见陷阱
8.1 作用域陷阱
javascript
/**
* 作用域相关陷阱
*/
console.log('\n=== 作用域陷阱 ===\n');
// ⚠️ 陷阱1: 变量提升
console.log('陷阱1: 变量提升');
console.log(' hoistedVar:', hoistedVar); // undefined(不是报错!)
var hoistedVar = 'I am hoisted';
// ✅ 避免方法: 使用 let/const
// console.log(notHoisted); // ❌ 报错: Cannot access before initialization
// let notHoisted = 'Not hoisted';
// ⚠️ 陷阱2: 意外创建全局变量
console.log('\n陷阱2: 意外的全局变量');
function createGlobal() {
accidentalGlobal = '我是全局的'; // 忘记声明,变成全局变量!
}
createGlobal();
console.log(' accidentalGlobal:', accidentalGlobal);
// ✅ 避免方法: 始终声明变量,使用严格模式
function avoidGlobal() {
'use strict';
// undeclared = 'error'; // ❌ 严格模式下会报错
const declared = 'safe';
console.log(' declared:', declared);
}
avoidGlobal();8.2 闭包陷阱 (The Loop Problem)
🐛 Debugging Challenge: 共享的词法环境
这是一个经典的面试题,也是无数初学者的噩梦。 问题的根源在于:
var只有一个函数作用域。 在循环中,虽然你创建了 3 个闭包,但它们共享了同一个变量i的引用(即同一个词法环境)。 当setTimeout回调执行时,循环早已结束,i已经变成了 3。修复原理: 使用
let会为每次循环迭代创建一个新的词法环境 (New Lexical Environment)。每个闭包捕获的是属于它自己的那个i。
javascript
/**
* 闭包相关陷阱
*/
console.log('\n=== 闭包陷阱 ===\n');
// ⚠️ 陷阱: 循环中的闭包(之前已经演示过)
console.log('循环中的 var 陷阱:');
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(` i = ${i}`); // 都是 3!
}, 0);
}
// ✅ 解决: 使用 let
console.log('\n使用 let 解决:');
for (let j = 0; j < 3; j++) {
setTimeout(function() {
console.log(` j = ${j}`); // 0, 1, 2
}, 0);
}8.3 this 陷阱
javascript
/**
* this 绑定陷阱
*/
console.log('\n=== this 陷阱 ===\n');
const user = {
name: 'Alice',
greet: function() {
console.log(` Hello, ${this.name}`);
}
};
// ⚠️ 陷阱: this 丢失
console.log('陷阱: 赋值后 this 丢失');
const greetFunction = user.greet;
// greetFunction(); // ❌ this 不再指向 user
// ✅ 解决方案: 使用 bind
const boundGreet = user.greet.bind(user);
boundGreet(); // ✅ 正常工作
// 或者使用箭头函数
const user2 = {
name: 'Bob',
greet: function() {
const arrowGreet = () => {
console.log(` Hello, ${this.name}`);
};
return arrowGreet;
}
};
const greet2 = user2.greet();
greet2(); // ✅ 正常工作9. 章节练习
练习1: 函数基础
编写一个函数 calculateBMI(weight, height),计算并返回 BMI 指数。
- BMI = 体重(kg) / 身高²(m²)
- 返回一个对象,包含 BMI 值和分类(偏瘦/正常/偏胖/肥胖)
<details> <summary>查看答案</summary>
javascript
function calculateBMI(weight, height) {
console.log(`计算 BMI: 体重=${weight}kg, 身高=${height}m`);
// 计算 BMI
const bmi = weight / (height * height);
// 判断分类
let category;
if (bmi < 18.5) {
category = '偏瘦';
} else if (bmi < 24) {
category = '正常';
} else if (bmi < 28) {
category = '偏胖';
} else {
category = '肥胖';
}
const result = {
bmi: bmi.toFixed(2),
category: category
};
console.log(`BMI: ${result.bmi}, 分类: ${result.category}`);
return result;
}
// 测试
calculateBMI(70, 1.75); // BMI: 22.86, 分类: 正常</details>
练习2: 闭包应用
实现一个 createIDGenerator() 函数,返回一个 ID 生成器:
- 第一次调用返回 1
- 每次调用自动递增
- 提供
reset()方法重置为 1
<details> <summary>查看答案</summary>
javascript
function createIDGenerator() {
let currentID = 0;
console.log('创建 ID 生成器');
return {
next: function() {
currentID++;
console.log(`生成 ID: ${currentID}`);
return currentID;
},
reset: function() {
console.log(`重置 ID 生成器 (之前: ${currentID})`);
currentID = 0;
},
current: function() {
console.log(`当前 ID: ${currentID}`);
return currentID;
}
};
}
// 测试
const idGen = createIDGenerator();
console.log('');
idGen.next(); // 1
idGen.next(); // 2
idGen.next(); // 3
idGen.reset(); // 重置
idGen.next(); // 1</details>
练习3: 高阶函数
实现一个 repeat(n, action) 函数:
n: 重复次数action: 要执行的函数- 执行
action函数 n 次
<details> <summary>查看答案</summary>
javascript
function repeat(n, action) {
console.log(`重复执行 ${n} 次`);
for (let i = 0; i < n; i++) {
console.log(`第 ${i + 1} 次:`);
action(i);
}
}
// 测试
repeat(3, (index) => {
console.log(` 执行任务 ${index + 1}`);
});
// 输出:
// 重复执行 3 次
// 第 1 次:
// 执行任务 1
// 第 2 次:
// 执行任务 2
// 第 3 次:
// 执行任务 3</details>
练习4: 数组方法
给定学生数组:
javascript
const students = [
{ name: 'Alice', score: 85 },
{ name: 'Bob', score: 92 },
{ name: 'Charlie', score: 78 },
{ name: 'David', score: 95 }
];完成以下任务:
- 找出所有分数 ≥ 90 的学生
- 计算所有学生的平均分
- 获取所有学生的名字数组
<details> <summary>查看答案</summary>
javascript
const students = [
{ name: 'Alice', score: 85 },
{ name: 'Bob', score: 92 },
{ name: 'Charlie', score: 78 },
{ name: 'David', score: 95 }
];
console.log('学生列表:', students);
console.log('');
// 1. 找出分数 ≥ 90 的学生
const topStudents = students.filter(s => s.score >= 90);
console.log('优秀学生 (≥90):', topStudents);
// 2. 计算平均分
const average = students.reduce((sum, s) => sum + s.score, 0) / students.length;
console.log('平均分:', average.toFixed(2));
// 3. 获取所有名字
const names = students.map(s => s.name);
console.log('学生名字:', names);</details>
练习5: 综合应用
实现一个 debounce(func, delay) 函数:
- 防抖功能: 只有在停止调用
delay毫秒后才执行func - 用途: 防止频繁触发事件(如搜索框输入)
<details> <summary>查看答案</summary>
javascript
function debounce(func, delay) {
let timeoutId;
console.log(`创建防抖函数,延迟: ${delay}ms`);
return function(...args) {
console.log(`防抖函数被调用,重置定时器`);
// 清除之前的定时器
if (timeoutId) {
clearTimeout(timeoutId);
console.log(' 清除旧定时器');
}
// 设置新的定时器
timeoutId = setTimeout(() => {
console.log(` ${delay}ms 后执行实际函数`);
func.apply(this, args);
}, delay);
};
}
// 测试
const search = debounce((keyword) => {
console.log(` 搜索关键词: "${keyword}"\n`);
}, 500);
// 模拟快速输入
search('a');
search('ab');
search('abc');
// 只有最后一次会在 500ms 后执行</details>
📚 下一步
恭喜你完成了第2章的学习!现在你已经掌握了:
- ✅ 函数的多种定义方式
- ✅ 作用域和作用域链
- ✅ 闭包的原理和应用
- ✅ this 关键字的绑定规则
- ✅ 高阶函数的概念和使用
准备好了吗? 让我们继续学习第3章:对象和数组操作!