2

在理解闭包之前,需要先来了解几个概念,上下文、作用域链、活动对象、变量对象:

  • 上下文:函数的上下文决定了他们可以访问哪些数据,以及他们的行为。全局上下文是最外层的上下文,当代码执行流进入到函数时,函数的上下文被推到上下文栈上,当函数执行完之后,上下文栈会弹出该函数上下文。
  • 作用域链:上下文中代码执行的时候会创建作用域链,它决定了各级上下文中代码访问变量或函数的顺序。代码正在执行的上下文变量对象总是位于作用域链最顶端,然后是包含上下文对象,然后是下一个包含上下文对象,直至到全局上下文对象为止。
  • 活动对象:函数上下文中,包含其中变量的对象。
  • 变量对象:全局上下文中,包含其中变量的对象。

我们来总结一下,在调用一个函数时,它的作用域链都保存了哪些对象?

  1. 在函数被调用时,先创建一个执行上下文,并创建一个作用域链。
  2. argument和其他命名参数初始化该函数的活动对象。
  3. 外部函数的活动对象是该函数作用域链的第二个对象,该作用域链一直向外部串起所有的包含该函数的活动对象,知道全局上下文才终止。

下面声明并调用了一个方法compare(),现在来梳理一下该方法从创建到执行的过程:

  function compare(value1, value2) {
    if (value1 < value2) {
      return -1;
    } else if (value1 > value2) {
      return 1;
    } else {
      return 0;
    }
  }

  let result = compare(5, 6);

1.执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈

    ECStack = [//执行上下文栈
        globalContext
    ];

2.全局上下文变量即变量对象初始化,初始化的同时,compare 函数被创建,创建作用域链,并在内部属性[[scope]]中预装载全局变量对象。

  globalContext = {//变量对象初始化
        VO: [global],
        Scope: [globalContext.VO],
        this: globalContext.VO
    }

  compare.[[scope]] = [//compare在作用域链预装载全局变量对象
        globalContext.VO
  ];

3.执行 compare 函数,创建 compare 函数执行上下文,compare 函数执行上下文被压入执行上下文栈

  ECStack = [//执行上下文栈
        comapreContext,
        globalContext
    ];

4.comapre 函数执行上下文初始化并为变量赋值:
1)复制函数 [[scope]] 属性创建作用域链,
2)用 arguments 创建活动对象,
3)初始化活动对象,即加入形参、函数声明、变量声明,
4)将活动对象压入 comapre 作用域链顶端。

    comapreContext = {
        AO: {
            arguments: {
                0:5
                1:6
                length: 2
            },
            scope: undefined,
        },
        Scope: [AO, globalContext.VO],
    }

5.执行代码,函数执行完毕后返回,并将函数comapre的执行上下文从执行上下文栈中弹出。

    ECStack = [
        globalContext
    ];

compare()方法的作用域链如下图:
作用域链实际上是一个包含指针的列表,每个指针分别指向一个变量对象,但是物理上不会包含相应的对象。

image.png

闭包

《JavaScript高级编程》: 闭包是指那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。
MDN--闭包:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。

定义略显抽象,借用阮一峰老师的理解:闭包就是能够读取其他函数内部变量的函数。 由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

  var winVar = 'window-小白';

  function fun() {
    var funVar = 'fun-小白';
    console.log(winVar)//window-小白
    console.log(funVar)//fun-小白
  }

  fun();
  console.log(funVar)//Uncaught ReferenceError: funVar is not defined

函数作用域链如下图:当函数在作用域链顶端找不到winVar对象,就会去全局变量对象中寻找,而在winodow中,无法访问到fun()函数的变量,因此需要使用闭包。
image.png
闭包可以用在许多地方。它的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中

  function fun() {
    var funVar = 'fun-小白';
    return function () {
      return funVar;
    }
  }

  let variable = fun();
  let result = variable();
  console.log(result)//fun-小白

函数的作用域链如下图,在fun方法返回匿名函数后,匿名函数的作用域链被初始化为包含fun的活动对象和全局变量对象,虽然fun()函数执行结束后,其执行上下文的作用域链会销毁,但是在匿名函数的作用域链中,仍然有对他的引用。这样应该就不难理解为什么闭包可以读取函数内部的变量,也可以让变量的值始终保持在内存中了。
image.png

返回的匿名函数被保存在variable方法中,把variable()方法设置为null可以解除对函数的引用,从而让垃圾回收程序将内存释放掉。 variable = null;

同时因为闭包会保留包含它们的函数作用域,所以比其他函数更占内存,过度使用闭包会导致内存过度占用,所以不能滥用闭包,否则会造成网页的性能问题。

参考资料:


很白的小白
145 声望125 粉丝