闭包

118 阅读5分钟

什么是闭包?

红宝书闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数。

 MDN:一个函数和对其周围状态的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。

个人理解:闭包其实就是一个可以访问其他函数内部变量的函数。即一个定义在函数内部的函数,或者直接说闭包是个内嵌函数也可以。

function fun1() {

	var a = 1;

	return function(){

		console.log(a);

	};

}

fun1();

var result = fun1();

result();  // 1

闭包产生的原因

其实很简单,当访问一个变量时,代码解释器会首先在当前的作用域查找,如果没找到,就去父级作用域去查找,直到找到该变量或者不存在父级作用域中,这样的链路就是作用域链。

需要注意的是,每一个子函数都会拷贝上级的作用域,形成一个作用域的链条。

var a = 1;

function fun1() {

  var a = 2

  function fun2() {

    var a = 3;

    console.log(a);//3

  }

}

从中可以看出,fun1 函数的作用域指向全局作用域(window)和它自己本身;fun2 函数的作用域指向全局作用域 (window)、fun1 和它本身;而作用域是从最底层向上找,直到找到全局作用域 window 为止,如果全局还没有的话就会报错。

由此可见,闭包产生的本质就是:当前环境中存在指向父级作用域的引用

function fun1() {

  var a = 2

  function fun2() {

    console.log(a);  //2

  }

  return fun2;

}

var result = fun1();

result();

那是不是只有返回函数才算是产生了闭包呢?其实也不是,回到闭包的本质,我们只需要让父级作用域的引用存在即可

var fun3;

function fun1() {

  var a = 2

  fun3 = function() {

    console.log(a);

  }

}

fun1();

fun3();

可以看出,其中实现的结果和前一段代码的效果其实是一样的,就是在给 fun3 函数赋值后,fun3 函数就拥有了 window、fun1 和 fun3 本身这几个作用域的访问权限;然后还是从下往上查找,直到找到 fun1 的作用域中存在 a 这个变量;因此输出的结果还是 2,最后产生了闭包,形式变了,本质没有改变。

因此最后返回的不管是不是函数,也都不能说明没有产生闭包。讲到这里你这里可以再深入体会一下闭包的内涵。

闭包的表现形式

1. 返回一个函数,上面讲原因的时候已经说过,这里就不赘述了。

2. 在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。请看下面这段代码,这些都是平常开发中用到的形式。

// 定时器

setTimeout(function handler(){

  console.log('1');

},1000);

// 事件监听

$('#app').click(function(){

  console.log('Event Listener');

});

3. 作为函数参数传递的形式,比如下面的例子。

var a = 1;

function foo(){

  var a = 2;

  function baz(){

    console.log(a);

  }

  bar(baz);

}

function bar(fn){

  // 这就是闭包

  fn();

}

foo();  // 输出2,而不是1

4. IIFE(立即执行函数),创建了闭包,保存了全局作用域(window)和当前函数的作用域,因此可以输出全局的变量,如下所示。

var a = 2;

(function IIFE(){

  console.log(a);  // 输出2

})();

IIFE 这个函数会稍微有些特殊,算是一种自执行匿名函数,这个匿名函数拥有独立的作用域。这不仅可以避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域,我们经常能在高级的 JavaScript 编程中看见此类函数。

如何解决循环输出问题?

for(var i = 1; i <= 5; i ++){

  setTimeout(function() {

    console.log(i)

  }, 0)

}
  1. setTimeout 为宏任务,由于 JS 中单线程 eventLoop 机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行。

  2. 因为 setTimeout 函数也是一种闭包,往上找它的父级作用域链就是 window,变量 i 为 window 上的全局变量,开始执行 setTimeout 之前变量 i 已经就是 6 了,因此最后输出的连续就都是 6。、

可以利用 IIFE(立即执行函数),当每次 for 循环时,把此时的变量 i 传递到定时器中,然后执行,改造之后的代码如下。

for(var i = 1;i <= 5;i++){

  (function(j){

    setTimeout(function timer(){

      console.log(j)

    }, 0)

  })(i)

}

ES6 中新增的 let 定义变量的方式,使得 ES6 之后 JS 发生革命性的变化,让 JS 有了块级作用域,代码的作用域以块级为单位进行执行。通过改造后的代码,可以实现上面想要的结果。

for(let i = 1; i <= 5; i++){

  setTimeout(function() {

    console.log(i);

  },0)

}

setTimeout 作为经常使用的定时器,它是存在第三个参数的,日常工作中我们经常使用的一般是前两个,一个是回调函数,另外一个是时间,而第三个参数用得比较少。那么结合第三个参数,调整完之后的代码如下。

for(var i=1;i<=5;i++){

  setTimeout(function(j) {

    console.log(j)

  }, 0, i)

}

闭包可能造成的内存泄露:当内部函数被全局变量引用的时候就会造成内存泄露,解决办法当代码使用完后重复改变全局变量的指向让其指向为null,或者在另外一个函数内部使用。