JS闭包概念再复习

141 阅读4分钟

这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战

介绍

闭包就是能够读取其他函数内部变量的函数。在JS中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成定义在一个函数内部的函数。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

闭包的产生

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

function foo() { 
 let a = 1; 
 function bar() { 
     console.log( 1 ); // 1 
 } 
 bar(); 
}

foo();

以上代码并不是一个的闭包,函数必须在另一个上下文中调用另一个上下文的变量。

在顶级作用域中定义了一个foo函数并执行,在foo函数内定义了一个bar函数,bar函数打印了定义在foo词法作用域的a变量,但自己也处于该作用域中。因此js引擎在解释这段代码时,是根据作用域链逐层往上查询变量a是否存在。

function foo() { 
 let a = 1; 
 return function () { 
     console.log( a ); 
 } 
} 
var baz = foo(); 
baz();

这就是闭包,baz变量被赋值为一个函数的引用,该函数定义在foo函数内部,并且使用了foo词法作用域中的变量a,完美符合了闭包的定义。

由于垃圾回收机制,foo函数在执行完毕后,其作用域会被销毁,因此内部定义的变量a及函数bar也会被销毁,但执行结果显示它们依旧存在,这是因为存在闭包。

顶级作用域中的baz引用了foo词法作用域中返回的函数,相当于foo函数变相地被引用了,这使得引擎不会回收foo的词法作用域,这形成了闭包,使得bar函数可以继续访问定义时的词法作用域。

闭包使得函数可以继续访问定义时的词法作用域。

当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。

function foo() { 
 var a = 2; 
 function baz() { 
     console.log( a ); // 2 
 } 
 bar( baz ); 
} 
function bar(fn) { 
 fn(); 
}

把内部函数 baz 传递给 bar函数调用。

var fn; 
function foo() { 
 var a = 2; 
 function baz() { 
 console.log( a ); 
 } 
 fn = baz; 
} 
function bar() { 
 fn();
} 
foo(); 
bar(); // 2

无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用 域的引用,无论在何处执行这个函数都会使用闭包。

识别闭包

function wait(message) { 
 setTimeout( function timer() { 
 console.log( message ); 
 }, 1000 ); 
} 
wait( "Hello, closure!" );

1秒后会输出一条提示信息,而这条信息是在1秒前传入,并且存在于wait的词法作用域中,它被使用是在timer的词法作用域,根据引擎查询变量的规则,他会根据作用域链逐层向上查询,这里它的确查询到了,说明wait执行往后依旧存在,这都是闭包造成的。

本质上如果将函数当作参数传递,就会造成闭包。例如在定时器、事件监听器、Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!

循环与闭包

要说明闭包,for 循环是最常见的例子。

//实际结果是每隔一秒打印一个 '6'
for (var i=1; i<=5; i++) { 
 setTimeout( function timer() { 
 console.log( i ); 
 }, i*1000 ); 
}

代码分成2部分,循环部分是同步执行代码,定时器部分是异步代码。

for循环中使用var定义变量i不会形成块作用域,因此i的作用域是顶级作用域,在循环结束后,i依旧才存在,并且值为6

异步部分,timer函数与for循环所形成的块之间构成了一个闭包,并引用了变量i,此时的i并不是每次循环时i的复制,而是循环结束后的i。

根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i

这是因为在使用var定义的变量不存在块级作用域。

通过IIFE创建新的作用域。

for (var i=1; i<=5; i++) { 
  (function() { 
  var j = i; 
  setTimeout( function timer() { 
  console.log( j ); 
  }, j*1000 ); 
  })(); 
}

每次循环,IIFE都会创建一个独立的作用域,并将外层作用域的i都回被保存在自己的作用域中,以供timer函数的闭包调用。

更加简洁的实现

for (var i=1; i<=5; i++) { 
 (function(j) { 
 setTimeout( function timer() { 
 console.log( j ); 
 }, j*1000 ); 
 })( i ); 
}

//利用let来形成块作用域
for (let i=1; i<=5; i++) { 
 setTimeout( function timer() { 
 console.log( i ); 
 }, i*1000 ); 
}

总结

函数可以记住并访问被定义时所在的词法作用域,因此当函数在当前词法作用域之外执行,可以访问之前作用域的变量和方法,这就是闭包。