资讯专栏INFORMATION COLUMN

谈谈JavaScript的词法环境和闭包(一)

AlphaWatch / 2660人阅读

摘要:换句话说,定义在闭包中的函数可以记忆它被创建时候的环境。词法环境的概念定义摘自百科。一个词法环境由一个环境记录项和可能为空的外部词法环境引用构成。中使用词法环境管理静态作用域。

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

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

闭包的概念

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

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

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

分析:

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

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

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

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

词法环境的概念

定义(摘自wiki百科)。

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

变量作用域

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

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

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

var x = 10;
function getX() {
    alert(x);
}
function foo() {
    var x = 20;
    getX();
}
foo();

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

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

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

词法环境包含两部分

环境记录

形参

函数声明

变量

其它...

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

环境记录初始化

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

形参会在初始化的时候定义值,但是函数内部定义的变量只声明不定义(不赋值),这个需要用JS中的Hoisting机制来解释,具体可以看这一篇文章:《理解 JavaScript(二):Scoping & Hoisting》。

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

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:
x: undefined(声明变量而非定义变量)
bar: undefined(声明变量而非定义变量)
外部环境(outer) null

step2: 执行x=10

全局环境
环境记录(record) foo:
x: 10()
bar: undefined(声明变量而非定义变量)
外部环境(outer) null

step3:执行var bar = foo(20)语句之前,将foo函数的环境记录初始化

foo 环境
环境记录(record) y: 20(定义形参)
bar:
z: undefined(声明变量而非定义变量)
外部环境(outer) 全局环境

step4:执行var bar = foo(20)语句,变量bar接收foo函数中返回的bar函数

foo 环境
环境记录(record) y: 20
bar:
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

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

一句话总结

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/80193.html

相关文章

  • 谈谈eval另

    摘要:直到有一天关于闭包某一天,小伙伴们讨论到说关于闭包变量的问题时,君指出,如果一个函数没有引用到其所处闭包的变量,那这个变量所指向的空间将被释放。对于的引擎而言,如,,,无论闭包中是否包含,它们都不会释放掉那些再也引用不到的变量。 之前在祼看ECMA262-5,在说到eval的地方,死活看不明白为什么会有一节专门扯到Direct Call to Eval: A direct ca...

    ybak 评论0 收藏0
  • 还担心面试官问闭包

    摘要:一言以蔽之,闭包,你就得掌握。当函数记住并访问所在的词法作用域,闭包就产生了。所以闭包才会得以实现。从技术上讲,这就是闭包。执行后,他的内部作用域并不会消失,函数依然保持有作用域的闭包。 网上总结闭包的文章已经烂大街了,不敢说笔者这篇文章多么多么xxx,只是个人理解总结。各位看官瞅瞅就好,大神还希望多多指正。此篇文章总结与《JavaScript忍者秘籍》 《你不知道的JavaScri...

    tinyq 评论0 收藏0
  • JavaScript基础系列---闭包及其应用

    摘要:所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。所以本文中将以维基百科中的定义为准即在计算机科学中,闭包,又称词法闭包或函数闭包,是引用了自由变量的函数。 闭包(closure)是JavaScript中一个神秘的概念,许多人都对它难以理解,我也一直处于似懂非懂的状态,前几天深入了解了一下执行环境以及作用域链,可戳查看详情,而闭包与作用域及作用域链的关系密不可分,所...

    leoperfect 评论0 收藏0
  • 《你不知道javascript》笔记_作用域与闭包

    摘要:建筑的顶层代表全局作用域。实际的块级作用域远不止如此块级作用域函数作用域早期盛行的立即执行函数就是为了形成块级作用域,不污染全局。这便是闭包的特点吧经典面试题下面的代码输出内容答案个如何处理能够输出闭包方式方式下一篇你不知道的笔记 下一篇:《你不知道的javascript》笔记_this 写在前面 这一系列的笔记是在《javascript高级程序设计》读书笔记系列的升华版本,旨在将零碎...

    galaxy_robot 评论0 收藏0
  • javascript闭包

    摘要:闭包的定义闭包是函数和声明该函数的词法作用域的组合。上面的和都是闭包。然而在一个闭包内对变量的修改,不会影响到另一个闭包中的变量。原因是赋值给的是闭包。由于循环在事件触发之前早已执行完毕,变量被三个闭包共享已经变成了。 闭包的定义: 闭包是函数和声明该函数的词法作用域的组合。 先看如下例子: function makeFn(){ var name = Mirror; f...

    binaryTree 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<