# 闭包

# 到底什么是闭包?

一句话来讲就是:让函数当前词法作用域之外作用域执行,就产生了闭包。

此时函数可以记住并访问声明时所在的词法作用域

# 如何产生闭包?

根据上述闭包的概念,要想产生闭包,我们只需要把内部函数传递到所在的词法作用域之外就能制造闭包。

:只要使用了回调函数,实际上就在使用闭包

// 闭包1
function fn1() {
  var num1 = 1;
  function fn2() {
    console.log(num1);
  }
  return fn2;
}

var fn2 = fn1(); // 此时就产生了闭包,因为 函数fn1 内部的 函数fn2 被传递到了外部,并赋值给了 变量fn2 ,此时 变量fn2 会持有 函数fn1 的词法作用域
fn2(); // 打印1
// 闭包2
var fn3;
function fn1() {
  var num1 = 1;
  function fn2() {
    console.log(num1);
  }
  fn3 = fn2;
}

function fn4() {
  fn3(); // 这里也会产生闭包
}

fn4(); // 打印1
// 闭包3 回调函数
function fn1() {
  var num1 = 1;
  function fn2() {
    console.log(num1);
  }
  fn3(fn2); // 打印1
}

function fn3(cb) {
  cb(); // 这里执行了回调,产生了闭包
}
// 闭包3 回调函数,更常见也更容易被忽视
var num1 = 1;
setTimeout(function fn1() {
  console.log(num1); // 打印1
}, 100);

// 此时的fn1也会产生闭包
// 因为fn1声明在setTimeout所在的 词法作用域,但是执行是在 setTimeout 内部
// 此时的 setTimeout 如果在 全局作用域(window),fn1 将持有整个 全局作用域,如果不及时清除,很有可能会产生内存泄漏

# 面试中会碰到的闭包问题

也和JS的事件循环Event Loop)有关,后续会补充相关知识。

for (var i = 0; i < 6; i++) {
  setTimeout(function fn1() {
    console.log(i);
  }, i * 1000);
}
// 结果:打印 5个6
/**
 * 原因:
 * 
 * 这就关系到了JS的事件循环,这里先简单知道这个概念:
 * 
 * setTimeout是异步函数,JS会在所有的 同步代码 执行之后 再执行 异步函数。
 * 
 * for循环执行完之后,i为6,setTimeout执行时,内部函数fn1获取到的i就为6,所以执行结果 为 5个6。
**/

如何解决呢?

// 方法1 创建函数作用域
for (var i = 0; i < 6; i++) {
  (function(j) {
    setTimeout(function fn1() {
      console.log(j);
    }, j * 1000);
  })(i);
}

// 结果:打印 1 2 3 4 5
/**
 * 原因:
 * 
 * IIFE会创建 函数作用域,for循环5次,创建了5个函数作用域。
 * 
 * 函数作用域 内的 变量 都是独立于自己当前的作用域的。
 *
 * 把i当作实参传递给IIFE,j就成为当前 函数作用域 的值。
**/
// 方法2 使用 let 创建类似块作用域
for (let i = 0; i < 6; i++) {
    setTimeout(function fn1() {
      console.log(i);
    }, i * 1000);
}

// 结果:打印 1 2 3 4 5
/**
 * 原因:
 * 
 * 使用了let 块作用域 的特性。
 * 
 * for循环头部的let有一个 特殊行为,每次迭代都会声明。
**/

# Tips

模块化的实现

大家可以想象一下,自己平时使用的 import/export 等方式,也是基于闭包的原理实现的。

创建一个封闭的作用域,把需要的函数 export出去,这些被export函数在被调用时,就会持有声明所在的词法作用域

如果觉得内容对你有帮助,请点个关注,你们的鼓励是我持续更新下去的动力,比心。

如需转载请注明出处,感恩。

更多内容可先关注GitHub (opens new window)