闭包不只是面试名词,它会在定时器传参、事件回调、私有变量、循环绑定和 ES6 偏函数里反复出现。下面按原文五个场景把例子摊开。记住一条:返回的函数不要随手去引用循环变量。
参考:CSDN 原文;廖雪峰:闭包。
一、setTimeout 传参
原生 setTimeout 传的第一个函数不能带参数,用闭包可以做到传参:
setTimeout(function(param){
alert(param)
},1000)
//通过闭包可以实现传参效果
function func(param){
return function(){
alert(param)
}
}
var f1 = func(1);
setTimeout(f1,1000);
二、回调
我们定义行为,再把它绑到点击或按键上。代码通常作为回调(事件触发时调用的函数)挂上去:
<body>
<p>哈哈哈哈哈哈</p>
<h1>hhhhhhhhh</h1>
<h2>qqqqqqqqq</h2>
<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a>
<script>
function changeSize(size){
return function(){
document.body.style.fontSize = size + 'px';
};
}
var size12 = changeSize(12);
var size14 = changeSize(14);
var size16 = changeSize(16);
document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
</script>
</body>
三、封装变量
用闭包定义能访问私有函数和私有变量的公有函数:
var counter = (function(){
var privateCounter = 0; //私有变量
function change(val){
privateCounter += val;
}
return {
increment:function(){ //三个闭包共享一个词法环境
change(1);
},
decrement:function(){
change(-1);
},
value:function(){
return privateCounter;
}
};
})();
console.log(counter.value());//0
counter.increment();
counter.increment();//2
-
-
环境中有一个局部变量、一个局部函数,通过匿名函数返回的对象上的三个公共函数访问。
另一个计数器
用 JavaScript 创建计数器:
'use strict';
function create_counter(initial) {
var x = initial || 0;
return {
inc: function () {
x += 1;
return x;
}
}
}
运行结果:
var c1 = create_counter();
c1.inc(); // 1
c1.inc(); // 2
c1.inc(); // 3
var c2 = create_counter(10);
c2.inc(); // 11
c2.inc(); // 12
c2.inc(); // 13
返回对象里实现了一个闭包,携带局部变量 x,外部根本访问不到。闭包就是携带状态的函数,并且状态可以完全对外隐藏。
| 场景 |
闭包做了什么 |
| setTimeout |
把参数封进返回的无参函数 |
| 事件回调 |
changeSize(n) 返回真正的 onclick |
| 模块/计数器 |
IIFE 里的私有变量只经公开方法改 |
| 循环绑定 |
用立即执行函数把当前 i 拷到 n |
| 偏函数 addX |
内层函数一直能读到 x |
四、循环绑定的经典坑
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push(function () {
return i * i;
});
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
每次循环都创建了一个新函数,三个函数放进数组返回。你可能以为 f1() / f2() / f3() 是 1 4 9,实际是:
f1(); // 16
f2(); // 16
f3(); // 16
全部都是 16。返回的函数引用了变量 i,但并不立刻执行。三个函数都返回时,它们引用的 i 已经变成 4,4×4=16。
返回闭包时牢记:返回函数不要引用任何循环变量,或后续会变化的变量。
如果一定要引用,再包一层,用参数绑住当前值:
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push((function (n) {
return function () {
return n * n;
}
})(i));
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
f1(); // 1
f2(); // 4
f3(); // 9
这里用的是「创建匿名函数并立刻执行」:
(function (x) {
return x * x;
})(3); // 9
五、ES6 里不经意的闭包
局部应用(partial application)就是闭包:
let c = 4
const addX = x => n => n + x
const addThree = addX(3)
let d = addThree(c)
console.log('example partial application', d)
不用箭头函数的等效写法:
let c = 4
function addX(x) {
return function(n) {
return n + x
}
}
const addThree = addX(3)
let d = addThree(c)
console.log('example partial application', d)
通用加法 addX 接受 x,返回另一个函数;内层把参数和 x 相加。x 是闭包的一部分。声明 addThree 时,它带着函数定义和含 x 的闭包。调用时既能读到 x,也能读到传入的 n,控制台打印 7。
一句话总结:闭包把外层变量带进稍后执行的函数:定时器、回调、私有状态都靠它;循环里要用 IIFE/参数把当前值拷走,否则几个函数会共享同一个变了的 i。