首页 > 代码库 > JavaScript函数表达式

JavaScript函数表达式

定义函数的方式

定义函数的方式有两种:一种是函数声明,另一种是函数表达式。
函数声明语法如下:

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

主流浏览器还给函数定义了一个name属性,值是functionName.

function f() {};
var k = f;
alert(k.name);//f

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

sayHi();//代码不会出现错误,因为在预编译时
function sayHi() {
    alert("Hi");
}

第二种方式就是函数字面量的形式,例如下面代码是最常见的一种形式

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

这种形式类似于变量赋值,创建一个匿名函数(拉姆达函数)赋值给变量。匿名函数的name属性时空字符串。函数表达式和其他表达式一样,使用前必须先赋值。
把函数当成值来使用的情况下,都可以使用匿名函数。

递归

递归函数是在一个函数通过调用自身的情况下构成的,如下代码所示:

//递归函数
function factorial(num) {
    //递归尾部判断
    if (num <= 1) {
        return 1;
    } else {
        //调用自身
        return num * factorial(num - 1);    
    }
}
alert(factorial(5));//120
//将函数赋值给另一个变量名,并将原来名字指向null时,这时会出现问题
var anotherFactorial = factorial;
factorial = null;
anotherFactorial(5);//error:factorial is not a function

上面代码错误的原因是调用函数时是将函数名写死了,这种情况下可以使用arguments.callee。它指向正在执行的函数的指针,因此可以用它来改写递归函数。

//递归函数
function factorial(num) {
    //递归尾部判断
    if (num <= 1) {
        return 1;
    } else {
        //arguments.callee指向正在执行的函数自身
        return num * arguments.callee(num - 1); 
    }
}

但是在严格模式下,不能通过脚本访问arguments.callee,访问这个属性会导致错误,不过可以使用命名函数表达式来达成相同的结果。

//命名函数表达式
var factorial = (function f(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * f(num - 1);    
    }
});

alert(factorial(5));//120
var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(5));//120

闭包

闭包是指有权访问另一个函数作用于中的变量的函数。当某个函数被调用时,会创建一个执行环境以及相应的作用域链,并把作用域链赋值给一个特殊的内部属性[[Scope]],然后使用this、arguments和其他命名参数的值来初始化函数的活动对象(active Object)。

function compare( value1, value2 ) {
    return value1 - value2;
}
var result = compare( 5, 10 );

上面代码在全局定义了一个compare()函数,然后在全局调用了它。第一调用compare()函数时,会创建一个包含this、arguments、value1、value2的活动对象。全局执行环境的变量对象(包含this、result和compare)在compare()执行环境的作用域链中处于第二位。
技术分享
后台的每个执行环境都有一个表示变量的对象——变量对象。全局环境的变量对象始终存在,而像compare()函数这样的局部环境的变量对象,只会在函数执行的过程中存在。在创建compare()函数时,会预先包含全局对象的作用域链,这个作用域链被保存在内部的[[Scope]]属性中。当调用compare()函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起执行环境的作用域链。之后会将一个新的活动对象推到作用域链前端。对于compare()的执行环境而言,其作用域链包含两个变量对象:本地活动对象和全局变量对象。
显然作用域链是一个指向变量对象的指针列表,它只引用但不包含变量对象。
无论什么时候在函数中访问一个变量时,就会在作用域链中搜索具有相应名字的变量。当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域。但是闭包有所不同,在一个函数内部定义的函数会将外部函数的活动对象添加到自己的作用域链上。因此内部函数可以访问外部函数定义的所有变量。

function createComparisonFunction(propertyName) {
    //返回一个内部匿名函数
    return function(object1, object2) {
        //内部函数可以访问外部函数的propertyName变量
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];
        if (value1 > value2) {
            return 1;
        } else if (value1 < value2) {
            return -1;
        } else {
            return 0;
        }
    }
}

var compare = createComparisonFunction("name");
var result = compare({name:"Nic"},{name:"Greg"});

变量compare指向了返回的匿名函数,它的作用域链被初始化为自身活动对象、createComparisonFunction()活动对象和全局变量对。所以就算createComparisonFunction()执行完成,其活动对象也不会销毁,因为匿名函数的作用域链中引用着它的活动对象。但是createComparisonFunction()执行完成后它自身的执行环境的作用域链会销毁,但它的活动对象仍然会留在内存,直到匿名函数被销毁。例如:

var compare = createComparisonFunction("name");
var result = compare({name:"Nic"},{name:"Greg"});
compare = null;//解除对匿名函数的引用

技术分享

闭包与变量

作用域链的这种配置机制引出了一个副作用:闭包只能取得外部函数中的任何变量的最后的值。例如:

function outer() {
    var result = new Array();
    for (var i = 0; i < 10; i++) {
        result[i] = function() {
            alert(i);
        }
    }
    return result;
}
var result = outer();
result[5]();//10, 而不是5

表面上看似乎每个函数都应该返回自己的索引值,但实际上每个函数都会返回10,因为每个函数作用域链中都保存着outer()的活动对象,所以它们引用的都是同一个变量i,当outer()函数返回后,变量i的值是10。我们可以通过创建另外一个匿名函数强制让闭包的行为符合预期:

function outer() {
    var result = new Array();
    for (var i = 0; i < 10; i++) {
        result[i] = function(num){
            return function() {
                alert(num);
                debugger;
            };
        }(i);
    }
    return result;
}
var result = outer();
result[5]();//5

这个版本我们并没有把闭包赋值给数组,而是定义了一个立即执行的匿名函数,匿名函数返回了一个访问num的闭包,这样一来result数组中的每个函数都有自己num变量的一个副本,就可以返回不同的值了。

关于this对象

闭包中使用this对象也可能导致一些问题,this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window;而当函数作为某个对象的方法调用时,this指向调用的对象。
匿名函数的执行环境具有全局性,因此this对象通常执行window,但有时由于编写闭包的方式不同会有差别。

var name = "The Window";
var obj = {
    name: "My Obj",
    getNameFun: function() {
        return function() {
            debugger;
            return this.name;
        }
    }
}
alert(obj.getNameFun()());//The Window(在非严格模式下)

为什么会去到全局的name对象呢?之前提到过,每个函数在执行时,其活动对象都会自动获得两个特殊变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止因,因此永远不可能直接访问外部函数中的这两个变量。不过,可以把外部作用域的this对象保存在一个闭包能够访问的变量里,就可以让闭包访问该对象了。

var name = "The Window";
var obj = {
    name: "My Obj",
    getNameFun: function() {
        var that = this;
        return function() {
            return that.name;
        }
    }
}
alert(obj.getNameFun()());//My Obj

在一下几种特殊情况下,this的值可能会意外的改变。比如以下代码是修改前面例子的结果。

var name = "The Window";
var obj = {
    name: "My Obj",
    getNameFun: function() {
        alert(this.name);
    }
};
obj.getNameFun();// "My Obj"
(obj.getNameFun)();// "My Obj"
(obj.getNameFun=obj.getNameFun)();// "The Window"

obj.getNameFun和(obj.getNameFun)相等(obj.getNameFun == (obj.getNameFun)为true),第三条代码先执行了一条赋值语句,然后再调用赋值后的结果。因为这个赋值表达式的值是函数本身,所以this值不能得到保持,结果就返回 “The Window”

内存泄漏

闭包会导致IE9之前版本出现一些问题,具体来说就是如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素无法被销毁。例如:

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

以上代码创建了一个作为element元素事件处理程序的闭包,而这个闭包又创建了一个循环引用,由于匿名函数保存了一个对assignHandler()的活动对象的引用,因此就会导致无法减少element的引用数。只要匿名函数存在,element的引用数至少是1,因此它所占据的内存就永远都不会被回收。可以通过以下代码进行修改。

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

模仿块级作用域

如前所示,JavaScript没有块级作用域的概念。JavaScript的块级定义的变量实际上是定义在函数中而不是语句中。

function outputNumbers(count) {
    for(var i=0;i<10;i++){
        //console.log(i);
    }
    console.log(i);
}

在类似于Java和C语言中,无法在for循环外访问i的,因为i是在for循环的块级作用域中。而JavaScript中由于没有块级作用域,所以i是能够访问的。
匿名函数时可以用作模拟块级作用域,用作块级作用域(通常也叫做私有作用域)的匿名函数的语法如下所示:

//匿名自执行函数
(function(){
    //这里是块级作用域
})()

(function(){})实际上是一个函数表达式,我们可以根据以下代码进行理解:

var fun = function(){
    //some code
};
fun();//调用fun函数
/*我们是将一个函数表达式传递给了一个变量,所以fun()等价于下面代码
function(){
    //some code
}()
但是这样不符合JavaScript代码语法规则,因为JavaScript会把function开头的作为函数声明语句,把函数
声明语句转化为函数表达式,需要在外面包一个小括号
*/
(function(){
    //some code
})()

//无论什么时候需要一些临时变量,就可以用私有作用域
function outputNumbers(count) {
    (function(){
        //匿名自执行函数执行完,i就被销毁了,外部访问不到,保证变量私有化
        for(var i=0;i<10;i++){
            //console.log(i);
        }
    }();
    console.log(i);//导致错误
}

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

(function(){
    var now = new Date();
    if (now.getMonth() == 0 && now.getDate() == 1) {
        //11日
        alert("Happy new year");
    }
})()

私有变量

严格来讲,JavaScript没有私有变量的概念,所有对象属性都是公有的。不过,JavaScript有私有变量的概念,任何函数中定义的变量都可以认为是私有变量,因为不能再函数外部访问这些变量,私有变量包括:参数、局部变量和函数内部定义的其他函数。
我们把有权访问私有变量的方法称为特权方法,有两种在对象上创建特权方法的方式。第一种是在函数中创建特权方法。

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

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

在创建MyObject实例后,除了使用publicMethod这一个途径外,没有任何办法可以直接访问privateVariable和privateFunction。
利用私有成员和特权成员,可以隐藏那些不应该被直接修改的数据,例如:

function Person(name) {
    this.getName = function(){
        return name;
    };
    this.setName = function(value) {
        name = value;
    }
}

var p = new Person("li");
alert(p.getName());//li
p.setName("kk");
alert(p.getName());//kk

由于getName和setName方法是在构造函数内部定义的,他们作为闭包能够通过作用域链访问name。在构造函数中定义特权方法的缺点就是构造函数模式是针对每个实例创建同样一组新方法的。静态私有变量可以解决这个问题。

静态私有变量

先看代码

(function(){
    var name;

    //一个全局的Person函数
    Person = function(value){
        name = value;
    };

    Person.prototype.getName = function(){
        return name;
    };

    Person.prototype.setName = function(value){
        name = value;
    };

})();

var person1 = new Person("nick");
alert(person1.getName());
person1.setName("adi");
alert(person1.getName());

var person2 = new Person("Mick");
alert(person1.getName());
alert(person2.getName());

在这种模式下,name变成了一个静态的,所有对象共享的属性。也就是说在一个实例上调用setName()方法会影响所有实例。结果就是所有实例多返回相同的name值。

模块模式

前面的模式是用于为自定义类型创建私有变量和特权方法的。模块模式则是为单例创建私有变量和特权方法。单例,就是只有一个实例的对象。按照惯例,JavaScript是以对象字面量的方式来创建单例对象的。

var singleton = {
    name: value,
    method: function(){
        //这里是方法的代码
    }
}

这种模式在需要对单例进行初始化,同时又要维护其私有变量时是非常有用的。例如:

var application = function() {
    //私有变量和函数
    var components = new Array();
    //初始化
    components.push(new Object());

    //公共
    return {
        //方法中定义了方法,访问了方法外的变量,保存了外部方法的作用域链
        getComponentCount: function(){
            return components.length;
        },
        registComponent: function(component) {
            if (typeof component == "object") {
                components.push(component);
            }
        }
    }
}();
alert(application.getComponentCount());

以这种模式创建的每个单例都是Object的实例,因为最终是返回了一个对象字面量。事实上这也没什么,毕竟单例通常是作为全局对象存在,我们不会将他传递给一个参数。因此也没有必要使用instanceof操作符检测其对象类型。

增强的模块模式

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

var application = function() {
    //私有变量和函数
    var privateVariable = 10();
    function privateFunction() {
        return false;
    };
    //创建对象
    var object = new CustomType();

    object.publicPrototype = true;
    object.publicMethod = function(){
        privateVariable++;
        return privateFunction();
    };
    return object;
}();
alert(application.getComponentCount());
<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    JavaScript函数表达式