《JavaScript高级程序设计》学习笔记,第七章

函数表达式是JavaScript当中一个既强大又令人困惑的特性,特别是其中涉及到的闭包,更是令许多的初学者困惑不已。

在之前的章节中有介绍过,定义函数的方法有两种:一种是函数声明。

    function functionName(arg0, arg1, arg2) {
        //函数体
    }

对于函数声明,有一个重要的特征就是可以把函数声明放在调用它的语句后面,因为解释器会在执行语句之前先读取函数的声明。

另一种方法是使用函数表达式。

    var functionName = function(arg0, arg1, arg2){
        //函数体
    };

函数表达式与函数声明的一个主要不同就是,它必须在定义之后才能使用,否则将会报错。函数表达式中的函数是一个匿名函数,即function关键字后面没有标识符。既然可以把函数赋值给变量,也就可以把函数作为其它函数的返回值。

递归

JavaScript中的递归有个问题,将保存函数的变量赋值给另一个变量时,因为函数名称改变了,所以在递归调用的时候会出现问题:

    function factorial(num){
        if (num <= 1){
            return 1;
        } else {
            return num * factorial(num-1);
        }
    }

    var anotherFactorial = factorial;
    factorial = null;
    alert(anotherFactorial(4)); //出错!

要解决这个问题,可以使用arguments.callee。这个属性是一个指向正在执行的函数的指针,所以可以利用它来代替函数名,这就确保递归调用时函数名称改变也不会出错。但是,在严格模式下,使用argments.callee会导致错误。我们可以使用命名函数来达成相同的结果:

    var factorial = (function f(num){
        if (num <= 1){
            return 1;
        } else {
            return num * f(num-1);
        }
    });

闭包

闭包是一个容易令人困惑的概念。闭包是指有权访问另一个函数作用域中的变量的函数。创建装饰的常见方式就是嵌套定义函数。

我们在第4章的时候了解过作用域链。而对于作用域链清晰地理解,是理解闭包的重要关键。

当调用函数的时候,会为函数创建一个执行环境,并将该环境的活动对象加入到作用域链的前端。作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

当在一个函数内部定义另一个函数的时候,外部函数的活动对象会被添加到内部函数的作用域链中。一般情况下,当函数执行完毕后,它的活动对象就会被销毁。但是,当有内部函数与其构成闭包时,因为内部函数的作用域链仍在引用外部函数的活动对象,因此只有当内部的函数也被销毁后,这个外部活动对象才会被销毁。

闭包与变量

闭包只能取得包含函数(即外部函数)中任何变量的最后一个值。

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

这个函数返回的函数数组中的每个函数都会返回10。因为每个函数的作用域链中都保存着createFunctions()的活动对象,所以它们引用的都是同一个变量i。当createFunctions()返回后,i的值是10,所以每个函数引用保存的变量i都是10。可以使用另一个匿名函数来修正这个问题:

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

关于this对象

this对象是在运行时,基于函数的执行环境绑定的。但是,匿名函数的执行环境具有全局性,因此其this对象通常指向window

    var name = "The Window";
    var object = {
        name : "My Object",
        getNameFunc : function(){
            return function(){
                return this.name;
            };
        }
    };
    alert(object.getNameFunc()()); //"The Window"(在非严格模式下)

这是因为每个函数都有自己的this,所以当内部函数在搜索这个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问到外部函数中的this

模仿块级作用域

之前讲过,在JavaScript当中没有块级作用域。但是,我们可以使用匿名函数来模仿块级作用域。

    (function(){
        // 这里是块级作用域
    })();

在函数的声明包含在一对圆括号中,表示它实际上是一个函数表达式,而紧随其后的另一对圆括号会立即调用这个函数。

在需要用到块级作用域的时候,就可以这样使用:

    function outputNumbers(count){
        (function () {
            for (var i=0; i < count; i++){
                alert(i);
            }
        })();

        alert(i); //导致一个错误!
    }

私有变量

从技术角度来讲,JavsScript中是没有私有成员的概念的,所有对象属性都是公开的。因但是对于函数而言,函数里面的变量对外部都是私有的,我们可以利用在函数里面创建一个闭包,来创建用于访问函数内部私有变量的公有方法。

对于有权访问私有变量和私有函数的公有方法,我们称之为特权方法(privileged method)。创建特权方法的方式有两种:一是在构造函数当中定义特权方法。

    function MyObject(){
        //私有变量和私有函数
        var privateVariable = 10;
        function privateFunction(){
            return false;
        }

        //特权方法
        this.publicMethod = function (){
            privateVariable++;
            return privateFunction();
        };
    }

对于这个例子而言,变量privateVariable和函数privateFunction()只能通过特权方法publicMethod()来访问,我们无法直接访问到内部私有的变量。

使用模式的缺点是针对每个实例都会创建同样一组新方法,可以使用另一种方法,静态私有变量来避免这个问题。

静态私有变量

通过在私有作用域中定义私有变量和函数,也可以创建特权方法:

    (function(){
        //私有变量和私有函数
        var privateVariable = 10;

        function privateFunction(){
            return false;
        }

        //构造函数
        MyObject = function(){
        };
        //公有/特权方法
        MyObject.prototype.publicMethod = function(){
            privateVariable++;
            return privateFunction();
        };
    })();

这个模式使用了函数表达式来定义特权方法,因为函数声明只能创建局部的函数,同样地,对于MyObject我们也没有使用var关键字,这样可以使其成为一个全局变量。

这个模式与构造函数模式的主要区别在于,私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法作为一个装饰,问题保存着对包含作用域的引用。

模块模式

模块模式是为单例创建私有变量和特权的方法。所谓单例,即只有一个实例的对象。

一般情况下,JavaScript是以对象字面量的方式来创建单例对象的。

模块模式通过为单例添加私有变量和特权方法能够使其得到增强,其语法形式如下:

    var singleton = function(){
        //私有变量和私有函数
        var privateVariable = 10;

        function privateFunction(){
            return false;
        }

        //特权/公有方法和属性
        return {
            publicProperty: true,
            publicMethod : function(){
                privateVariable++;
                return privateFunction();
            }
        };
    }();

如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。

增强的模块模式

这个模式即在返回对象之前加入对其增强的代码。这种增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性或方法对其加以增加的情况。

    var singleton = function(){
    
        //私有变量和私有函数
        var privateVariable = 10;
            function privateFunction(){
            return false;
        }

        //创建对象
        var object = new CustomType();

        //添加特权/公有属性和方法
        object.publicProperty = true;
        object.publicMethod = function(){
            privateVariable++;
            return privateFunction();
        };

        //返回这个对象
        return object;
    }();

小结

这章主要讨论了JavaScript当中的函数表达式与闭包。理解闭包的一个重要基础就是要透彻理解执行环境和作用域链。

函数表达式和闭包都是极其有用的特性,利用它们可以实现很多功能。不过,因为创建闭包必须维护额外的作用域,所以过度使用它们可能会占用大量内存。