函数表达式

Posted vieber

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了函数表达式相关的知识,希望对你有一定的参考价值。

函数表达式是javascript中一个既强大又容易令人困惑的特性,在javaScript中定义函数的方式有两种,一种是函数声明,一种是函数表达式。函数声明的语法是这样的。

function functionName(arg0, arg1) {
}

首先是function关键字,然后是函数的名字,除过ie的主流浏览器都给函数定义了一个非标准的name属性,通过这个属性可以访问到给函数指定的名字。这个属性的值永远等于跟在function关键字后面的标识符。

alert(functionName.name);//functionName

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

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

这个例子不会抛出错误,因为在代码执行之前会先读取函数声明。
第二种创建函数的方式是使用函数表达式。函数表达式有几种不同的语法形式。下面是最常见的一种形式。

var functionName = function (arg0, arg1, arg2) {

}

这种形式看起来好像是常规的常量赋值语句,即创建一个函数并将他赋值给变量functionName,这种情况下常见的函数叫匿名函数。
函数表达式与其他表达式一样,在使用前必须先赋值。以下代码会导致错误

sayHi();//错误,函数不存在
var sayHi = function() {
    alert("hi");
}

理解函数提升的关键就是理解函数声明与函数表达式之间的区别。例如,执行以下代码的结果可能会让人意想不到

//不要这样做
if (conditon){
    function sayHi(){
        alert("hi");
    }
} else {
    function sayHi(){
        alert("yo");
    }
}

表面上看,以上代码表示在condtion为true时,使用一个sayHi()的定义,否则就使用另外一个定义。实际上,这在ECMAScript中属于无效语法,javaScript引擎会尝试修正错误,将其转化为合理的状态,但问题是浏览器尝试修正错误的做法并不一致,大部分浏览器会返回第二个声明。ff会在conditions为true时返回第一个声明。因此使用这种声明方式很危险。不过使用函数表达式就没什么问题了。

递归

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

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

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

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

以上代码先把factorial()函数保存在变量anotherFactorial中,然后将factorial变量置为null,结果指向原始函数的应用只剩下一个。但在接下来调用anotherFactorial()时,由于必须执行factorial()(因为递归函数体内要执行),而factorial已经不再是函数,所以就会导致错误。在这种情况下,使用arguments.callee可以解决这个问题。
我们知道,arguments.callee是一个执行正在执行的函数的指针,因此可以用他来实现对函数的递归调用,例如:

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

通过使用arguments.callee代替函数名,可以确保无论怎样调用函数都不会出现问题,因此在编写递归函数时,使用arguments.callee总比使用函数名更保险。
但在严格模式下,不能通过脚本访问arguements.callee。访问这个属性会导致错误。不过,可以使用命名函数表达式来达成相同的结果。例如:

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

以上代码创建了一个名为f()的命名函数表达式,然后将他赋值给变量factorial。即使把函数赋值给了另一个变量,函数的.名字f仍然有效,所以递归调用照样能正确完成,这种方式在严格模式和非严格模式下都行得通。

闭包

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

function ceeateComparisonFunction(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定义的那两行代码是内部函数(一个匿名函数)中的代码,这两行代码访问了外部函数中的变量propertyName,即使这个内部函数被返回了,而且是在其他地方被调用了,但他仍然可以访问变量propertyName。之所以还能访问这个变量,是因为内部函数的作用域链中包含了createComparisonFunciton()的作用域,要彻底搞清楚其中的细节,必须从理解函数被调用的时候都会发生什么入手。
当某个函数被调用时,会创建一个执行环境及相应的作用域链,使用arguments和其他命名参数的值来初始化函数的活动对象,但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,。。。直至作为作用域链终点的全局执行环境。
在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量。
作用域链本质上是一个指向变量对象的指针列表,他只引用但不实际包含变量对象。
无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量,一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象),但是闭包的情况又有所不同。
在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到他的作用域链中,因此,在createComparisonFunction()函数内部定义的匿名函数的作用域链中,实际上将会包含外部函数的createComparisonFunction()的活动对象。当ceateComparisonFunction()函数返回后,其执行环境的作用域链会被销毁,但他的活动对象仍然会留在内存中,知道匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁。例如:

//创建函数
var compareNames = createComparisonFunction("name");
//调用函数
var result = compareNames({name: "Nicholas"},{name: "Greg"});
//接触对匿名函数的引用(以便释放内存)
compareNames = null;

闭包与变量

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

function createFunction() {
    var result = new Array();

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

这个函数会返回一个函数数组。表面上看,似乎每个函数都应该返自己的索引值,即位置0的函数返回0,位置1的函数返回1,,,。但实际上,每个函数都返回10。因为每个函数的作用域链中都保存着createFunction()函数的活动对象,所以他们引用的都是同一个变量i。当createFunction()函数返回后,变量i的值是10,此时每个函数都引用着保存变量i的同一个变量对象,所以在每个函数内部i的值都是10,但是,我们可以通过创建另一个匿名函数强制让闭包的行为符合预期。如下所示:

function createFunction(){
    var result = new Array();

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

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

关于this对象

在闭包中使用this对象也可能会导致一些问题。我们知道,this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行具有全局性,因此其this对象通常指向window。但有时候由于编写闭包的方式不同,这一点可能不会那么明显。下面来看一个例子。

var name = "The window";

var object = {
    name: "Object",

    getNameFunc: function(){
        return function(){
            return this.name;
        };
    }
}

alert(object.getNameFunc()());//"The window"(在非严格模式下)

调用Object.getNameFunc()()就会立即调用他返回的函数,结果就是返回一个字符串,然而,这个例子返回的字符串是“The window”,即全局name变量的值,为什么匿名函数没有取得其包含作用域(或外部作用域)的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.getNameFunct()());//"My Object"

this和arguments也存在同样的问题。如果想访问作用域中的arguments对象,必须将对该对象的引用保存到另一个闭包能够访问的变量中。
有几个特殊情况下,this的值可能会意外改变。比如,下面的代码是修改前面例子的结果。

var name = "The window";

var object = {
    name: "My Object",

    getName: function(){
        return this.name;
    }
}

这里的getName()方法只简单地返回this.name的值,以下是几种调用object.getName()的方式以及各自的结果。

object.getName();//My object

(object.getName)();//My object

(object.getName = object,getName)();//The window,在非严格模式下

第一行和第二行this的值得到了维持,因为object.getName()和(object.getName)的定义是相同的。第三行代码先执行了一条赋值语句,然后再调用赋值后的结构。因此这个赋值表达式的值是函数本身(等于就是剥离出来),所以this的值不能得到维持,结果就返回了“The Window”。

内存泄漏

由于IE9之前的版本Jscipt对象和com对象使用不同的垃圾收集例程,引用计数,因此闭包在ie的这些版本中会导致一些特殊的问题。具体来说,如果闭包的作用域链中保存着一个html元素,那么就意味着该元素无法被销毁。来看下面这个例子:

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

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


    function assignHandler () {
        var elements = document.getElementById("someElement");
        var id = element.id;

        element.onclick = function  () {
            alert(id);
        };

        element = null;
    }

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

模拟块级作用域

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

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

这个函数中定义了一个for循环,而变量i的初始值被设置为0.在java和c++等语言中,变量i只会在for循环的语句块中有定义,循环一旦结束,变量i就会被销毁,可是在javaScript中,变量i是定义在outputNumbers()的活动对象中,因此从它有定义开始,就可以在函数内部随处访问它,即使像下面这样错误地重新声明同一个变量,也不会改变它的值。

function outputNumber (count) {
    for (var i = 0; i < count; i++) {
            alert(i);
        }
        var i ;//重新声明变量
        alert(i);//计数
    }

javaScript从来不会告诉你是否多次声明了同一变量,遇到这种情况,它只会对后续的声明视而不见(不过,他会执行后续声明中的变量初始化)。匿名函数可以用来模仿块级作用域并避免这个问题。用块级作用域(通常称为私有作用域)的匿名函数的语法如下

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

以上代码定义并立即调用了一个匿名函数。将函数声明包含在一对圆括号中,表示他实际上是一个函数表达式。而紧随其后的另一对圆括号会立即调用这个函数。如果感觉不好理解,来看下面这个例子

var count = 5;
outputNumbers(count);

为了让代码更简洁,我们在调用函数时用5来代替变量count,如下所示:

outputNumbers(5);

这样做之所以可行,是因为变量只不过是值的另一种表现形式,因此用实际的值替换变量没有问题。再看下面的例子:

var someFunction = function() {
}
someFunction();

这个例子先定义了一个函数,然后立即调用了它。定义函数的方式是创建了一个匿名函数,并把匿名函数赋值给变量someFunction,而调用函数的方式是在函数名称后面添加一对圆括号,即someFunction(),通过前面的例子,我们知道可以用实际的值来取代变量count,那在这里是不是也可以用函数的值直接取代函数名呢?然而,下面的代码却会导致错误。

function () {
}()//出错

这段代码会导致语法错误,是因为javaScript将function关键字当作一个函数声明的开始,而函数声明后面不能跟圆括号。然而,函数表达式的后面可以跟圆括号。要将函数声明转化成函数表达式,只要像下面这样给它加上一对圆括号即可。

(function  () {

})();

无论在什么地方,只需要临时需要一些变量,就可以使用私有作用域,例如:

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

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

在这个重写后的outputNumbers()函数中,我们在for循环外部插入了一个私有作用域,在匿名函数中定义的任何变量,都会在执行结束时被销毁。因此,变量i只能在循环中使用,使用后即被销毁。而在私有作用域中能够访问变量count,是因为这个匿名函数是一个闭包,他能够访问包含作用域中的所有变量。
这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。一般来说,我们都应该尽量少向全局作用域中添加变量和函数。在一个由很多开发人员参与的大型应用程序中,过多的全局变量和函数很容易导致命名冲突。而通过创建私有作用域,每个开发人员即可使用自己的变量,又不必担心搞乱全局作用域。例如:

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

把上面这段代码放在全局作用域中,可以用来确定哪一天是1月1日,如果到了这一天,就会向用户显示一条祝贺新年的消息。其中的变量now现在是匿名函数中的局部变量,而我们不必在全局作用域中创建它。
这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用,只要函数执行完毕,就可以立即销毁其作用域链了。

私有变量

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


    function add (num1, num2) {
        var sum = num1 + num2;
        return sum;
    }

在这个函数内部,有3个私有变量:num1、num2和sum。在函数内部可以访问这几个变量,但在函数外部不能访问他们。如果在这个函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些变量。而利用这一点,就可以创建用于访问私有变量的公有方法。
我们把有权访问私有变量和私有函数的共有方法成为特权方法,有两种在对象上创建特权的方式,第一种是在构造函数中定义特权方法,基本模式如下:

    function MyObject () {

        //私有变量和私有函数
        var privateVariable = 10;

        function privateFunction () {
            return false;
        }

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

这个模式在构造函数内部定义了所有私有变量和函数。然后,又继续创建了能够访问这些私有成员的特权方法。能够在构造函数中定义特权方法。是因为特权方法作为闭包有权访问在构造函数中定义的所有变量和函数。对这个例子而言,变量privateVariabele和函数privateFunction()只能通过特权方法publicMethod()来访问,在创建MyObject的实例后,除了使用publicMethod()这一个途径外,没有任何办法可以直接访问privateVariable和privateFunction()
利用私有和特权成员,可以隐藏那些不应该被直接修改的数据,例如:


    function Person (name) {

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

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

    var person = new Person("Nicholas");
    alert(person.getName());//"nicholas"
    person.setName("Greg");
    alert(person.getName());//"Greg"

以上代码的构造函数中定义了两个特权方法,私有变量name在Person的每一个实例中都不相同,因为每次调用构造函数都会重新创建这两个方法。不过,在构造函数中定义特权方法也有一个缺点,就是你必须使用构造函数模式来达到这个目的。构造函数的缺点是针对每个实例都会创建一组新方法,而使用静态私有变量来实现特权方法就可以避免这个问题。

静态私有变量

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

    (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"

这个例子中的Person构造函数与setName()和getName()方法一样,都有权访问私有变量name,在这种模式下,变量name,就变成了一个静态的,由所有实例共享的属性
多查找作用域链中的一个层次,就会在一定程度上影响查找的速度,而正式使用闭包和私有变量的一个明显的不足之处。

以上是关于函数表达式的主要内容,如果未能解决你的问题,请参考以下文章

VSCode自定义代码片段——声明函数

js函数表达式和函数声明的区别

VSCode自定义代码片段8——声明函数

简化条件表达式

如何理解这段代码片段中的两对括号?

我的重构第一步