彻底理解js闭包

1,685 阅读6分钟

引言

闭包是个老生长谈的话题了,对于闭包网上也有很多不同的看法

  • 《你不知道的javaScript》对于闭包是这么定义的:函数创建和函数执行不在同一个作用域下就会形成闭包。
  • MDN对于闭包的定义为:闭包是指那些能够访问自由变量的函数
  • 《JS高级程序设计-第3版》对于闭包的定义: 闭包是指有权访问另外一个函数作用域中的变量的函数

好,现在你不用着急去搞明白它们的意思,我们先来讲下自由变量这个是个什么东西。

自由变量

自由变量:是指在函数中使用的变量即不是函数参数arguments也不是函数内部声明的变量,那这个变量就是自由变量

代码实例

function outer(){
  var public = '前端自学驿站:';
  function inner(name){
    // 这个函数中用到了两个变量
    public: 不是自己的函数,也不是函数内部定义的变量
    name: 函数自己的参数
    console.log(public + name);  //前端自学驿站:北歌
  }
  return inner('北歌'); // 返回inner函数执行的返回值
}
outer();

上面这段代码中中public不是自己的参数也不是函数内部声明的变量,那它就是一个自由变量

动动脑来分析下,思考下上面代码有形成闭包吗?

举例

上面的代码分析出来了吗? 现在我们再来看个例子,对比分析下闭包究竟是个什么东西。

function fun() {
  var count = 1;
  return function () {
    count++; // 自由变量
    console.log(count);
  }
}

var fun2 = fun(); // fun2 => 返回的匿名函数
fun2(); // 函数在全局作用域调用,创建的时候是在fun作用域中创建的

简要的执行过程如下:

  • 1、进入全局代码,全局调用栈执行,全局上下文初始化
  • 2、fun调用,创建fun函数执行上下文,fun执行上下文被压入执行上下文栈
  • 3、fun执行上下文初始化, 形成AO(Active Object)活动对象,分析函数内的变量,参数, 确定this的指向
  • 4、fun进入执行完毕返回匿名函数给fun2
  • 5、fun2调用, 按照2、3的步骤走一遍。

下面让我们来看看chrome控制台

上面截图已经把所要知道的标识了,关于chrome的调式这里就不多讲了,不懂的可以回过头去看js基础大纲

一步步调式下去,在调用fun2函数的时候形成了闭包(Closure),从控制台可以看到(这是chrome浏览器为了开发人员好调式给我们搞出来的,可以把闭包认为是一个容器,存储的是键值对形式的值)。

所以说上面的代码形成了闭包,让我们来看看它是不是符合上面对于闭包的定义

  • 《你不知道的javaScript》:函数创建和函数执行不在同一个作用域下就会形成闭包。
  • ​ MDN:闭包是指那些能够访问自由变量的函数。
  • 《JS高级程序设计-第3版》闭包是指有权访问另外一个函数作用域中的变量的函数

对照上面的代码我们不难发现上面三个条件都符合

现在我们回过头来看看最前面的代码,它是闭包嘛?不是!

让我们来看看他为什么不是

function outer(){
  var public = '前端自学驿站:';
  function inner(name){
    // 这个函数中用到了两个变量
    public: 不是自己的函数,也不是函数内部定义的变量
    name: 函数自己的参数
    console.log(public + name);  //前端自学驿站:北歌
    return {
      a: 1
    }
  }
  return inner('北歌'); // 返回inner函数执行的返回值
}
let r = outer();
console.log(r)
console.log(r)

// 1.函数创建和函数执行是在同一个作用域下 (不符合)
// 2.引用了自由变量 (符合)
// 3.访问了另外一个函数作用域中的变量 (符合)

认为是的伙伴会说,从chrome控制台调式时inner函数调用时确实形成了短暂的闭包,但是在函数执行完成时外层函数没有东西被占用,outer函数执行的栈内存就被销毁了,所以从确切来说这不算是一个闭包。

总结

上面介绍的是实践角度,对于闭包的有多种介绍且说法不一。

按照的我理解闭包得符合两个条件:

1、函数执行完毕,函数内部创建的东西被函数外部引用了,形成不销毁的栈内存

2、在代码中引用了自由变量

闭包面试题

好了,现在让我们一起来做常见的闭包相关的面试题看看你有没有正真撑握它!

function fun(n, o) {
  console.log(o);
  return {
    fun: function (m) {
      return fun(m, n);
    }
  };
}

var a = fun(0); // ?
a.fun(1); // ?
a.fun(2); // ?
a.fun(3); // ?
var b = fun(0).fun(1).fun(2).fun(3); // ?
var c = fun(0).fun(1); // ?
c.fun(2); // ?
c.fun(3); // ?

解析

为了看的方便建议将题目复制到IDE中跟解析对照着看,这样更易于理解。

// 为了能够解析方便, 我将代码做下拆分,以下是上半部分
function fun(n, o) {
  /**
   * 第一次执行fun
   * n: 0  
   * o: undefined
   */

  /**
   * 第二次执行:a.fun(1)中调用的fun(1, 0)
   * n:1
   * o: 0
   */

  /**
   * 第三次执行:a.fun(2)中调用的fun(2, 0)
   * n:2
   * o: 0
   */

   /**
   * 第四次执行:a.fun(3)中调用的fun(3, 0)
   * n:3
   * o: 0
   */
  console.log(o);
  return {
    fun: function (m) {
      /**
       * 第一次调用: m: 1
       * fun(1, n是自由变量用的是上层作用域中的0)
       */

      /**
       * 第二次调用: m: 2
       * fun(2, n是自由变量用的是上层作用域中的0)
       */

      /**
       * 第二次调用: m: 3
       * fun(3, n是自由变量用的是上层作用域中的0)
       */
      return fun(m, n);
    }
  };
}

var a = fun(0); // undefined 执行返回了一个对象被a引用了,形成闭包fun中的变量没有被销毁
a.fun(1); // 0
a.fun(2); // 0
a.fun(3); // 0
// => 下半部分,解析也是只写下半部分的
function fun(n, o) {
  /**
   * 第一次执行:fun(0)
   * n: 0  
   * o: undefined
   */

   /**
   * 第二次执行:对象.fun(1)调用的fun(1, 0)
   * n: 1  
   * o: 0
   */

   /**
   * 第三次执行:对象.fun(2)调用的fun(2, 1)
   * n: 2
   * o: 1
   */

    /**
   * 第四次次执行:对象.fun(3)调用的fun(3, 2)
   * n: 3
   * o: 2
   */
  console.log(o);
  return {
    fun: function (m) {
       /**
       * 第一次调用:对象.fun(1)
       * m: 1
       * fun(1, n是用的是上层作用域中的0)
       */

       /**
       * 第二次调用:对象.fun(2)
       * m: 2
       * fun(2, n是用的是上层作用域中的1)
       */

       /**
       * 第二次调用:对象.fun(3)
       * m: 3
       * fun(3, n是用的是上层作用域中的2)
       */
      return fun(m, n);
    }
  };
}

/* var a = fun(0); // undefined 执行返回了一个对象被a引用了,形成闭包fun中的变量没有被销毁
a.fun(1); // 0
a.fun(2); // 0
a.fun(3); // 0 */


// 第一次执行fun(0)返回的是对象,调用对象的中的fun方法
var b = fun(0).fun(1).fun(2).fun(3); // undefined, 0, 1, 2

// 能看懂上面的话剩下的这些应该就能自己分析出来了,答案也不给了分析好后直接控制台输出
var c = fun(0).fun(1); // ?
c.fun(2); // ?
c.fun(3); // ?

好了,解析到这就差不多不然代码篇幅太长了,只要把前面搞懂后面就完全没有问题。解析部分可以好好看看。