JavaScript 作用域与闭包详解
作用域和闭包是 JavaScript 中最重要的概念之一,理解它们对于编写高质量代码至关重要。
一、作用域 (Scope)
1. 作用域基本概念
作用域决定了变量和函数的可访问范围。JavaScript 有以下几种作用域:
- 全局作用域:在函数外部声明的变量
- 函数作用域:在函数内部声明的变量
- 块级作用域 (ES6+): 由
let
和const
在{}
内声明的变量
javascript
// 全局作用域
var globalVar = "I'm global";
function example() {
// 函数作用域
var functionVar = "I'm in function";
if (true) {
// 块级作用域
let blockVar = "I'm in block";
console.log(blockVar); // 可访问
}
console.log(blockVar); // 报错: blockVar is not defined
}
2. 作用域链
当访问一个变量时,JavaScript 引擎会按照以下顺序查找:
- 当前作用域
- 外层作用域
- 直到全局作用域
ini
let a = 1;
function outer() {
let b = 2;
function inner() {
let c = 3;
console.log(a + b + c); // 6 (可以访问所有变量)
}
inner();
}
outer();
3. 变量提升 (Hoisting)
var
声明的变量会提升到函数/全局作用域顶部let
和const
也有提升,但存在"暂时性死区" (TDZ)
ini
console.log(x); // undefined (变量提升)
var x = 5;
console.log(y); // 报错: Cannot access 'y' before initialization
let y = 10;
二、闭包 (Closure)
1. 闭包基本概念
闭包是指函数能够记住并访问其词法作用域,即使该函数在其词法作用域之外执行。
简单来说:当一个函数可以访问并记住它被声明时所处的环境(包括变量等),即使这个函数在其他地方被调用,就形成了闭包。
闭包的形成条件
- 函数嵌套:一个函数内部定义了另一个函数
- 内部函数引用外部函数的变量
- 内部函数被外部使用(返回、传递给其他函数等)
javascript
function outer() {
let count = 0; // 局部变量
// 内部函数inner就是一个闭包
return function inner() {
count++; // 访问外部函数的变量
return count;
};
}
const counter = outer(); // outer执行完毕,按理说count应该被销毁
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
在这个例子中:
inner
函数可以访问outer
函数的count
变量- 即使
outer
已经执行完毕,count
仍然被保留 - 每次调用
counter()
都会修改并记住count
的值
2.闭包的优缺点
优点:
- 实现数据私有化,创建私有变量
- 保持变量在内存中,实现状态持久化
- 模块化开发,避免全局污染
缺点:
- 过度使用可能导致内存占用过高
- 不合理的闭包使用可能导致内存泄漏
- 可能增加代码复杂度,降低可读性
3. 闭包的实际应用
1) 数据私有化
javascript
function createPerson(name) {
let age = 0;
return {
getName: () => name,
getAge: () => age,
celebrateBirthday: () => {
age++;
return `Happy birthday, ${name}! You're now ${age}.`;
}
};
}
const john = createPerson("John");
console.log(john.getName()); // "John"
console.log(john.getAge()); // 0
john.celebrateBirthday();
console.log(john.getAge()); // 1
2) 模块模式
ini
const calculator = (function() {
let memory = 0;
return {
add: function(a, b) {
memory = a + b;
return memory;
},
subtract: function(a, b) {
memory = a - b;
return memory;
},
getMemory: function() {
return memory;
},
clearMemory: function() {
memory = 0;
}
};
})();
calculator.add(5, 3); // 8
console.log(calculator.getMemory()); // 8
3)函数工厂
scss
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
4. 闭包的注意事项
- 内存泄漏风险:闭包会保留对外部变量的引用,可能导致内存无法释放
javascript
// 不好的实践
function createHeavyObject() {
const bigArray = new Array(1000000).fill("data");
return function() {
console.log("I'm holding a reference to " +bigArray);
};
}
const leak = createHeavyObject();
leak();
// bigArray 无法被垃圾回收,因为闭包还在引用它
- 循环中的闭包问题
javascript
// 常见问题
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // 全部输出5
}, 100);
}
// 解决方案1: 使用let (块级作用域)
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // 0,1,2,3,4
}, 100);
}
// 解决方案2: IIFE
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 0,1,2,3,4
}, 100);
})(i);
}
三、作用域与闭包的关系
特性 | 作用域 | 闭包 |
---|---|---|
定义 | 变量可访问的范围 | 函数记住并访问其词法作用域的能力 |
创建时机 | 代码编写时确定 | 函数被定义时创建 |
生命周期 | 执行上下文结束时销毁 | 只要闭包存在,相关作用域就保持 |
主要用途 | 控制变量可见性 | 数据封装、私有变量、函数工厂等 |
四、最佳实践
- 优先使用
let
和const
替代var
,避免变量提升和全局污染 - 合理使用闭包,避免不必要的内存占用
- 模块化代码,利用闭包实现封装
- 注意循环中的闭包,使用块级作用域或IIFE解决
五、高级应用
1. 函数柯里化 (Currying)
javascript
function multiply(a) {
return function(b) {
return function(c) {
return a * b * c;
};
};
}
const result = multiply(2)(3)(4); // 24
2. 记忆化 (Memoization)
ini
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (cache[key]) {
return cache[key];
}
const result = fn(...args);
cache[key] = result;
return result;
};
}
const factorial = memoize(function(n) {
return n <= 1 ? 1 : n * factorial(n - 1);
});
console.log(factorial(5)); // 120 (计算并缓存)
console.log(factorial(5)); // 120 (直接从缓存读取)
理解作用域和闭包是成为高级JavaScript开发者的关键。通过实践这些概念,你将能够编写更高效、更模块化的代码。