闭包
我们都知道函数由于作用域的存在,外部一般是无法访问函数内部局部变量的:
1 function f1() { 2 var a = 1 3 } 4 console.log(a);//Uncaught ReferenceError: a is not definedat Untitled-1.html:14
但是我们可以通过在f1中再定义一个函数f2,根据作用域链的规则,f2是可以访问f1的局部变量的,我们再把f2当作f1的函数返回值,那么f1外部不也可以读取f1的变量了!
1 function f1() { 2 var a = 1; 3 function f2() { 4 console.log(a); 5 } 6 return f2; 7 } 8 9 var result = f1() 10 result() //1
这便是一个闭包产生的结果!chrom浏览器会将这个执行上下文的函数名f1称作闭包。
闭包:是一种特殊对象,由某个函数以及它执行上下文共同组成,当该内部函数访问了外部上下文中的值便产生闭包
形成条件:
- 函数的嵌套
- 内部函数引用外部函数的标变量
一个简单闭包应用例子:
1 for (var i=1; i<=5; i++) { 2 setTimeout( function timer() { 3 console.log( i ); 4 }, i*1000 ); 5 } //我们想要每隔一秒分别输出12345;但实际上每隔一秒产生一个6 共输出5个6
由于setTimeout运用到js中的异步机制:setTimeout每执行时,会将其内部内容作为异步任务放入异步任务队列中,然后继续执行完所有主线任务,再一步步执行任务队列。
所以上面实际上内部函数function timer() 一直等待for循环执行完毕后,再开始执行,此时i=6,故异步输出5个6。我们可以用利用闭包来达到想要的效果:
1 for (var i=1;i<=5;i++){ 2 function f1 () { ----------- 3 var j = i; 4 setTimeout( function timer(){ 在此间形成闭包f1 5 console.log(j); 6 }, j*1000); 7 } ----------- 8 f1() 9 } 此时便会每隔一秒分别输出12345;达到我们本来想要的结果。
不同点在于第二个做法将循环内部套上了一层函数f1,满足了闭包条件产生了闭包; 通过j保存了每一次循环过后不同的i再通过闭包将其滞留; 尽管异步仍然回等所有主线函数执行完毕最后再一个个执行,但闭包f1的存在导致即便执行异步队列中function timer() 执行时 闭包中f1中的变量j仍然被防止垃圾回收机制销毁.以达到我们想要的效果。
就好比每次循环都会形成新的不同的作用域,闭包导致每个不同的i都被封闭到不同的作用域中了。
还可以通过立即执行函数简化成:
1 for (var i=1; i<=5; i++) { 2 (function(j){ 3 setTimeout(function timer() { 4 console.log(j); 5 }, 0) 6 })(i) 7 }
此处的j=i还只是为了方便理解,可以不写;
上述代码等于:
1 var i = 1; 2 (function f1(){ 3 var j = i; 4 setTimeout(function timer() { 5 console.log(j); 6 }, j*1000) 7 })(); 8 var i =2; 9 (function f1(){ 10 var j = i; 11 setTimeout(function timer() { 12 console.log(j); 13 }, j*1000) 14 })(); 15 var i =3; 16 (function f1(){ 17 var j = i; 18 setTimeout(function timer() { 19 console.log(j); 20 }, j*1000) 21 })(); 22 var i =4; 23 (function f1(){ 24 var j = i; 25 setTimeout(function timer() { 26 console.log(j); 27 }, j*1000) 28 })(); 29 var i =5; 30 (function f1(){ 31 var j = i; 32 setTimeout(function timer() { 33 console.log(j); 34 }, j*1000) 35 })()
说白了就是这个例子我们需要每次迭代时创建新的作用域,在这个作用域内形成闭包。
es6中let声明,除了不形成变量提升,可以形成块级作用域之外,还有更多的不同:可以劫持块级作用域,并在该作用域声明变量。本质上将该块转换成可关闭的作用域。
如此便可利用let完成上例:
1 for(var i=1;i<=5;i++){ 2 let j = i; 3 setTimeout(function timer() { 4 console.log(j) 5 }, j*1000); 6 }
根据你不知道的js中指出 for循环头部的let声明还会存在一个特殊行为:指出变量在循环过程中不止被声明一次,即每次迭代都会声明一次,随后每个迭代都会使用上一个迭代结束时的值来初始化该变量。既然如此,那么还有更简单的实现方法:
1 for(let i=1;i<=5;i++){ 2 setTimeout(function timer() { 3 console.log(i) 4 }, i*1000); 5 } // 闭包与块级作用域联手天下无敌!
闭包的一些应用场景:往后深入学习再写
- 模块化
- 柯里化