javascript学习-闭包

javascript学习-闭包

1.什么是闭包

大多数书本中对闭包的定义是:“闭包是指有权访问另外一个函数做用域中的变量的函数。”。这个概念过于抽象了,对初学者而言没啥帮助。好在《Javascript忍者秘籍》5.1中给了一个例子来进一步的解释了什么是闭包:javascript

            var outerValue= 'ninja';
            
            var later;
            
            function outerFunction() {
                var innerValue = "samurai";
                
                function innerFunction(paramValue) {
                    assert(outerValue == "ninja", "I can see the outerValue.");
                    assert(innerValue == "samurai", "I can see the innerValue.");
                    assert(paramValue == "wakizashi", "I can see the paramValue.");
                    assert(tooLater == "ronin", "Inner can see the tooLater.");
                }
                
                later = innerFunction;
            }
            
            assert(tooLater, "Outer can't see the tooLater.");
            
            var tooLater = "ronin";
            
            outerFunction();
            
            later("wakizashi");

测试结果是:前端

看,这个later指向的就是一个闭包,它实际指向了一个外部函数outerFunction中的一个内部函数innerFunction。当outerFunction函数被调用经过全局变量later将innerFunction函数从outerFunction函数这个封闭的监狱里放出来后,innerFunction函数就一会儿变得超级厉害了,成为了闭包,一旦调用闭包,它既能看见全局的outerValue,监狱里的innerValue,本身随身携带的paramValue,还能看见之前根本不认识的tooLater。java

固然了,我认为这里例子并不完整,为了形式的完整性,我给它增强一下:缓存

            asserts();

            test("函数闭包", function() {
                var before_outerFunction = "before_outerFunction";

                function outerFunction(outerParam) {
                    var before_innerFunction = "before_innerFunction";

                    function innerFunction(innerParam) {
                        return {
                            before_outerFunction : before_outerFunction,
                            after_outerFunction : after_outerFunction,
                            before_innerFunction : before_innerFunction,
                            after_innerFunction : after_innerFunction,
                            outerParam : outerParam,
                            innerParam : innerParam,
                            before_callClosure : before_callClosure,
                            after_callClosure : after_callClosure,
                        };
                    }

                    var after_innerFunction = "after_innerFunction";
                    
                    return innerFunction;
                }

                var after_outerFunction = "after_outerFunction";

                var closure = outerFunction("outerParam");

                var before_callClosure = "before_callClosure";

                var ret = closure("innerParam");
                
                assert(ret.before_outerFunction, "before_outerFunction");
                assert(ret.after_outerFunction, "after_outerFunction");
                assert(ret.before_innerFunction, "before_innerFunction");
                assert(ret.after_innerFunction, "after_innerFunction");
                assert(ret.outerParam, "outerParam");
                assert(ret.innerParam, "innerParam");
                assert(ret.before_callClosure, "before_callClosure");
                assert(ret.after_callClosure, "after_callClosure");

                var after_callClosure = "after_callClosure";
            });

测试结果是:闭包

结论就是,当闭包被调用的那一刻,它立刻就立地成佛了,既能看到眼前看到的,也能看到曾今看到的,前世此生,形形色色,全都历历在目啊。只有还没有发生的after_callClosure,那个实在是看不到。函数

难怪不止一本书中提到,只有理解了闭包才能真正的理解Javascript,这玩意就是一个反直觉的异类啊。性能

2.函数做用域链

若是只是认识一下什么是闭包,那上面的一段就够了,可是这远远称不上理解闭包。《Javascript忍者秘籍》在这里及其不负责任的开始大讲特讲怎么使用闭包:学习

  • 用闭包实现私有变量
  • 在回调函数中使用闭包
  • 在定时器中使用闭包
  • 用闭包实现函数的bind
  • 用闭包实现函数的curry化
  • 用闭包实现函数结果的缓存
  • 用闭包实现函数的包装

这个忍者师傅估计当时是喝多了,简单露了两手后就扔给咱们一堆的掌法、步法、剑法、刀法。惟独把最关键的内功心法给忘了。好了,不期望它了,仍是本身接着找师傅吧,有钱就是任性,请了一堆的师傅在桌上摆着,因此才这么有底气。因而我看到了《Javascript高级程序设计》这位牛逼的不行的师傅。高手就是高手,一上来就告诉我,要理解闭包,先翻到第四章看看什么是做用域链。立马翻过去看啊,4.2关于执行环境及做用域,只有短短的两页,大致意思是:经过执行环境、做用域链、活动对象,咱们实现了变量的一层层查找。得了,我智商不够,继续换老师,因而我又找到了《高性能Javascript》,第二章中的一小段,题目是“做用域链和标识符解析”,一样是短短的两页,字字珠玑,图文并茂,立马有种醍醐灌顶的感受。测试

每个Javascript函数都表示为一个对象,更确切的说,是Function对象的一个实例。this

当编译器看到下面的全局函数:

                function add(num1, num2) {
                    var sum = num1 + num2;
                    return sum;
                }

它经过相似下面的代码来建立函数对象:

var add = new Function("num1", "num2", "var sum = num1 + num2;\nreturn sum;");

Function函数中要自动完成做用域链的构造:

  1. 建立add函数对象的做用域链对象,并把引用保存在add函数对象的[[Scope]]属性中
  2. 把做用域链对象的第一项指向全局做用域对象

听起来是蛮复杂,用下面的图形象化一下:

当add被调用时,例如经过下面的代码:

var total = add(5, 10);

此次轮到引擎来干活了,它要完成下面的工做:

  1. 执行此函数会建立一个称为执行环境(execution context)的内部对象。一个执行环境定义了一个函数执行时的环境。函数每次执行时都对应的执行环境都是临时的,因此屡次调用同一个函数就会致使建立多个执行环境。当函数执行完毕,执行环境就被销毁。
  2. 每一个执行环境都有本身的做用域链,用于解析标识符。当执行环境被建立时,它的做用域链初始化为当前运行函数的[[Scope]]属性中的对象。这些值按照它们出如今函数中的顺序,被复制到执行环境的做用域链中。
  3. 这个过程一旦完成,一个被称为“活动对象”(activation object)的新对象就为执行环境建立好了。活动对象做为函数运行时的变量对象,包含了此函数的全部局部变量,命名参数,参数集合以及this。
  4. 而后活动对象被推入到执行环境做用域链的最前端。
  5. 函数执行过程当中每次解析标识符,就在执行环境的做用域链中从前日后的查找
  6. 函数执行完毕,执行环境被销毁,活动对象也随之被销毁。

再用书上的图形象化一下:

3.闭包与函数做用域链

仍然是《高性能Javascript》,第二章其中的一小段,题目是“闭包、做用域和内存”。给出的示例代码以下:

                function saveDocument(id) {                    
                }

                function assignEvents() {
                    var id = "xdi9592";
                    document.getElementById("save-btn").onclick = function(event) {
                        saveDocument(id);
                    }
                }

assignEvents()函数给一个DOM元素设置事件处理函数。这个事件处理函数就是一个闭包,它在assignEvents()执行时建立,而且能访问所属做用域的id变量。为了让这个闭包访问id,必须建立一个特定的做用域链。

当assignEvents()函数执行时,一个包含了变量id以及其余数据的活动对象被建立。它成为执行环境做用域链中的第一个对象,而全局对象紧随其后。当闭包被建立时,它的[[Scope]]属性被初始化为这些对象。

因为闭包的[[Scope]]属性包含了与执行环境做用域链相同的的对象的引用,所以会产生反作用。一般来讲,函数的活动对象会随着执行环境一同销毁。但引入闭包时,因为引用仍然存在于闭包的[[Scope]]属性中,所以激活对象没法被销毁。

当闭包被执行时,会建立一个执行环境,它的做用域链与属性[[Scope]]中所引用的两个相同的做用域链对象一块儿被初始化,而后一个活动对象为闭包自身所建立。

 

《Javascript高级程序设计》也有一个相似的例子,示例代码以下:

                function createComparisonFunction(propertyName) {
                    return function(object1, object2) {
                        var value1 = object1[propertyName];
                        var value2 = object2[propertyName];
                        if (value1 < value2) {
                            return -1;
                        } else if (value1 > value2) {
                            return 1;
                        } else {
                            return 0;
                        }
                    };
                }                

                //建立函数
                var compareNames = createComparisonFunction("name");
                //调用函数
                var result = compareNames({ name: "Nicholas" }, { name: "Greg" });
                //解除对匿名函数的引用(以便释放内存)
                compareNames = null;

函数compareNames被调用时的执行环境以下图:

这张图其实很容易引发误解,不少人觉得这张图是函数compareNames被调用时的一个快照,其实不是,当createComparisonFunction函数执行时,createComparisonFunction函数的执行环境是没错的。可是它返回的那个匿名函数是一个闭包,所以该匿名函数的做用域链会复制createComparisonFunction函数的执行环境的做用域链,而后createComparisonFunction函数结束,createComparisonFunction函数的执行环境被销毁,可是createComparisonFunction函数的活动对象由于被闭包引用了,因此没法销毁。

当compareNames执行时,它依然遵循着普通函数的执行流程:

  1. 建立函数的执行环境;
  2. 建立函数的执行环境的做用域链,并复制函数的做用域链;
  3. 建立函数的活动对象,并加入到函数的执行环境的做用域链的第一条

4.全局对象是什么

这个问题提的彷佛很没有水平啊,学习Javascript的初学者哪一个不知道全局对象的重要性呢。可是若是换一个角度来看,若是把全部Javascript代码认为是写在一个最外层的超级函数里的。那么当这个超级函数执行时,它应该也会继续函数调用的三板斧:

  1. 建立超级函数的执行环境;
  2. 建立超级函数的执行环境的做用域链,并复制超级函数的做用域链,此时为空;
  3. 建立超级函数的活动对象,并加入到超级函数的执行环境的做用域链的第一条

根据函数做用域链的检索机制和全局对象的用法,咱们彷佛能够获得一个推论:全局对象其实就是超级函数的活动对象。

再进一步,咱们定义的全部全局函数其实都是闭包,由于它们都把超级函数的活动对象,也就是全局函数复制到了本身的函数做用域链中。

再次回到咱们开头对闭包的测试用例,若是咱们不是直接返回内部函数,而是直接在外部函数里调用内部函数呢?

            test("函数闭包", function() {
                var before_outerFunction = "before_outerFunction";

                function outerFunction(outerParam) {
                    var before_innerFunction = "before_innerFunction";

                    function innerFunction(innerParam) {
                        return {
                            before_outerFunction : before_outerFunction,
                            after_outerFunction : after_outerFunction,
                            before_innerFunction : before_innerFunction,
                            after_innerFunction : after_innerFunction,
                            outerParam : outerParam,
                            innerParam : innerParam,
                            before_callClosure : before_callClosure,
                            after_callClosure : after_callClosure,
                        };
                    }

                    var after_innerFunction = "after_innerFunction";

                    var ret = innerFunction("xxx");
                    assert(ret.before_outerFunction, "before_outerFunction");
                    assert(ret.after_outerFunction, "after_outerFunction");
                    assert(ret.before_innerFunction, "before_innerFunction");
                    assert(ret.after_innerFunction, "after_innerFunction");
                    assert(ret.outerParam, "outerParam");
                    assert(ret.innerParam, "innerParam");
                    assert(ret.before_callClosure, "before_callClosure");
                    assert(ret.after_callClosure, "after_callClosure");
                    
                    return innerFunction;
                }

                var after_outerFunction = "after_outerFunction";

                var closure = outerFunction("outerParam");
                //log(closure);

                var before_callClosure = "before_callClosure";

                var ret = closure("innerParam");

                assert(ret.before_outerFunction, "before_outerFunction");
                assert(ret.after_outerFunction, "after_outerFunction");
                assert(ret.before_innerFunction, "before_innerFunction");
                assert(ret.after_innerFunction, "after_innerFunction");
                assert(ret.outerParam, "outerParam");
                assert(ret.innerParam, "innerParam");
                assert(ret.before_callClosure, "before_callClosure");
                assert(ret.after_callClosure, "after_callClosure");

                var after_callClosure = "after_callClosure";
            });

测试结果是:

一切和预想的同样,所谓的闭包并非return是才发生的,而是在内部函数被编译器建立函数对象的那一刻就决定的。为函数的做用域链复制当前函数执行环境的做用域链。用下图形象化一下:

最关键的是这套函数定义、函数调用机制是能够无限嵌套下去的,并且函数定义和函数调用的时机也是分离的。每一个函数执行时都有本身的活动对象负责管理本身的做用域,执行环境做用域链的职责不过是把这些嵌套的函数的各自的活动对象串联起来。函数的做用域链的职责不过是至关于一个中间变量,负责保存上一级函数执行环境的做用域链。

因此内部函数直接在外部函数内调用的话也能访问到内部函数的相关变量,不是由于内部函数调用时真的能够看见外部函数,而是由于内部函数的执行环境的做用域链中已经复制了外部函数的执行环境的做用域链。函数的执行环境的做用域链是自完备的,函数调用时只会在本身的函数的执行环境的做用域链中查找,其实它根本就不知道外部函数或者全局函数什么的。

若是内部函数不return出去的话,一切都会随着外部函数调用完成,外部函数的执行环境对象被销毁,致使外部函数的执行环境做用域链的被销毁,致使外部函数活动对象被销毁,与此同时内部函数对象做为局部变量也会被销毁。这一系列的销毁过程将内部函数的做用域链复制了外部函数的执行环境的做用域链的“罪证”被掩盖得完美无缺。临时的外部函数的活动对象也绝对不会跑到笼子外面去。

可是一旦内部函数被return出去的话,内部函数的做用域链复制了外部函数的执行环境的做用域链的“罪证”被暴露了,本该被销毁的外部函数的活动对象也意外地活了下来,并随时等待着随着内部函数被调用而继续呼风唤雨。其实不只仅是外部函数的活动对象,外部函数的执行环境的做用域链上的全部活动对象都意外的活了下来,若是咱们构造一个二层以上的函数嵌套,不断地进行函数的定义和函数的调用,最后返回一个最内层的函数,你就会发现一组本该死去的活动对象都意外的活了下来。

闭包执行后直接设置为null能够保证那些意外活下来的活动对象被清除吗?按道理应该是这样的,不过很不肯定,这取决于引擎的垃圾回收机制怎么玩的,若是按照引用计数的垃圾收集方式,这个推论应该是真的,可是目前大多数引擎采用的倒是标记清除的垃圾收集方式,这就很难保证这个推论是真的了。或者更简单的,随着闭包引用变量的自动清除,也能让那些活动对象寿终正寝,也是说得过去的。这或许也正好解释了看到的Javascript代码中不多有对闭包主动设置为null的。

在Javascript的世界里,其设计思想果真仍是一如既往的单纯质朴。

如何管理函数?Javascript回答说用函数对象。

如何管理函数的做用域?Javascript回答说用活动对象。

若是函数调用有嵌套呢?Javascript回答说用做用域链,把活动对象串起来。

若是一个外部函数返回了一个内部函数,致使外部函数的活动对象泄露了怎么办?Javascript回答说那就叫作闭包吧。

 5.后记

本文的一切功劳属于那些经典书籍的做者们,我不生产知识,我只是知识的搬运工。本文的一切错误属于我我的,谁让我是初学者呢,有时候搬错了也是不免的,那就在不断地错误不断地改正中不断地成长吧。

相关文章
相关标签/搜索