Day 8: 作用域与闭包
🎯 学习目标
- 理解 作用域与闭包 的核心概念和工作原理
- 掌握相关语法和最佳实践
- 学会在实际项目中应用 作用域与闭包
💡 核心概念
作用域(Scope)决定了变量的可访问范围,而闭包(Closure)是JavaScript中最强大的特性之一。理解这两个概念对于编写高质量的JavaScript代码至关重要。
关键概念
1. 作用域(Scope)
作用域是变量和函数的可访问范围。JavaScript有三种作用域:
- 全局作用域:在任何函数外声明的变量
let globalVar = "全局变量";
function test() {
console.log(globalVar); // 可以访问
}
- 函数作用域:在函数内声明的变量,只在函数内可访问
function test() {
let localVar = "局部变量";
console.log(localVar); // 可以访问
}
console.log(localVar); // 错误:无法访问
- 块级作用域:let/const在{}块内声明的变量
if (true) {
let blockVar = "块级变量";
console.log(blockVar); // 可以访问
}
console.log(blockVar); // 错误:无法访问
2. 变量提升(Hoisting)
var声明的变量会被提升到函数顶部,但赋值不会提升:
console.log(myVar); // undefined(不是错误)
var myVar = 10;
// 实际执行顺序
var myVar; // 声明提升
console.log(myVar); // undefined
myVar = 10; // 赋值留在原地
let/const不会被提升:
console.log(myLet); // ReferenceError
let myLet = 10;
最佳实践:始终在作用域顶部声明变量,避免混淆。
3. 闭包(Closure)
闭包是函数和其词法环境的组合。简单来说,内部函数可以访问外部函数的变量,即使外部函数已经返回。
闭包示例:
function createCounter() {
let count = 0; // 私有变量
return function() {
count++; // 访问外部变量
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
闭包的用途:
- 数据私有化(模拟私有方法)
- 函数工厂(创建特定功能的函数)
- 维持状态(变量在函数调用间保持)
🎮 示例代码
示例1:作用域链
// 全局变量
let globalVar = "全局";
function outerFunction() {
// 外层函数变量
let outerVar = "外层";
function innerFunction() {
// 内层函数变量
let innerVar = "内层";
console.log(innerVar); // ✅ 可以访问
console.log(outerVar); // ✅ 可以访问
console.log(globalVar); // ✅ 可以访问
}
innerFunction();
console.log(outerVar); // ✅ 可以访问
// console.log(innerVar); // ❌ 错误:无法访问
}
outerFunction();
// console.log(outerVar); // ❌ 错误:无法访问
console.log(globalVar); // ✅ 可以访问
/*
输出:
内层
外层
全局
外层
全局
*/
示例2:闭包实现数据私有化
function createPerson(name) {
// 私有变量
let age = 0;
return {
getName: function() {
return name;
},
getAge: function() {
return age;
},
setAge: function(newAge) {
if (newAge > 0 && newAge < 150) {
age = newAge;
} else {
console.log("无效的年龄");
}
},
incrementAge: function() {
age++;
}
};
}
const person = createPerson("张三");
console.log(person.getName()); // 张三
console.log(person.getAge()); // 0
person.setAge(25);
console.log(person.getAge()); // 25
person.incrementAge();
console.log(person.getAge()); // 26
// 无法直接访问age
console.log(person.age); // undefined
// person.age = -10; // 不会影响内部age
/*
这样实现了数据私有化,外部代码只能通过提供的方法访问和修改数据。
*/
示例3:闭包实现函数工厂
// 函数工厂:创建特定功能的函数
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}
// 创建不同的乘法函数
const double = createMultiplier(2);
const triple = createMultiplier(3);
const quadruple = createMultiplier(4);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(quadruple(5)); // 20
// 实际应用:折扣计算器
function createDiscountCalculator(discountRate) {
return function(price) {
return price * (1 - discountRate);
};
}
const studentDiscount = createDiscountCalculator(0.1); // 9折
const vipDiscount = createDiscountCalculator(0.2); // 8折
console.log(studentDiscount(100)); // 90
console.log(vipDiscount(100)); // 80
示例4:闭包陷阱(循环问题)
// ❌ 错误示例:常见陷阱
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 都输出 3
}, 100);
}
// 原因:var没有块级作用域,所有函数共享同一个i
// ✅ 解决方法1:使用let
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 输出 0, 1, 2
}, 100);
}
// ✅ 解决方法2:使用闭包
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 输出 0, 1, 2
}, 100);
})(i);
}
// ✅ 解决方法3:使用IIFE返回函数
for (var i = 0; i < 3; i++) {
setTimeout(
(function(j) {
return function() {
console.log(j);
};
})(i),
100
);
}
✍️ 练习任务
练习 1: 练习1:作用域测试
分析以下代码的输出:
let x = 10;
function test1() {
let x = 20;
console.log(x);
}
function test2() {
console.log(x);
let x = 30;
}
test1();
test2();
console.log(x);
问题:
- test1() 输出什么?
- test2() 会报错吗?为什么?
- 最后的 console.log(x) 输出什么?
要求:解释每个输出的原因,说明作用域规则。
提示: 注意变量提升和暂时性死区(TDZ)
练习 2: 练习2:实现计数器
使用闭包实现一个计数器模块,要求:
-
提供以下方法:
- increment(): 计数+1
- decrement(): 计数-1
- getCount(): 获取当前计数
- reset(): 重置为0
-
计数值不能为负数
-
提供多个独立的计数器实例
示例用法:
const counter1 = createCounter();
const counter2 = createCounter();
counter1.increment();
counter1.increment();
console.log(counter1.getCount()); // 2
counter2.increment();
console.log(counter2.getCount()); // 1
提示: 使用闭包保护count变量,返回对象包含操作方法
练习 3: 练习3:缓存函数结果
使用闭包实现一个memoize函数,缓存函数的计算结果:
function memoize(fn) {
// 你的代码
}
// 慢速函数
function slowFunction(n) {
console.log("计算中...");
return n * n;
}
const memoized = memoize(slowFunction);
console.log(memoized(5)); // 输出:计算中... 25
console.log(memoized(5)); // 直接输出:25(不计算)
console.log(memoized(10)); // 输出:计算中... 100
console.log(memoized(5)); // 直接输出:25
要求:
- 第一次调用时执行并缓存结果
- 相同参数直接返回缓存结果
- 使用对象存储缓存
提示: 用参数作为key,结果作为value存储在对象中
🎓 今日挑战
今日挑战:实现事件管理器
使用闭包实现一个完整的事件管理器(EventEmitter):
功能要求:
- on(event, callback):注册事件监听器
emitter.on('click', function(data) {
console.log('点击事件:', data);
});
- emit(event, data):触发事件
emitter.emit('click', {x: 100, y: 200});
- off(event, callback):移除事件监听器
emitter.off('click', handler);
- once(event, callback):只触发一次的监听器
emitter.once('init', function() {
console.log('只执行一次');
});
技术要求:
- 使用闭包保护events对象
- 支持同一事件的多个监听器
- once监听器触发后自动移除
- 移除不存在的监听器不报错
测试代码:
const emitter = createEventEmitter();
let count = 0;
emitter.on('test', () => count++);
emitter.on('test', () => count++);
emitter.once('test', () => count += 10);
emitter.emit('test'); // count = 12 (2 + 10)
emitter.emit('test'); // count = 14 (2,once已移除)
扩展功能(可选):
- 支持通配符监听所有事件
- 添加removeAllListeners(event)方法
- 添加listenerCount(event)方法
提示: 使用对象存储事件,key是事件名,value是回调数组
💡 常见问题
Q1: 什么时候使用闭包?
A: 闭包的典型使用场景:
1. 数据私有化:
function createBankAccount() {
let balance = 0;
return {
deposit: (amount) => balance += amount,
getBalance: () => balance
};
}
2. 函数工厂:
function createGreeter(greeting) {
return function(name) {
console.log(`${greeting}, ${name}!`);
};
}
3. 维持状态:
function createCounter() {
let count = 0;
return () => ++count;
}
什么时候避免:
- 不需要保持状态时,不要用闭包
- 注意内存占用,闭包会一直保持作用域
Q2: 闭包会导致内存泄漏吗?
A: 闭包本身不会导致内存泄漏,但不小心使用会占用过多内存。
问题示例:
function createElements() {
let largeData = new Array(1000000);
document.getElementById('btn')
.addEventListener('click', function() {
console.log('clicked');
// largeData被闭包引用,不会被回收
});
}
解决方案:
function createElements() {
let largeData = new Array(1000000);
function handleClick() {
console.log('clicked');
// 不引用largeData
}
document.getElementById('btn')
.addEventListener('click', handleClick);
// 使用完后可以释放
largeData = null;
}
最佳实践:
- 闭包中只引用需要的数据
- 不再使用时设置为null
- 移除事件监听器:removeEventListener
Q3: let、const、var有什么区别?
A: 三大区别:
1. 作用域:
- var: 函数作用域
- let/const: 块级作用域
if (true) {
var a = 1;
let b = 2;
}
console.log(a); // 1
console.log(b); // ReferenceError
2. 变量提升:
- var: 提升并初始化为undefined
- let/const: 提升但不初始化(TDZ)
console.log(a); // undefined
var a = 1;
console.log(b); // ReferenceError
let b = 2;
3. 重复声明:
- var: 允许重复声明
- let/const: 不允许
var a = 1;
var a = 2; // OK
let b = 1;
let b = 2; // SyntaxError
选择建议:
- 默认使用const(不会被重新赋值)
- 需要重新赋值时使用let
- 避免使用var
Q4: 如何调试闭包问题?
A: 调试闭包的技巧:
1. 使用console.log查看闭包变量:
function outer() {
let count = 0;
return function() {
console.log('count:', count); // 查看值
count++;
};
}
2. 浏览器开发者工具:
- 在闭包函数内设置断点
- 查看Scope面板的Closure部分
- 可以看到所有闭包变量
3. 检查意外的闭包:
// 检查函数长度
function create() {
let data = 'large data';
return function() {
console.log('hello');
// data被意外引用
};
}
// 解决:明确不引用
function create() {
let data = 'large data';
let result = function() {
console.log('hello');
};
data = null; // 释放引用
return result;
}
4. 内存分析:
- 使用Chrome Memory面板
- 拍摄堆快照
- 查看分离的DOM树和闭包
学习时间: 2026-03-09 22:22
难度: ⭐⭐⭐⭐☆
预计用时: 2-3 小时
关键词: 作用域与闭包