第七章:JS函数表达式

本章内容:

函数表达式是 JavaScript 中的一个既强大又容易令人困惑的特性。定义函数的 方式有两种:一种是函数声明,另一种就是函数表达式。

关于函数声明,它的一个重要特征就是函数声明提升(function declaration hoisting),意思是在执行 代码之前会先读取函数声明。这就意味着可以把函数声明放在调用它的语句后面。

    sayHi();
    function sayHi(){
        alert("Hi!");
    }

第二种创建函数的方式是使用函数表达式。函数表达式有几种不同的语法形式。下面是最常见的一种形式

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

这种形式看起来好像是常规的变量赋值语句,即创建一个函数并将它赋值给变量 functionName。 这种情况下创建的函数叫做匿名函数(anonymous function),因为 function 关键字后面没有标识符。

匿名函数有时候也叫拉姆达函数。

理解函数提升的关键,就是理解函数声明与函数表达式之间的区别。

7.1 递归

递归函数是在一个函数通过名字调用自身的情况下构成的,

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

这是一个经典的递归阶乘函数。虽然这个函数表面看来没什么问题,但下面的代码却可能导致它出错。

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

在接下来调用 anotherFactorial()时,由于必须执行 factorial(),而 factorial 已经不再是函数,所以就会导致错误

在这种情况下,使用argu- ments.callee可以解决这个问题。我们知道,arguments.callee是一个指向正在执行的函数的指针,因此可以用它来实现对函数的递归调用,

五种基本类型:Undefied,Null,Number,Boolean,String

引用类型:Object / Array / Function / Date / RegExp / Error / Map / Set

闭包

有不少开发人员总是搞不清匿名函数和闭包这两个概念,因此经常混用。闭包是指有权访问另一个 函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。

一般来讲, 当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况又有所不同。

在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。

解除对匿名函数的引用(以便释放内存)

    compareNames = null;

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,我们建议读者只在绝对必要时再考虑使用闭包。虽然像V8等优化后的JavaScript引擎会尝试回收被闭包占用的内存,但请大家还是要慎重使用闭包

看来关于内存泄露的问题,以后要谈谈

7.2.1 闭包与变量

作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最 后一个值。别忘了闭包所保存的是整个变量对象,而不是某个特殊的变量。下面这个例子可以清晰地说 明这个问题。

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

因为每个函数的作用域链中都保存着createFunctions()函数的活动对象,所以它们引用的都是同一个变量i。当 createFunctions()函数返回后,变量i的值是10,此时每个函数都引用着保存变量i的同一个变量 对象,所以在每个函数内部 i的值都是10。但是,我们可以通过创建另一个匿名函数强制让闭包的行为符合预期,如下所示。

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

定义了一个匿名函数,并将立即执行该匿名函数的结果赋给数组

这样一来,result数组中的每个函数都有自己num变量的一个副本,因此就可以返回各自不同的数值了

7.2.2 关于this对象

this 对象是在运行时基于函数的执 行环境绑定的:在全局函数中,this 等于 window,而当函数被作为某个对象的方法调用时,this 等 于那个对象。不过,匿名函数的执行环境具有全局性,因此其 this 对象通常指向 window1。但有时候 由于编写闭包的方式不同,这一点可能不会那么明显。如下:

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

为什么匿名函数没 有取得其包含作用域(或外部作用域)的 this 对象呢?

前面曾经提到过,每个函数在被调用时都会自动取得两个特殊变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。

不过,把外部作用域中的 this 对象保存在一个闭包能够访问 到的变量里,就可以让闭包访问该对象了,如下所示。

    var name = "The Window";
        var object = {
            name : "My Object",
            getNameFunc : function(){
        var that = this;
            return function(){
                return that.name;
        }; }
    };
    alert(object.getNameFunc()());  //"My Object"

代码中突出的行展示了这个例子与前一个例子之间的不同之处。在定义匿名函数之前,我们把this对象赋值给了一个名叫that 的变量。而在定义了闭包之后,闭包也可以访问这个变量,因为它是我们在包含函数中特意声名的一个变量。即使在函数返回之后,that也仍然引用着object,所以调用object.getNameFunc()()就返回了”My Object”。

this和arguments也存在同样的问题。如果想访问作用域中的arguments对象,必须将对该对象的引用保存到另一个闭包能够访问的变量中。

7.2.3 内存泄漏

由于 IE9之前的版本对JScript对象和COM对象使用不同的垃圾收集例程(第4章曾经讨论过),因此闭包在IE的这些版本中会导致一些特殊的问题。具体来说,如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素将无法被销毁。

    function assignHandler(){
            var element = document.getElementById("someElement");
            element.onclick = function(){
                alert(element.id);
            };
    }

以上代码创建了一个作为 element 元素事件处理程序的闭包,而这个闭包则又创建了一个循环引 用(事件将在第 13 章讨论)。由于匿名函数保存了一个对 assignHandler()的活动对象的引用,因此 就会导致无法减少 element 的引用数。只要匿名函数存在,element 的引用数至少也是 1,因此它所 占用的内存就永远不会被回收。不过,这个问题可以通过稍微改写一下代码来解决,如下所示。

    function assignHandler(){
    var element = document.getElementById("someElement"); var id = element.id;
            element.onclick = function(){
    alert(id);
    };
            element = null;
    }

在上面的代码中,通过把element.id的一个副本保存在一个变量中,并且在闭包中引用该变量消除了循环引用。但仅仅做到这一步,还是不能解决内存泄漏的问题。必须要记住:闭包会引用包含函数的整个活动对象,而其中包含着element。即使闭包不直接引用element,包含函数的活动对象中也仍然会保存一个引用。因此,有必要把element变量设置为null。这样就能够解除对DOM对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。

7.3 模仿块级作用域

如前所述,JavaScript没有块级作用域的概念。这意味着在块语句中定义的变量,实际上是在包含 函数中而非语句中创建的,来看下面的例子。

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

个函数中定义了一个 for 循环,而变量 i 的初始值被设置为 0。在 Java、C++等语言中,变量 i 只会在 for 循环的语句块中有定义,循环一旦结束,变量 i 就会被销毁。可是在 JavaScrip 中,变量 i 是定义在 ouputNumbers()的活动对象中的,因此从它有定义开始,就可以在函数内部随处访问它。

匿名函数可以用来模仿块级作用域并避免这个问题。用作块级作用域(通常称为私有作用域)的匿名函数的语法如下所示。

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

将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式。一对圆括号将函数声明转换成函数表达式。

无论在什么时候,只要临时需要一些变量,就可以使用私有作用域。

    function outputNumbers(count){
        (function () {
            for (var i=0; i < count; i++){
                alert(i);
            }
        })();
        alert(i); //􏰠􏰡􏰢􏰣􏰤􏰥􏰦 导致一个错误!
    }

这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。

这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。

7.4 私有变量

严格来讲,Javascript中没有私有成员的概念;所有对象属性都是公有的。不过,倒是有一个私有变量的概念。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。私有变量包括函数的参数,局部变量和在函数内部定义的其他函数。

如果在这个函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些变量。而利用这一点,就可以创建用于访问私有变量的公有方法。

我们把有权访问私有变量和私有函数的公有方法称为特权方法(privileged method)。

有两种在对象 上创建特权方法的方式。第一种是在构造函数中定义特权方法,基本模式如下。

    function MyObject(){
       //私有变量和私有函数 
        var privateVariable = 10;
        function privateFunction(){
            return false;
    }
    //特权方法
    this.publicMethod = function (){
            privateVariable++;
            return privateFunction();
        };
    }

不过,在构造函数中定义特权方法也有一个缺点,那就是你必须使用构造函数模式来达到这个目的。

曾经讨论过,构造函数模式的缺点是针对每个实例都会创建同样一组新方法,而使用静态私有变量来实现特权方法就可以避免这个问题。

7.4.1 静态私有变量

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

    (function(){
    //私有变量和私有函数
    var privateVariable = 10;
            function privateFunction(){
                return false;
    }
    //构造函数
    MyObject = function(){ };
    //公有/特权方法
    MyObject.prototype.publicMethod = function(){
                privateVariable++;
                return privateFunction();
            };
    })();

需要注意的是,这个模式在定义构造函数时并没有使用函数声明,而是 使用了函数表达式。函数声明只能创建局部函数,但那并不是我们想要的。出于同样的原因,我们也没 有在声明 MyObject 时使用 var 关键字。记住:初始化未经声明的变量,总是会创建一个全局变量。 因此,MyObject 就成了一个全局变量,能够在私有作用域之外被访问到。但也要知道,在严格模式下给未经声明的变量赋值会导致错误。

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

    (function(){
            var name = "";
            Person = function(value){
                name = value;
    };
            Person.prototype.getName = function(){
                return name;
            };
            Person.prototype.setName = function (value){
            name = value;
        };
    })();
    var person1 = new Person("Nicholas"); alert(person1.getName()); //"Nicholas" person1.setName("Greg"); alert(person1.getName()); //"Greg"
    var person2 = new Person("Michael");
    alert(person1.getName()); //"Michael"
    alert(person2.getName()); //"Michael"

以这种方式创建静态私有变量会因为使用原型而增进代码复用,但每个实例都没有自己的私有变量。到底是使用实例变量,还是静态私有变量,最终还是要视你的具体需求而定。

查找作用域链中的一个层次,就会在一定程度上影响查找速度。而这正是使用 闭包和私有变量的一个显明的不足之处。

7.4.2 模块模式

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

    var singleton = function(){
    //私有变量和私有函数
    var privateVariable = 10;
            function privateFunction(){
                return false;
        }//特权/公有方法和属性
            return {
                publicProperty: true,
                publicMethod : function(){
                    privateVariable++;
                    return privateFunction();
                }
    }; }();

7.4.3 增强的模块模式

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

7.5 小结