首先来谈谈闭包,js经典问题了。解释也是众说纷纭,大同小异。这里引用阮一峰老师对它的简单解释:闭包是能够读取其他函数内部变量的函数。
看一个例子:
function foo() { var a = 2; function bar() { console.log( a ); } return bar; } var baz = foo(); baz(); // 2
这就是一个闭包函数,bar的执行能读取到foo中的变量a ,闭包与作用域息息相关。
函数 bar() 的词法作用域能够访问 foo() 的内部作用域
再看一个例子
function foo() { var a = 2; function baz() { console.log( a ); // 2 } bar( baz ); } function bar(fn) { fn(); }
bar函数的执行能取到foo中的变量,原理是fn就是baz而它又在foo内,可以通过作用域链往上查找到foo作用域下的变量a.baz就是中间的桥梁。
var fn; function foo() { var a = 2; function baz() { console.log( a ); } fn = baz; // 将 baz 分配给全局变量
} function bar() { fn(); // 妈妈快看呀,这就是闭包! } foo(); bar(); // 2
上面这个例子,全局变量fn是个桥梁,连接了foo内部的baz函数,bar函数执行,fn函数执行,刚好fn函数就是foo内部的baz函数,这样就能取到baz的上级作用域foo中的a变量。
闭包的使用场景:在定时器、事件监听器、 Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!
闭包的优点;防止全局变量的冲突,缺点:变量始终保存在内存中,容易造成内存泄露。
提到闭包,其实与作用域,作用域链是绑定在一起的。作用域又分为显示和隐式。然后又是对于闭包函数的执行又涉及到执行上下文,以及this(执行上下文中的一个属性) 关于this指向问题,可以看我另外的一篇博客,基本涉及到每一种情况。对于执行上下文,下一篇博客再写它。
这里先看看作用域,看一个常见的例子!
for (var i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); }
实际上输出结果是:5个6,因为延迟定时器的队列原因,即使是1000ms后第一个延迟函数执行,能获取到的i值是整个for循环之后的i值了。
其实头疼的是不能挽留住每次循环的i值,所以想要得到理想的输出1,2,3,4,5.就需要考虑如何挽留的问题了。
第一种:
for (var i=1; i<=5; i++) { (function() { var j = i; setTimeout( function timer() { console.log( j ); }, j*1000 ); })(); }
让循环中的延时器函数包裹在一个立即自执行的函数中,该函数中命名一个j变量,保存住j值。
第二种:
for (var i=1; i<=5; i++) { (function(j) { setTimeout( function timer() { console.log( j ); }, j*1000 ); })( i ); }
原理类似于上面的,但是是通过把i作为形参的方式传给自执行函数,然后再往里传递给延时函数。
第三种:
for (let i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); }
利用es6的let,生成块级作用域。相当于每次迭代都重新生成了。最常用的方式
第四种:
for (var i=1; i<=5; i++) { let j = i; // 是的,闭包的块作用域! setTimeout( function timer() { console.log( j ); }, j*1000 ); }
循环中的i依然是var类型声明的,但是,在每次的循环中,声明一个j来接受该次循环的i值,并生成块级作用域。
以上方法,第一种和第二种属于隐式作用域,并没有新的作用域生成,只是隐式的劫持了已存在的作用域,挽留住了稍纵即逝的i值。
第三种和第四种方法利用了let的特性,let 声明会创建一个显示的作用域并与其进行 绑定。显式作用域不仅更加突出,在代码重构时也表现得更加健壮。在语法上,通过强制 性地将所有变量声明提升到块的顶部来产生更简洁的代码。这样更容易判断变量是否属于 某个作用域。