作用域链回顾
JavaScript 有三种作用域:
- 全局作用域:在任何函数外部声明的变量
- 函数作用域:在函数内部用
var声明的变量 - 块级作用域:ES6 的
let/const在{}内生效
作用域链的查找规则很简单:从当前作用域向外逐层查找,直到全局作用域。内层可以访问外层,外层无法访问内层。
var global = 'g';
function outer() {
var outerVar = 'o';
function inner() {
var innerVar = 'i';
console.log(global); // 'g' — 找到全局
console.log(outerVar); // 'o' — 找到外层函数
}
inner();
console.log(innerVar); // ReferenceError — 外层访问不到内层
}
闭包的定义
闭包(Closure)= 函数 + 它所引用的外部变量的环境。
更直白地说:一个函数在定义时所处的词法环境被保留了下来,即使这个函数在其他地方执行。
形成闭包的条件:
- 函数嵌套
- 内部函数引用了外部函数的变量
- 内部函数被返回或传递到外部
function createGreeting(prefix) {
// prefix 被闭包捕获
return function(name) {
return prefix + ', ' + name;
};
}
const hello = createGreeting('Hello');
const hi = createGreeting('Hi');
hello('Alice'); // 'Hello, Alice'
hi('Bob'); // 'Hi, Bob'
createGreeting 已经执行完毕,但 prefix 变量没有被回收,因为返回的函数仍然引用着它。
经典示例
计数器
function makeCounter(initial) {
let count = initial || 0;
return {
increment() { return ++count; },
decrement() { return --count; },
getCount() { return count; }
};
}
const counter = makeCounter(0);
counter.increment(); // 1
counter.increment(); // 2
counter.decrement(); // 1
counter.getCount(); // 1
// count 变量外部完全无法直接访问
私有变量
function createUser(name) {
let _password = ''; // 外部无法直接访问
return {
getName() { return name; },
setPassword(pwd) { _password = pwd; },
checkPassword(pwd) { return _password === pwd; }
};
}
const user = createUser('张三');
user.setPassword('123456');
user.checkPassword('123456'); // true
user._password; // undefined — 真正的私有
模块模式
ES6 之前,闭包是实现模块化的主要手段:
var EventBus = (function() {
var listeners = {}; // 私有状态
return {
on: function(event, fn) {
(listeners[event] = listeners[event] || []).push(fn);
},
emit: function(event, data) {
(listeners[event] || []).forEach(fn => fn(data));
},
off: function(event, fn) {
listeners[event] = (listeners[event] || []).filter(f => f !== fn);
}
};
})();
循环中的闭包陷阱
经典面试题:
// 用 var — 输出全是 5
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}
// 输出:5 5 5 5 5
// 原因:var 是函数作用域,循环结束后 i = 5,所有回调共享同一个 i
三种解决方式:
// 方式一:IIFE 创建独立作用域
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() { console.log(j); }, 100);
})(i);
}
// 方式二:let 块级作用域(推荐)
for (let i = 0; i < 5; i++) {
setTimeout(function() { console.log(i); }, 100);
}
// 方式三:setTimeout 第三个参数
for (var i = 0; i < 5; i++) {
setTimeout(function(j) { console.log(j); }, 100, i);
}
闭包与内存
闭包会阻止被引用的变量被垃圾回收(GC)。这是特性,不是 bug,但使用不当会造成内存泄漏。
可能泄漏的场景
function setupHandler() {
var hugeData = new Array(1000000).fill('x'); // 大数据
var button = document.getElementById('btn');
button.addEventListener('click', function() {
// 这个回调闭包引用了 hugeData 所在的作用域
// 即使只用了 hugeData.length,整个 hugeData 都不会被回收
console.log(hugeData.length);
});
}
如何避免
function setupHandler() {
var hugeData = new Array(1000000).fill('x');
var length = hugeData.length; // 只保留需要的值
// hugeData 失去引用后可被 GC
var button = document.getElementById('btn');
button.addEventListener('click', function() {
console.log(length);
});
}
原则:闭包只捕获你需要的东西,不要无意间持有大对象的引用。
实际应用
防抖(debounce)
function debounce(fn, delay) {
let timer = null; // 闭包保持 timer 引用
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
const handleInput = debounce(function(e) {
console.log('搜索:', e.target.value);
}, 300);
柯里化(currying)
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
};
}
const add = curry((a, b, c) => a + b + c);
add(1)(2)(3); // 6
add(1, 2)(3); // 6
缓存(memoize)
function memoize(fn) {
const cache = {}; // 闭包保持缓存
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
return cache[key];
}
return (cache[key] = fn.apply(this, args));
};
}
const factorial = memoize(function(n) {
return n <= 1 ? 1 : n * factorial(n - 1);
});
小结
- 闭包是 JavaScript 的核心机制,本质是函数对其词法环境的引用
- 用途:私有变量、模块模式、回调状态保持、函数式编程工具
- 注意内存:闭包会阻止 GC,避免无意间持有大对象
- 循环 +
var+ 异步是经典陷阱,用let或 IIFE 解决