JavaScript学习 六、函数表达式

前文说过定义函数的方式有两种,一种是函数声明、一种是函数表达式。两者最大的区别是函数声明提升,即函数的声明在执行代码前会先被读取。

递归

递归函数是在一个函数中通过名字调用自身的情况。前面我们讲过的一个计算乘阶的函数:

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

console.log(factorial(5));    //120

我们知道,函数名只是一个引用,所以也可以进行赋值,当factorial 被赋值为null或者其他函数引用时,就会发生错误,如下:

var anotherFactorial = factorial;
factorial = null;
anotherFactorial(5);   //TypeError: factorial is not a function

前文讲过,使用arguments.callee 可以解决问题。

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

console.log(factorial(5));    //120
var anotherFactorial = factorial;
factorial = null;
console.log(anotherFactorial(5));   //120

但是在严格模式下,使用arguments.callee 会导致错误。不过可以使用命名函数表达式来达到相同的效果。

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

console.log(factorial(5));    //120
var anotherFactorial = factorial;
factorial = null;
console.log(anotherFactorial(5));   //120

这种方式在严格模式和非严格模式下都能行得通。

闭包

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。

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 obj1 = {
    name: "Lilei",
    age: 18
};
var obj2 = {
    name: "HanMeimei",
    age: 17
};
console.log(compareNames(obj1, obj2));
compareNames = null;

这里需要理解compareNames 函数的作用域链,函数的作用域链保存在内部的[[Scope]] 属性中,当函数被调用的时候就为函数创建一个执行环境,然后通过复制函数的 [[Scope]] 属性中的对象构建其执行环境的作用域链。作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

createComparisonFunction 函数作用域链包括自身的变量 和 全局变量,compareNames 函数的作用域链包括 自身变量 和 createComparisionFunction 的变量 和 全局变量。

当createComparisonFunction 退出时,返回一个compareNames 函数,自身的作用域链销毁, 但是自身的活动对像由于被compareNames 引用,所以仍然会留在内存中,知道compareNames 函数被销毁( compareNames = null; )

1.闭包与变量

作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。

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

var res = createFunctions();
res.forEach(function(value, index, array){
    console.log(value.call());   // 10 10 10 ... 10 
});

函数返回十个函数组成的数组,数组中的十个函数的作用域链都是自身和createFunctions 的对象,读取变量 i 值时,找到作用域链的 createFunctions 对象,在数组中的函数调用的时候,createFunctions 对象中的 i 已经变成了10, 所以返回的函数返回值都是 10。

解决办法是再创建一个匿名函数,在作用域链中隔离开自身和 createFunctions 的对象。

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

var res = createFunctions();
res.forEach(function(value, index, array){
    console.log(value.call());   // 0 1 2 ... 9
});

本例中,我们没有直接把闭包赋值给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋给了属猪。这里的匿名函数有一个参数 num, 也就是最终的函数要返回的值。在调用每个匿名函数时,我们传入了变量 i 。 由于函数参数是按值传递的,所以就会将变量i的当前值复制给 num。而在这个匿名函数内部,有创建并返回了一个访问num 的闭包。这样一来,result 数组中每一个函数都有自己 num 变量的一个副本,因此就可以返回各自不同的数值了。

2.闭包的this对象

在闭包中使用 this 翠系那个也可能会导致一些问题。匿名函数的执行环境具有全局性,因此其this对象通常指向 global。

var global = function(){
    return this;
}();
global.name = "The global";

var object = {
    name: "My Object",
    getNameFunc: function(){
        return function(){
            return this.name;
        };
    }
}
console.log(object.getNameFunc()());  //The global

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

模仿块级作用域

如前所述,JavaScript 中没有块级作用域的概念,这意味着在块语句中定义的变量,实际上是在包含函数中而非语句中创建的。即使后面重新声明变量,也于事无补。

for(var i = 0; i<10; i++){

}
var i;
console.log(i);  //10

匿名函数可以解决这个问题,用作块级作用域(通常被称为私有作用域)的匿名函数的语法如下:

(function(){
    //todo code 块级作用域
})();

以上代码定义并立即调用了一个匿名函数。将函数声明包含在一对全括号中,表示它实际上是一个函数表达式。而金穗气候的另一对圆括号会立即调用这个函数。

(function(){
    for(var i =0; i<10; i++){
    }
})();
console.log(i);  //ReferenceError: i is not defined

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

私有变量

任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。私有变量包括函数的参数、局部变量 和 在函数内部定义的其他函数。

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

有两种在对象上创建特权方法的方式:

第一种:在构造函数中定义特权方法。

function MyObject(){
       //私有变量
    var privateVar = 10;
        //私有方法
    function privateFunc(){
        return false;
    }
        //特权方法
    this.publicMethod = function(){
        privateVar++;
        return privateFunc();
    }
}

定义特却方法有一个缺点,就是你必须使用构造函数模式来达到这个目的。构造函数构建对象的缺点是对每个实例都会创建同样一组方法。

第二种:静态私有变量

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

这个模式创建了一个私有作用域,兵在其中封装了一个构造函数及相应的方法。在私有作用域中首先定义了私有变量和私有函数,然后又定义了构造函数和公有方法。公有方法是在原型上定义的,这一点体现了典型的原型模式。需要注意的是,这个模式在定义构造函数时没有使用函数声明,而是使用了函数表达式。函数声明只能创建局部函数,但那不是我们想要的。出于同样的原因,我们也没有在声明MyObject 时使用var 关键字。记住:初始化未经声明的变量,总是会创建一个全局变量。因此MyObject就i成了一个全局变量,能够在私有作用域之外被访问到。但是在严格模式下,给未经声明的变量赋值会导致错误。

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

模块模式

前面的模式用于为自定义类型创建私有的变量和特权方法。而道格拉斯所说的模块模式则是为单利创建私有变量和特权方法。所谓单例(singleton),指的就是只有一个实例的对象。按照惯例,JavaScript是以对象字面量的方式来创建单例对象的。

var singleton = {
    name: value,
    method: function(){
    
    }
};

模块模式通过为单例添加私有变量和特权方法能够使其得到增强。

var singleton = function(){
    var privateVar = 10;
    function privateFunc(){
        return false;
    }
    return {
        publicProperty:  true,
        publicMethod: function(){
            privateVar++;
            return privateFunc();
        }
    };
}();

这个模块模式使用了一个返回对象的匿名函数。在这个匿名函数的内部,首先定义了私有变量和函数。然后,将一个对象字面量作为函数的值返回。返回的对象字面量中质保含可以公开的属性和方法。

增强的模块模式

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

var singleton = function(){
    var privateVar = 10;
    function privateFunc(){
        return false;
    }
    //创建对象
    var object = new CustomType();
    object.publicProperty = true;

    object.publicMethod = function(){
        privateVariable++;
        return privateFunction();
    };
    return object;
    
}();

小结

在JavaScript 编程中,函数表达式是一种非常有用的技术。使用函数表达式可以无需对函数命名,从而实现动态编程。匿名函数,也称为拉姆达函数,是一种使用JavaScript 函数的强大方式。

  • 函数表达式不同于函数声明。函数声明要求有名字,但函数表达式不需要。没有名字的函数表达式也叫匿名函数。
  • 在无法确定如何引用函数的情况下,递归函数就会变得比较复杂。
  • 地柜函数应该始终使用 arguments.callee 来递归调用自身,不要使用函数名----函数名可能会发生变化。

当在函数内部定义了其他函数是,就创建了闭包。闭包有权访问包含函数内部的所有变量。

  • 在后台执行环境中,闭包的作用域链包含着它自己的作用域、包含函数的作用域 和全局作用域。
  • 通常,函数的作用域及其所有变量都会在函数执行结束后销毁。
  • 但是,当函数返回了一个闭包是,这个函数的作用域将会一直在内存中保存到闭包不存在为止。

使用闭包可以在JavaScript 中模仿块级作用域(JavaScript中本省没有块级作用域的概念)。

  • 创建并立即调用一个函数,这样既可以执行其中的代码,有不会在内存中留下对该函数的引用。
  • 结果就是函数内部的所有变量都会被立即销毁----除非将某些变量赋值给了包含作用域(即外部作用域)中的变量。

闭包还可以用户与在对象中创建私有变量。

  • 即使JavaScript 中没有正式的私有变量属性的概念,但可以使用闭包来实现公有方法,而通过公有方法可以访问包含作用域中定义的变量。
  • 有权访问私有变量的公有方法叫特权方法。
  • 可以使用构造函数模式、原型模式来实现自定义类型的特却方法,也可以使用模块模式、增强的模块模式来实现单例的特权方法。

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