js闭包(看完必懂)

前端这点事 76 0

一、理论上的闭包

从技术理论的角度讲,所有的JavaScript函数都是闭包。

闭包定义:闭包是指那些能够访问自由变量的函数。

自由变量:是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

由此,可以看出闭包共有两部分组成:闭包 = 函数 + 函数能够访问的自由变量

举个例子:

var a = 1;

function foo() {
    console.log(a);
}
foo();

foo 函数可以访问变量 a,但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,所以 a 就是自由变量。 那么,函数 foo + foo 函数访问的自由变量 a 就构成了一个闭包……

从技术理论的角度讲,所有的JavaScript函数都是闭包。

显然上面讲述的并不是我们实践中用的闭包,我们再接着往下看。

 

二、实践上的闭包

上面是理论上的闭包,其实还有一个实践角度上的闭包。

先举个栗子:

function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope();
console.log(foo());  					//'local scope'

我们先分析一下这段代码中执行上下文栈和执行上下文的变化情况。 注:如果看不懂以下的执行过程,建议先阅读《JavaScript深入 执行上下文(五):整个过程》。

这里直接给出简要的执行过程:

  • (1)进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
  • (2)全局执行上下文初始化
  • (3)执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈
  • (4)checkscope 执行上下文初始化,创建活动对象、作用域链、this等
  • (5)checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
  • (6)执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈
  • (7)f 执行上下文初始化,创建变量对象、作用域链、this等
  • (8)f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

了解到这个过程,我们应该思考一个问题,那就是: 当 f 函数执行的时候,checkscope 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢?

1. [[scope]]属性

每个函数都有一个内部属性[[scope]](即作用域链) 现在我们根据上面谈的程序具体执行过程,来看下f函数的内部属性[[scope]],即f 执行上下文维护的作用域链:

fContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

没错,, 就是因为上面这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值(即 变量scope)。 说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它。从而实现了闭包这个概念。

那问题又来了:checkscopeContext都被销毁了,为什么checkscopeContext.AO 还能活在内存中呢?

如果你知道 JavaScript的垃圾回收机理,你就懂了。

 

2. JavaScript垃圾回收

JavaScript垃圾回收的机理:垃圾收集器 会跟踪找出不再使用的变量,然后 每隔固定时间间隔 释放掉其内存。

再看上面的例子:

  • ① checkscope函数没被谁引用或使用着,说明它执行完后会被垃圾收集器销毁;checkscopeContext也没被谁引用或使用着,所以在checkscope函数执行完毕后,它也会被一同销毁。
  • ② checkscope返回的f函数被foo所引用着,说明我们还会使用f函数,所以f函数不被销毁。且f函数执行上下文的[[scope]]属性(即 作用域链)还引用着 checkscopeContext.AO,说明我们还会使用 checkscopeContext.AO,所以不被销毁。

综述: 每个函数都有一个内部属性[[scope]](即作用域链),而正因为f函数没被销毁,所以该属性也被保留着;又因为作用域链的本质是一个指向 变量对象/活动对象 的指针列表(它只是引用 不包含实际对象),所以作用域链上的这些对象不会被垃圾收集器销毁,所以我们可以通过f函数的作用域链找到 它的父级乃至父父级的变量。

让我们再看一遍实践角度上闭包的定义:

  • (1)即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
  • (2)在代码中引用了自由变量

再总结一遍~

ECMAScript中,闭包指的是:

  • 从理论角度:闭包指所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  • 从实践角度:以下函数才算是闭包:即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)在代码中引用了自由变量

 

三、必刷题

例1:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

输出: 3 3 3

让我们分析一下原因: 当执行到 data[0] 函数之前,此时全局上下文的 VO 为:

globalContext = {
   VO: { data: [...],   i: 3 }
}

当执行 data[0] 函数的时候,data[0] 函数的作用域链为:

data[0]Context = {
   Scope: [AO, globalContext.VO]
}

data[0]Context 的 AO 并没有 i 值,所以会从 globalContext.VO 中查找,i 为 3,所以打印的结果就是 3。

data[1] 和 data[2] 是一样的道理。

所以让我们改成闭包看看:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
     return function(){
        console.log(i);
     }
  })(i);
}

data[0]();
data[1]();
data[2]();

当执行到 data[0] 函数之前,此时全局上下文的 VO 仍为:

globalContext = {
   VO: { data: [...],   i: 3 }
}

跟没改之前一模一样。

但当执行 data[0] 函数(即 return的函数)的时候,其作用域链为:

data[0]Context = {
   Scope: [AO, 匿名函数Context.AO,globalContext.VO]
}

匿名函数执行上下文的 AO 为:

匿名函数Context = {
  AO: {
    arguments: {
      0: 0,
      length: 1
    },
    i: 0
  }
}

data[0]Context 的 AO 并没有 i 值,所以会沿着作用域链从匿名函数Context.AO 中查找,这时候就会找 i 为 0,找到了就不会往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值为3),所以打印的结果就是 0。 data[1] 和 data[2] 是一样的道理。


其实我们要想输出0 1 2,可以直接将上面代码改为:

var data = [];

for (let i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

ES6中加入了块级作用域。 我们每创建一个函数会创建一个作用域。同理上面代码,我们用let声明i后,相当于每进行一次for循环就创建了一个(块级)作用域。每个作用域的AO都保存了一个不同的i值。

执行 data[0] 函数时,由于我们要打印i值,所以会沿着作用域链回溯寻找:首先会在当前匿名函数的作用域寻找i值,发现没有;再到块级作用域中找,发现有且为0,所以打印0;之后同理,输出:0 1 2


例2:

var globals = 0;
function test(parameter){
   var outerVal = 0;
   var outerVal2 = 0;
   console.log('outerVal2:',++outerVal2);
   return function(){
      var innerVal = 0;
      console.log('globals:',++globals);
      console.log('outerVal:',++outerVal);
      console.log('innerVal:',++innerVal);
      console.log('parameter:',++parameter);
   }
}

var a = test(0);
a();
a();

输出:

JavaScript深入 闭包

 

(1) 在执行到 a() 函数(即return的匿名函数)之前,此时全局上下文的 VO 为:

globalContext = {
   VO: { 
     globals: 0, 
     test:ƒ test(parameter),
     a:ƒ() 
   }
}

而test函数执行上下文的 AO 为:

testContext = {
    AO: {
        arguments: {
            0: 0,
            length: 1
        },
        parameter: 0,
        outerVal: 0,
        outerVal2: 0,
    }
}

(2) 当执行 a() 函数的时候,a() 函数的作用域链为:

aContext = {
    Scope: [AO, testContext.AO, globalContext.VO]
}

用上面的理论,解释这个例子,输出同理。

这里需要注意的是:第一次执行a()时,创建了a函数的执行上下文(aContext),执行完后销毁;第二次执行a()时,再次创建aContext,然后再次销毁。虽然aContext两次被销毁,但a函数的[[scope]]属性一直都留在内存里。


下面我们看一个误用闭包的例子~

例3:

function test(){
    var outerVal = 0;
    return function(){
        console.log(++outerVal);
    }
}
test()();
test()();

输出:1 1

为什么不是输出 1 2 呢? 首先你要知道,test()即返回的匿名函数,test()()即执行匿名函数。

再根据 JS垃圾回收的机理: 因为test函数返回的匿名函数没有被其他变量引用或使用着,说明我们不再继续使用该匿名函数,所以垃圾收集器会将其销毁。所以每次执行test()()后,变量outerVal都会被销毁。


为了方便闭包的分析,我们可以认为每个函数的[[scope]]属性(即作用域链),一直活在内存里,无论这个函数是否执行完毕。

标签: JavaScript

发表评论 (已有0条评论)

还木有评论哦,快来抢沙发吧~