美文网首页脑吉
JavaScript - 作用域和“闭包”

JavaScript - 作用域和“闭包”

作者: NARUTO_86 | 来源:发表于2014-09-02 21:46 被阅读208次

    继续一个人自言自语_

    今天想聊聊 JavaScript 的作用域,以及“闭包”。当然,仍旧带着我的个人特色。

    作用域

    JavaScript 据我了解只有一种作用域,叫做“函数作用域”,先看栗子:

    var a = "a-out";
    (function () {
        console.log(a);
        var a = "a-in";
    })();
    

    你觉得上面匿名函数执行后,会输出什么?

    实际试下,会发现是undefined,这个我慢慢来说吧。

    首先,JavaScript 没有“块级作用域”,不能在花括号中定义“局部变量”。所有的变量的作用域都是函数级的,也就是说,在函数内部任意的地方声明的变量,在函数内的任意位置都可以使用。

    当然,例外就是,不在任何函数内部的变量,就成了“全局变量”啦。同样,在函数内部,忘记使用 var 声明就直接使用的变量,也会成为全局变量,所以很多时候会把函数内部要使用的变量都在头部进行显式声明,以尽量避免麻烦。

    回过头来看上面的栗子:

    1. 第一行,声明了一个全局变量 a,然后进行了赋值。

    2. 匿名函数的第一行,向控制台输出变量 a 的值,由于函数内部的确声明了 a,所以这里输出内部变量 a 的值。但是对内部变量 a 的赋值在当前行的后面,目前该变量没有值,所以输出的是 undefined

    3. 匿名函数的第二行,声明了内部变量 a,然后进行了赋值。

    对于这件事情我的理解:

    既然 JavaScript 只有“函数作用域”(我说的),所以脚本解释器会先扫描下函数内部,识别出所有的声明的变量,记录下来。然后,在执行函数的这个阶段,遇到一个“变量名”,就先在函数内部的变量列表里面进行查找,找到了就把这个内部变量的当前值交给当前语句使用(当然如果是赋值语句,则为变量“绑定”了一个新的值)。

    那么,如果在函数内部,使用一个变量时,该变量并没有在函数内部声明呢?

    我把上面的栗子修改下:

    var a = "a-out";
    (function () {
        console.log(a);
        // var a = "a-in";
    })();
    

    试一下,会发现这里匿名函数打印出的是"a-out"。所以,在函数内部没有声明这个变量的话,就在函数的外部来找啦。对于多级嵌套的函数来说,就该是一层一层往外找,直到找到同名的变量,或者到“顶层”也找不到就停下来(这个情况下如果执行函数就出错了,提示变量没有定义)。

    当然,这个查找变量的过程并不直观,仅仅是我的描述而已,希望能帮你理解而不是相反。

    对于函数的参数,我也看作是函数的内部变量(我不用“局部变量”的说法,但我想你大概知道我指的是什么),不过其值是要等到函数执行时才能确定的。

    把函数的定义,和函数的执行分开来看。

    函数定义时,是在一个静态的环境下,函数的内外部的环境是固定的。尽管有很多变量的值还在变动,甚至只在每次执行时才能确定,但这并不妨碍我们去“引用”它。在函数执行的过程中,在每个具体的引用到变量的位置,都会被变量当时的值所替代(同样,声明语句例外,是重新赋值)。

    特别地,在函数定义阶段(或者说解释器解析而非执行函数时),能够使用哪些变量,也是确定的了。例如,函数内部声明了一个 a 变量,那么函数内部使用到 a 变量的语句,就是跟这个 a 关联的了。而如果内部没有,则在一层层的外部作用域(外部函数的作用域,如果有的话)里查找,如果还没有的话呢?那么你执行函数时就报错了呗。

    接着这个话题,我们引出“闭包”。

    闭包

    对于“闭包”这样严肃的东西,还是来看维基百科的定义:

    在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

    来看一个栗子:

    var count = (function () {
        var i = 0;
        return function () {
            return ++i;
        };
    })();
    
    count(); // 1
    count(); // 2
    

    在这个栗子中,有两个匿名函数,其中一个嵌套在另一个的内部。外层的匿名函数被立即执行了(通过 (function () {})() 的方式),然后的是内部的匿名函数,被作为返回值赋给了变量 count。所以,经过上面的过程,count 的值是一个函数对象。

    而 count 所关联的这个函数比较特别,在定义时,它引用了外部函数的变量 i,于是,外部变量 i 和这个函数构成了上面定义中的“闭包”,也就产生了上面的现象。

    那么这一切,和这个诡异的名字一起,有什么作用呢?只是让人觉得迷惑,或者让不熟悉的人大呼“NB”?

    我曾经看到过一种说法,是说使用闭包是为了在 JavaScript 中提供一种使用局部变量的机制。且不论这个对于闭包的评价本身正确与否,我们来试着理解下“局部变量”这回事。还是举个栗子:

    function Person(name) {
        this.name = name;
        this.getName = function () {
            return this.name;
        };
    }
    

    假设我们希望,其他人在使用到这个 Person 类(暂且叫做“类”吧,后面我想专门写一篇东西来聊聊 JavaScript 的继承和类)时,只能通过 getName() 来获取 name,而不能直接访问 name 并改变其值的话,我感觉不太现实。因为 JavaScript 没有提供像 private, public 这样的东东,所有的东西都默认是公开的。而如果非要这样做,毕竟这样做也是有着合理的应用场景的,通常可以通过闭包来严格实现(只是把属性名改为类似 _name 这样来提示他人不要乱改,毕竟不严格不是):

    function Person(name) {
        var _name = name;
        this.getName = function () {
            return _name;
        };
    }
    

    当然,前面提到过,也可以把函数的参数看作是内部变量,所以也可以直接这样:

    function Person(name) {
        this.getName = function () {
            return name;
        };
    }
    

    我们来使用下这个“类”:

    var me = new Person("luobo");
    me.getName(); // "luobo"
    

    显然,没有 name 属性,也就无法直接修改这个值啦。(当然,如果要作为“私有”成员使用的是对象,那么即便采用上述方法,由于返回的是对象本身,所以仍旧可以修改对象)

    回到上面的问题,关于闭包的作用,我还是觉得:闭包是语言本身提供的一种机制,并不见得就一定是为了什么特定目的而创造的,更加不会是为了创造“局部变量”的这一个目的。根据自己的需要,在合适的地方使用它就是了,只要你是真的会用就好_

    小结

    抱歉,今天情绪不佳,写东西不是很有激情,所以上面的文字尽管我的确花了心思,但自己都不太满意。作用域和“闭包”是我理解的 JavaScript 中的一个很重要的主题,花了很长时间我才有了上面的那些体会,但是叙述地有点没有头绪啦。

    关于作用域,我认为先要对于 JavaScript 代码的执行有一定的理解。(我说说我的理解吧,欢迎交流。)在浏览器环境下,没有写在任何函数内部的语句,就直接执行了。写在函数内部的代码,则只有等到函数被调用的时候,才会执行。但浏览器虽然没有执行函数,还是会先把函数“读”一遍,“理解”了之后记录下来。这样当任何时候需要使用这个函数的时候,就能直接拿来,结合当时的环境来执行啦。

    这个过程的细节中就有关作用域、闭包的身影啦。

    当然上面是我个人的理解,描述也不够准确和正确。但是我想对于函数的定义和执行的机制有更深入的理解还是很必要的,特别是想真的对 JavaScript 这门语言有更深入的理解的话。显然,我也还需要加油啊!

    关于“闭包”,这真的是一个“高级”的话题。而且,在不领会闭包的原理的情况下,很有可能不知不觉就给自己挖了一个坑出来,我就干过。

    var arr = ['a', 'b', 'c'], funcs = [];
    for (var i = 0, len = arr.length; i < len; i++) {
        funcs[i] = function () {
            return arr[i];
        };
    }
    

    上面这个造作的栗子中,我的本意是:得到一个数组 funcs,该数组的每一项都是一个返回数组 arr 中对应位置的值的函数。有点绕,不过我想你能明白是什么意思。但是,结果却并非如此:

    funcs[1](); // undefined
    

    奇怪,不应该返回 arr[1] 也就是 'b' 吗?

    曾经我为此烦恼过....后来我意识到,我不知不觉用闭包给自己挖了这个坑。

    如果你还没有看明白(当然,我的叙述本身就乱,难为你了),我来给些提示:

    • 试着在控制台输出变量 i,看下当前值
    • 然后在控制台输出 arr[i],看下当前值
    • for 循环中,其实每次构建的匿名函数,返回的就是 arr[i]
    • 通过 i = 1 把变量 i 的值改为 1
    • 再执行下上面的 funcs[1](),或者 funcs[0]() funcs[2]() 都一样,看下结果你应该就能明白了....

    好吧,这就是闭包。

    相关文章

      网友评论

      • NARUTO_86:据上次我自己的评论,又是一年过去了。
        “闭包”不仅在工作中使用着,在找工作时也会时不时被问及,已经是Web前端开发必须了解的基础知识点。
        有趣的是,前天遇到的一道面试题,跟我文章中第一个例子是差不多的样子。呵呵。
      • NARUTO_86:今天再回过头来看当时写的东西,很高兴当时有勇气来记录自己认为正确的理解,尽管现在看来漏洞太多。
        作用域、闭包这些东西,现在看来,远非当初想得那样简单,或者,确切地说,并非是更复杂,而是有更丰富的细节。这些细节,虽然现在的我也还不甚明了,可是已经“知道”,当时却是不知道。
        更深入的学习吧。
        不知道自己不知道 > 知道自己不知道 > 不知道自己知道 > 知道自己知道
        看山是山 > 看山不是山 > 看山还是山
      • 落笔:最后“小结”里面的例子,《JavaScript高级程序设计》解释:“闭包只能取得包含函数中任何变量的最后一个值,闭包所保存的是整个变量对象,而不是某个特殊变量”,这是作用域链配置机制引出的一个副作用。
      • NARUTO_86:先给自己来一条评论,缓解一下尴尬的气氛。

        很早之前,我对于“语句”和“表达式”是很模糊的,感觉是一类东西。后来,在读一些文章中慢慢发现有些不对劲,直到我专门关注这件事之后,才发现了更深入的东西,也有了更多的理解。这里我就不说自己的理解了,如果你还不清楚这两个的东西的区别,建议你专门去查一下。

        友情提示:在 JavaScript 中理解了“表达式”这回事,再回过头来看一些东西就好理解了,例如 IIFE(在我曾经举过的栗子中就有)。

      本文标题:JavaScript - 作用域和“闭包”

      本文链接:https://www.haomeiwen.com/subject/dsyytttx.html