12

一个资深的同事在我出发去面试前告诫我,问JS知识点的时候千万别主动提闭包,它就是一个坑啊!坑啊!啊!

闭包确实是js的难点和重点,其实也没那么可怕,关键是机制的理解,可以和函数一起单独拿出来说说,其实关于闭包的解释很多文章都写得比较详细了,这篇文章就作为自己学习过程的记录吧。

闭包的概念

首先明确一下闭包的概念:

MDN (Mozilla Develop Network) 上的对闭包的定义:

闭包是指能够访问自由变量的函数 (变量在本地使用,但在闭包中定义)。换句话说,定义在闭包中的函数可以“记忆”它被创建时候的环境。

分析:

  • 闭包由函数和与其相关的引用环境(词法环境)的组合而成

  • 闭包允许函数访问其引用环境(词法环境)中的变量(又称自由变量)

  • 广义上来说,所有JS的函数都可以称为闭包,因为JS函数在创建时保存了当前的词法环境

还是很拗口有木有,一脸懵逼的时候就应该从基础的概念开始找,所以我们来谈谈词法环境。

词法环境的概念

定义(摘自wiki百科)。

词法环境是一个用于定义特定变量和函数标识符在ECMAScript代码的词法嵌套结构上关联关系的规范类型。一个词法环境由一个环境记录项和可能为空的外部词法环境引用构成。

变量作用域

一般来说,在编程语言中都有变量作用域的概念,每个变量都有自己的生命周期和作用范围。
作用域有两种解析方式:

  1. 静态作用域
    又称为词法作用域,在编译阶就可以决定变量的引用,由程序定义的位置决定,和代码执行顺序无关,用嵌套的方式解析。

  2. 动态作用域
    在程序运行时候,和代码的执行顺序决定。用动态栈动态管理。

var x = 10;
function getX() {
    alert(x);
}
function foo() {
    var x = 20;
    getX();
}
foo();
  1. 在静态作用域下:
    全局作用域下有x, getX, foo三个变量,getXfoo都有自己的作用域。执行foo函数的时候,getX()被执行,但是getX的定义位置在全局作用域下的,取到的x是10,而不是20

  2. 在动态作用域下:
    运行这段代码时,先把x=10getXfoo按顺序压栈,然后执行foo函数,在函数中把x=20压栈,然后执行getX(),此时距离栈顶最近的x值为20,因此alert的值也是20

JavaScript使用的变量作用域是静态作用域。JS中作用域简单分为两部分:全局作用域和函数作用域。ES5中使用词法环境管理静态作用域。

词法环境包含两部分

  • 环境记录

    • 形参

    • 函数声明

    • 变量

    • 其它...

  • 对外部词法环境的引用(outer)

环境记录初始化

一段JS代码执行之前,会对环境记录进行初始化(声明提前),即将函数的形参、函数声明和变量先放入函数的环境记录中,特别需要注意的是:

以下面这段代码为例,解析环境记录初始化和代码执行的过程:

var x = 10;
function foo(y) {
    var z  = 30;
    function bar(q) {
        return x + y + z + q;
    }
    return bar;
}
var bar = foo(20);
bar(40);
  • step1:初始化全局环境

全局环境
环境记录(record) foo: <function>
x: undefined(声明变量而非定义变量)
bar: undefined(声明变量而非定义变量)
外部环境(outer) null
  • step2: 执行x=10

全局环境
环境记录(record) foo: <function>
x: 10()
bar: undefined(声明变量而非定义变量)
外部环境(outer) null
  • step3:执行var bar = foo(20)语句之前,将foo函数的环境记录初始化

foo 环境
环境记录(record) y: 20(定义形参)
bar: <function>
z: undefined(声明变量而非定义变量)
外部环境(outer) 全局环境
  • step4:执行var bar = foo(20)语句,变量bar接收foo函数中返回的bar函数

foo 环境
环境记录(record) y: 20
bar: <function>
z: 30(定义z)
外部环境(outer) 全局环境
  • step5:执行bar函数之前,初始化bar的词法环境

bar环境
环境记录(record) q: 40(定义形参q)
外部环境(outer) foo环境
  • step6:在foo函数内执行bar函数

    x + y + z + q = 10 + 20 + 30 + 40 = 100 

其实说了那么多,也是想强调一点:形参的值在环境初始化的时候就赋值了!因此形参的作用之一就是保存外部变量的值

一道闭包的面试题

查了一下关于闭包的面试题,用具体的例子说明闭包的应用场景。
最常见的答案来自于《JavaScript高级程序设计(第3版)》p181:

例子:

function creacteFunctions() {
    var result = new Array();
    for (var i = 0; i < 10; i++) {
        result[i] = function () {
            return i;
        }
    }
    return result;
}

这个函数返回了长度为10的函数数组,假设我们调用函数数组的第3个函数,在控制台中输入creacteFunctions()[2](),即执行函数数组里面的第三个函数,creacteFunctions()返回函数数组,[2]是取第三个函数的引用,最后一个()是执行第三个函数,返回结果却并不是预期的2,而是10.

因此,为了能够让闭包的行为符合预期,需要创建一个匿名函数:

function creacteFunctions() {
    var result = new Array();
    for (var i = 0; i < 10; i++) {
        result[i] = function (num) {
            return function() {
                return num;
            }
        }(i);
    }
    return result;
}

此时在控制台中输入creacteFunctions()[2](),即执行函数数组里面的第三个函数,返回的就是预期中的2
有了词法环境的初始化过程,这里也就非常容易理解了。匿名函数的形参num保存了每次执行的i的值。在function(num){...}(i)这个结构中,i作为形参num的实际值执行这个匿名函数,因此每次循环中的num直接初始化为i的值。
为了更清楚的提取这部分结构,我们将匿名函数命名为helper:

var helper = function (num) {
    return function() {
        return num;
    }
}

用helper函数重写第二段代码:

var helper = function (num) {
    return function() {
        return num;
    }
}
function creacteFunctions() {
    var result = new Array();
    for (var i = 0; i < 10; i++) {
        result[i] = helper(i);
    }
    return result;
}

在控制台中输入creacteFunctions()[2](),输出的也是预期中的2

未完待续哦,闭包可以讲的东西太多啦!

一句话总结

真正理解了作用域也就理解了闭包.


sherrylang
62 声望1 粉丝

程序媛