JavaScript深刻之闭包

JavaScript深刻系列第八篇,介绍理论上的闭包和实践上的闭包,以及从做用域链的角度解析经典的闭包题。git

定义

MDN 对闭包的定义为:github

闭包是指那些可以访问自由变量的函数。面试

那什么是自由变量呢?闭包

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

由此,咱们能够看出闭包共有两部分组成:函数

闭包 = 函数 + 函数可以访问的自由变量post

举个例子:ui

var a = 1;

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

foo();复制代码

foo 函数能够访问变量 a,可是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,因此 a 就是自由变量。this

那么,函数 foo + foo 函数访问的自由变量 a 不就是构成了一个闭包嘛……spa

还真是这样的!

因此在《JavaScript权威指南》中就讲到:从技术的角度讲,全部的JavaScript函数都是闭包。

咦,这怎么跟咱们平时看到的讲到的闭包不同呢!?

别着急,这是理论上的闭包,其实还有一个实践角度上的闭包,让咱们看看汤姆大叔翻译的关于闭包的文章中的定义:

ECMAScript中,闭包指的是:

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

接下来就来说讲实践上的闭包。

分析

让咱们先写个例子,例子依然是来自《JavaScript权威指南》,稍微作点改动:

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

var foo = checkscope();
foo();复制代码

首先咱们要分析一下这段代码中执行上下文栈和执行上下文的变化状况。

另外一个与这段代码类似的例子,在《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 值呢?

以上的代码,要是转换成 PHP,就会报错,由于在 PHP 中,f 函数只能读取到本身做用域和全局做用域里的值,因此读不到 checkscope 下的 scope 值。(这段我问的PHP同事……)

然而 JavaScript 倒是能够的!

当咱们了解了具体的执行过程后,咱们知道 f 执行上下文维护了一个做用域链:

fContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}复制代码

对的,就是由于这个做用域链,f 函数依然能够读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即便 checkscopeContext 被销毁了,可是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然能够经过 f 函数的做用域链找到它,正是由于 JavaScript 作到了这一点,从而实现了闭包这个概念。

因此,让咱们再看一遍实践角度上闭包的定义:

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

在这里再补充一个《JavaScript权威指南》英文原版对闭包的定义:

This combination of a function object and a scope (a set of variable bindings) in which the function’s variables are resolved is called a closure in the computer science literature.

闭包在计算机科学中也只是一个普通的概念,你们不要去想得太复杂。

必刷题

接下来,看这道刷题必刷,面试必考的闭包题:

var data = [];

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

data[0]();
data[1]();
data[2]();复制代码

答案是都是 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] 函数的时候,data[0] 函数的做用域链发生了改变:

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] 是同样的道理。

下一篇文章

JavaScript深刻之参数按值传递

相关连接

若是想了解执行上下文的具体变化,不妨按部就班,阅读这六篇:

《JavaScript深刻之词法做用域和动态做用域》

《JavaScript深刻之执行上下文栈》

《JavaScript深刻之变量对象》

《JavaScript深刻之做用域链》

《JavaScript深刻之从ECMAScript规范解读this》

《JavaScript深刻之执行上下文》

深刻系列

JavaScript深刻系列目录地址:github.com/mqyqingfeng…

JavaScript深刻系列预计写十五篇左右,旨在帮你们捋顺JavaScript底层知识,重点讲解如原型、做用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

若是有错误或者不严谨的地方,请务必给予指正,十分感谢。若是喜欢或者有所启发,欢迎star,对做者也是一种鼓励。

相关文章
相关标签/搜索