闭包函数

理解闭包,首先需要理解JS的作用域链(简单理解JS中的变量作用域)。前面这篇关于JS作用域的博文是大概一个多月前刚接触JS的时候写的,内容十分粗糙,当时用的是一个“变量仓库”的概念来理解JS的作用域。最近刚看了《JS高级程序设计》这一块,知道了执行环境的概念,发现之前的理解居然算是歪打正着。接下来就是理清这些知识点了,其实主要的内容也就是一个函数从创建到结束调用整个过程中的细节问题。 <!--more-->

1. 函数的流程

1.1. 创建函数

在创建函数的时候,会创建一个预先包含包含全局变量对象的初始作用域链,这个作用域链保存在函数内部的[[Scope]]属性中;

1.2. 调用函数

  • 在函数被调用时,会创建一个执行环境,这里可以大致地将执行环境理解为函数作用域。执行环境定义了变量或函数能够有权访问的其他数据,并决定了他们各自的行为;
  • 完成执行环境创建之后,会复制函数的[[Scope]]属性中的对象,并构建起该执行环境的实际作用域链
  • 完成作用域链创建之后,执行环境会创建一个表示函数内部变量的变量对象,并通过参数来初始化这个函数的变量对象,所有在该执行环境中定义的变量和函数都保存在这个变量对象中,作为该对象的属性和方法。(需要注意的是每个函数在调用的时候会自动取得this和arguments这两个变量,而该函数的变量对象不包含这两个特殊变量,取而代之的是一个arguments属性,该属性引用参数类数组对象);
  • 完成变量对象创建之后,该变量对象会被推入执行环境作用域链的前端,因此,作用域链的本质是一个指向变量对象的指针列表。
  • 函数的执行过程中需要注意的细节是就是变量声明提前,这里不再赘述。

1.3. 执行完毕

在一般的情况下,当函数执行完毕之后,局部变量对象就会被删除,内存中仅仅保留着作用域链末端的全局变量对象,但是在闭包函数的情况下就不一样了。

2. 闭包函数

闭包函数指的是有权访问另一个函数作用域中的变量的函数,因此在一个函数内部所创建的函数就是一个闭包函数。 定义的内部函数会将外部函数的变量对象添加至内部函数的作用域链中,即此时初始化的作用域链保活外部函数的变量对象与全局变量对象这两个变量对象,闭包函数的两个特点:

  • 第一个就是作用域链的特点,由于外部函数的变量对象包含了外部执行环境中定义的全部变量,因此内部函数就可以访问外部函数的变量;
  • 第二个的特点是当外部函数执行完毕之后,其执行环境的作用域链会被删除,却不会删除其自身的局部变量对象,因为其变量对象仍然保存在闭包函数的作用域链中,只有当闭包函数被销毁后,外部函数的变量对象才会被销毁,相当于延长了外部函数变量对象的生命周期。 由于闭包函数的作用域链中保存的是外部函数的变量对象,因此只能获取外部变量的最后一个值(还记得那个老土的“变量仓库”的比喻么?后定义的值会覆盖先定义的值,同名函数会覆盖同名变量的值),比如下面这个例子:
    function out(){
        var arrF = [];
        for (var i = 0; i < 5; ++i){
            arrF[i] =  function(){
                return i;
            }
        }
        return arrF;
    }
    var a = out();
    for ( var i = 0; i < a.length; ++i) {
        alert(a[i]());//这里输出的全部都是5,而不是0,1,2,3,4
    }

JS中的匿名函数的执行环境具有全局性,所以其this通常指向window(这是书上的原话,且貌似是ES3时代的故意设计,具体看这里,因此暂时没法深究。)。在闭包函数中调用this就可能出现问题,比如下面这个例子:

    var o = {};
    function out(){
        var x = (function(){
            return this;
        })();
        return x;
    }
    o.f = out;
    alert(o.f());//object Window

3. 闭包的应用

呃,就是这样,闭包就说完了!也不知道我理解的对不对,反正写下来就这几句话。闭包在JS中有如下几点应用:

3.1. 创建块级作用域

可以模仿创建块级作用域,具体的做法是创建并立即调用一个函数,这样就可以避免全局变量污染,并且看着也十分高大上的样子,貌似有三种这样的写法:

    "use strict";
    (function(){
        var i = 1;
        alert(i);//1
    })();
    alert(i);//报错

    !function(){
        var i = 1;
        alert(i);//1
    }();
    alert(i);//报错

    +function(){
        var i = 1;
        alert(i);//1
    }();    

    (function(){
        var i = 1;
        alert(i);//1
    })();
    alert(i);//报错

    !function(){
        var i = 1;
        alert(i);//1
    }();
    alert(i);//报错

    +function(){
        var i = 1;
        alert(i);//1
    }();
    alert(i);//报错

就改动这么点东西居然写了三段代码,因为我想凑字数...第一次看见这些写法还完全不知道是什么呢!

3.2. 创建私有变量

闭包也用于在对象中创建私有变量。对于在构造函数中定义的所有变量和方法,并不是公有属性和方法,因此无法被实例对象所访问,可以通过闭包的方式访问这些变量,并对他们进行操作。

    function Bird(){
        var num = 0;
        function fin(){
            alert("private function");
        }

        this.foo = function(){
            num++;
            fin();
        }
    }
    var o = new Bird();
    o.foo()

4. 最后

好吧,关于闭包大概现在就能写这么多,现在掌握的只是语法知识,距离实际使用还隔上十万八千里呢。刚工作十来天,一切都很忙,看书的效率变得好低,加油吧。

《计算机科学导论》读书笔记 浏览器渲染流程