Js的闭包
首先是闭包的定义:
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行的
这段代码清晰地展示了闭包:
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2,这就是闭包
分析一下这段代码
bar()的词法作用域可以放分foo()的内部作用域,然后我们把函数bar()本身当作一个值类型进行传递,在foo()执行之后,其返回值,也就是bar()函数本身,赋值给了变量baz,并且调用了baz(),实际上,这些操作只是在通过不同的标识符在不同的作用域中调用foo()的内部函数——bar()
bar()显然是可以正常被执行的,但是,在这个例子中,bar()是在它自己定义的词法作用域之外被执行的
在一般情况下,foo()函数在执行完毕之后foo()的整个内部作用域都会被摧毁,因为我们知道浏览气是有垃圾回收器来回收不再使用的的内存的。那么在这个例子当中,foo()的内部作用于会不会被摧毁呢,答案是:不会。
这就是闭包的神奇之处,闭包可以阻止这件事情的发生,foo()的内部作用于依然存在,没有被回收,那么是谁在使用这个作用域呢,不是别人,正是bar()本身在使用。因为bar()函数的位置,bar()拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,以供bar()在以后的任何时间被引用,bar()依然持有对该作用域的引用,而这个引用就叫做闭包
function foo() {
var a = 2;
function baz() {
console.log(a);
}
bar(baz);
}
function bar(fn) {
fn(); // 看,这也是闭包
}
传递函数当然也可以是间接的
var fn;
function foo() {
var a = 2;
function bar() {
console.log(a);
}
fn = bar;
}
function baz() {
fn();
}
foo();
baz(); // 2
无论使用何种手段,只要将内部函数传递到它所在的词法作用域之外,他都会持有对原始定义作用于的引用,无论在何时执行这个函数都会使用闭包
无论你有没有意识到,在我们之前写过的代码中闭包的身影无处不在,举个例子,定时器
function wait(message: string) {
setTimeout(function timer() {
console.log(message);
}, 1000);
}
wait('hello,closure');
如果将函数作为第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用,定时器、事件监听器、Ajax请求、跨组件通信、或者其他任何的异步或者同步操作中,只要使用回调函数,就是在使用闭包
接下来是最后一个例子,考虑这段代码
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i*1000);
}
你预期这段代码会有怎样的输出,反正我最初的预期(错误的)是:分别输出数字1~5,每秒一次,一个一个
但实际上,这段代码的输出是:以每秒一次的频率输出5次6
为什么会出现和我们预期不相符的情况呢,我们假设,循环中的每个迭代都会为自己捕获一个 i 的副本,但是根据作用域的工作原理,尽管循环中的5个定时器是在5个迭代中分别定义的,但是他们都是封闭在一个作用域中,他们共享了一个全局作用域,它们也就共享了 i ,所以并没有1~5,只有5个6(6的出处是for loop的结束条件是 i = 6)
那么我们该怎么改动这段代码才能让它达到我们预期的输出呢,我们需要更多的作用域
for (var i = 1; i <=5; i++) {
(function() {
setTimeout(function timer() {
console.log(i);
}, i*1000);
})();
}
这样可行吗,答案是否定的,经过了这样的改动,我们的确拥有了更多的词法作用域,的确每个迭代都会将IIFE在每次迭代中创建的作用域封闭起来,但是作用域如果是空的,又有什么作用呢,它需要有一点实质性的内容才能为我们所用
for (var i = 1; i <=5; i++) {
(function() {
var j = i;
setTimeout(function timer() {
console.log(j);
}, j*1000);
})();
}
down!,这段代码能够达到我们预期的输出了,我们对它进行一点改动
for (var i = 1; i <=5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j*1000);
})(i);
}
在迭代内使用IIFE会为每个迭代创建一个新的作用域,使用延迟函数的回调可以将作用域完全封闭在这个迭代内部,每个迭代通过 i 对 j 的传值会使我们有一个正确的变量供我们访问
完~