引用类型--Function类型(函数声明与函数表达式、arguments.callee、caller、apply、call、bind)

时间:2022-07-21 15:46:14

在ECMAScript中函数实际上是对象。每个函数都是Function类型的实例,而且都与其他引用类型一样具有属性和方法。由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。函数通常是使用函数声明(定义函数的方法之一)语法定义的,如:

function sum(num1 , num2){
returnnum1 + num2;
}

这与下面使用函数表达式(定义函数的方法之二)定义函数的方式几乎相差无几:

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

以上代码定义了变量sum并将其初始化为一个函数,上面的例子中function关键字后面没有函数名。这是因为在使用函数表达式定义函数时,没有必要使用函数名——通常变量sum即可以引用函数。另外,还要注意函数末尾有一个分号,就像声明其他变量一样。

最后一种定义函数的方式是使用Function构造函数(定义函数的方法之三)。Function构造函数可以接收任意数量的参数,但最后一个参数始终都被看成是函数体,而前面的参数则枚举出了新函数的参数,如:

var sum = new Function(“num1” , “num2” , “returnnum1 + num2”);//不推荐

/*从技术角度讲,这是一个函数表达式。但是,不推荐使用这种方法定义函数,因为这种语法会导致解析两次代码
(第一次是解析常规ECMAScript代码,第二次是解析传入构造函数中的字符串),从而影响性能。不过,
这种语法对于理解“函数是对象,函数名是指针”的概念倒是非常直观的。*/

由于函数名仅仅是执行函数的指针,因此函数名与包含对象指针的其他变量没有什么不同。换句话说,一个函数可能会有多个名字。

function sum(num1, num2){
return num1 + num2;
}
alert(sum(10,10)); // var anotherSum = sum; //使用不带圆括号的函数名是访问函数指针,而非调用函数
alert(anotherSum(10,10)); //20 //anotherSum和sum都指向了同一个函数,所以anotherSum()可以被调用并返回结果 sum = null; //与函数“断绝关系”
alert(anotherSum(10,10)); //20,anotherSum()可以正常被调用
使用不带圆括号的函数名是访问函数指针,而非调用函数。

没有重载

将函数名想象为指针,也有助于理解为什么ECMAScript中没有函数重载的概念,声明多个同名的函数,即使传入的参数的个数不一样,该名字也只属于最后一个函数。

函数声明与函数表达式

解析器在向执行环境中加载数据时,会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。

alert(sum(10 , 20));//
function sum(num1 , num2){
return num1 + num2;
}

以上代码可以正常运行,因为在代码开始执行之前,解析器就已经通过一个名为函数声明提升的过程,读取并将函数声明添加到执行环境中,对代码求值时,js引擎在第一遍会声明函数并将它们放到源代码树的顶部,所以即使声明函数的函数在它调用的代码后,js引擎也能把函数声明提前到顶部。

   alert(add(10 , 20));//报错,停止向下解析执行
var add = function(sum1 , sum2){
return sum1 + sum2;
};

以上代码会产生错误,因为函数位于一个初始化语句中,在执行到函数所在的语句之前,变量sum中不会保存有对函数的引用,接下来将调用代码调整到add函数声明的后面:

var add = function(sum1 , sum2){
return sum1 + sum2;
};
alert(add(10 , 20));//

此时的结果就正常了,从上面的对比中可以得出这样的结论:使用第一种方式(函数声明)声明的函数会在代码解析前被解析到当前环境中,而使用第二种方式(函数表达式)声明的函数只有在解析器解析到对应的代码行时才会在当前环境中存在,此时看下面的例子就比较容易理解了:

     function sum(num1 , num2){
return "第一个sum函数";
}
alert(sum(10, 20));//第三个sum函数
var sum = function(sum1 , sum2){
return "第二个sum函数";
};
function sum(num1 , num2){
return "第三个sum函数";
}
alert(sum(10 , 20));//第二个sum函数

上面的例子中声明了3个同名的函数,上面说到,JavaScript中没有重载的概念,函数名属于最后一个声明的函数实例。对上面代码中的第一个alert(),这个结论没有错,但是对于最后的alert(),显然sum这个函数名指向的是第二个函数实例,造成这样的结果是因为第二个函数实例是最后一个被解析的,也就是说,环境中最终的sum变量指向了第二个函数实例。因此,此时就可以将上面的结论修改为:在JavaScript中没有重载的概念,多个重名的函数声明,该函数名属于最后一个被解析的函数实例。

也可以同时使用函数声明和函数表达式,如var sum = function sum(num1 , num2){},与单独使用函数表达式是等价的。不过,这种语法在Safari中会导致错误。

作为值的函数

因为ECMAScript中的函数名本身就是变量,所以函数也可以作为值来使用。也就是说,不仅可以像传递参数一样把一个函数传递给另一个函数,而且可以将一个函数作为另一个函数的结果返回。如下:

function callSomeFunction (someFunction, someArgument) {
return somFunction (someArgument);
}

这个函数接受两个参数:第一个是一个函数,第二个是要传递给该函数的一个值。下面是传递函数的一个例子:

function add10 (num) {
return num + 10;
} var result1 = callSomeFunction (add10, 10);
alert(result1); // function getGreeting (name) {
return "Hello, " + name;
} var result2 = callSomeFunction (getGreeting, "Nicholas");
alert(result2); //"Hello, Nicholas"

callSomeFunction()函数是通用的,不管第一个参数中传递进来的是什么函数,callSomeFunction函数都会返回执行第一个参数后的结果。要访问函数的指针而不执行函数的话,必须去掉函数的圆括号,所以传递给callSomeFunction的是add10和getGreeting,而不是它们执行之后的结果。

也可以从一个函数中返回另一个函数。比较典型的如数组sort()方法的比较函数,它需要接收两个参数,比较它们的值。假设有一个对象数组,要根据某个对象属性排序,可以定义一个函数,它接收一个属性名,然后根据这个属性名来创建一个函数名,下面是这个函数的定义:

function createComparisonFunction (propertyName) {
return function (object1, object2) { //object1、object2类似于sort传递给compare的value1和value2
var value1 = object1[propertyName];
var value2 = object2[propertyName]; if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
};
} var data = [{name: "Zachary", age: 28}, {name: "Nicholas", age: 29}]; data.sort(createComparisonFunction("name")); //根据name属性比较
alert(data[0].name); //Nicholas data.sort(createComparisonFunction("age"));//根据age属性比较
alert(data[0].name); //Zachary

函数内部属性

在函数内部,有两个特殊的对象:argumentsthis

1、arguments

arguments的主要用途是保存函数参数,这个对象还有一个名叫callee(callee 属性它表示对函数对象本身的引用,这有利于匿名函数的递归或者保证函数的封装性)的属性,该属性是一个指针,指向拥有这个arguments对象的函数。它可以完成函数体与函数名的解耦,如下面这个阶乘函数的应用:

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

定义阶乘函数一般都要用到递归算法;如上面的代码所示,在函数有名字,而且名字以后也不会变的情况下,这样定义没有问题。但问题是这个函数的执行与函数名factorial紧紧耦合在了一起。为了消除这种紧密耦合的现象,可以像下面这样使用arguments.callee

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

这个重写后的factorial()函数的函数体内,没有再引用函数名factorial。这样,无论引用函数时使用的是什么名字,都可以保证正常完成递归调用,例如:

     function factorial(num){
if(num <=1){
return 1;
}else{
return num*arguments.callee(num-1);
}
}
var trueFactorial = factorial;
factorial = function(){
return 0;
}; alert(trueFactorial(5));//
alert(factorial(5));//

在此,变量trueFactorial获得了factorial的值,实际上是在另一个位置上保存了一个函数的指针。然后,我们又将一个简单地返回0的函数赋值给factorial变量。如果像原来的factorial()那样不使用arguments.callee,调用trueFactorial()就会返回0。可是,在解除了函数体内的代码与函数名的耦合状态之后,trueFactorial()仍然能够正常地计算阶乘;至于factorial(),它现在只是一个返回0的函数。

2、this

函数内部另一个特殊对象是this,其行为与Java和C#中的this大致类似。即,this引用的是函数据以执行的环境对象——或者也可以说是this值(当在网页的全局作用域中调用函数时,this对象引用的就是window)。如下例

window.color = "red";
var o = { color: "blue"}; function sayColor() {
alert(this.color);
} sayColor(); //"red" o.sayColor = sayColor;
o.sayColor(); //"blue"

sayColor()在全局域中定义,this引用了window对象。因为在调用函数之前,this的值并不确定,因此this可能会在代码执行过程中引用不同的对象。当在全局域中调用时,this引用的是全局对象window;当把这个函数赋给对象o并调用o.sayColor()是,this引用的是对象o

3、caller属性

这个属性中保存着对当前函数的函数的引用,该函数调用了当前函数。对于函数来说,caller 属性只有在函数执行时才有定义。 如果是在全局作用域中调用当前函数,那么 caller 包含的就是 null 。

     function outer(){
inner();
}
function inner(){
alert(inner.caller);
}
outer(); //显示outer()函数的源代码

上面的的代码会导致警告框中显示outer()函数的源代码。因为outer()调用了inner(),所以inner.caller就指向outer()。为了实现更松散的耦合,也可以通过arguments.callee.caller来访问同样的信息。

     function outer(){
inner();
}
function inner(){
alert(arguments.callee.caller);
}
outer();

IE、Firefox、Chrome和Safari的所有版本以及Opera9.6都迟滞caller属性。

当函数在严格模式下运行时,访问arguments.callee会导致错误。ECMAScript5还定义了arguments.caller属性,但在严格模式下访问它也会导致错误,而在非严格模式下这个属性始终是undefined。定义这个属性是为了分清arguments.caller和函数的caller属性。以上变化否是为了加强这门语言的安全性,这样第三方代码就不能在相同的环境里窥视其它代码了。

严格模式还有一个限制:不能为函数的caller属性赋值,否则会导致错误。

函数属性和方法

属性

  1. length:表示函数希望接收的命名参数的个数
  2. prototype:在ECMAScript核心所定义的全部属性中,最耐人寻味的就要数prototype属性了。对于ECMAScript中的引用类型而言,proptotype是保存它们所有实例方法的真正所在。换句话说,诸如toString()和valueOf()等方法实际上都保存在prototype名下,只不过是通过各自对象的实例访问罢了。在创建自定义引用类型即实现继承时,prototype属性的作用是极为重要的。在ECMAScript5中,prototype属性是不可枚举的,因此使用for-in无法发现。

方法

每个函数都包含两个非继承而来的方法:apply()call()。这两个方法都是在特定的作用域中调用函数,实际上等于设置函数体内this对象的值。

  • apply()方法:Function.apply(obj,args)方法接收两个参数,一个是obj:这个对象将代替Function类里this对象,即其中运行函数的作用域,另一个是参数数组,可以是Array的实例,也可以是arguments对象。
    function sum(num1 , num2){
    return num1 + num2;
    }
    function callSum1(num1 , num2){
    return sum.apply(this , arguments);//传入arguments对象,此处的this是window
    }
    function callSum2(num1 , num2){
    return sum.apply(this , [num1 , num2]);//传入数组,此处的this是window
    }
    alert(callSum1(10,10));//
    alert(callSum2(10,10));//

    在上面这个例子中,callSum1()在执行sum()函数时传入了this作为this值(因为是在全局作用域中调用的,所以传入的就是window对象)和arguments对象。而callSum2同样也调用了sum()函数,但它传入的则是this和一个参数数组。这两个函数都会正常执行并返回正确的结果。

  • call()方法:call()方法与apply()方法的作用相同,他们的区别仅在于接收参数的方式不同。对于call()方法而言,第一个参数是this值没有变化,变化的是其余参数都直接传递给函数。即,在使用call()方法时,传递给函数的参数必须逐个列举出来,如下:
    function sum(num1, num2) {
    return num1 + num2;
    }
    function callSum(num1, num2) {
    return sum.call(this, num1, num2);
    } alert(callSum(10, 10)); //

    在使用call()方法的情况下,callSum()必须明确地传入每一个参数。结果与使用apply()没有什么不同。至于是使用apply()还是call(),完全取决于你采用哪种给函数传递参数的方式最方便。事实上,传递参数并非apply()和call()真正的用武之地;它们真正强大的地方是能够扩充函数赖以运行的作用域。如:

         window.color = "red";
    var o = {color:"blue"};
    function sayColor(){
    alert(this.color);
    } sayColor();//red
    sayColor.call(this);//red
    sayColor.call(window);//red
    sayColor.call(o);//blue

    这个例子中sayColor()是作为全局函数定义的,而且当在全局作用域中调用它时,它确实会显示”red”——因为this.color的值会转换成对window.color的求值。而sayColor.call(this)和sayColor.call(window),则是两种显式地在全局作用域中调用函数的方式,结果当然都会显示”red”。但是,当运行sayColor.call(o)时,函数的执行环境就不一样了,因为此时函数体内的this对象指向了o,于是结果显示的是”blue”。使用call()或(apply())来扩充作用域的最大好处就是对象不需要与方法有任何耦合关系。在前面例子的第一个版本中,先将sayColor()函数放到了对象o中,然后再通过o来调用它们的;而在这里重写的例子中,就不需要先前那个多余的步骤了。

  • bind():这个方法会创建一个函数的实例,其this值会被绑定到传给bind()函数的值。
         window.color = "red";
    var o = {color:"blue"};
    function sayColor(){
    alert(this.color);
    }
    var objectSayColor = sayColor.bind(o);
    objectSayColor();//blue

    在这里,sayColor()调用bind()并传入对象o,创建了objectSayColor()函数。objectSayColor()函数的this等于o,因此即使是在全局作用域中调用这个函数,也会看到”blue”。

    支持bind()方法的浏览器有IE9+、Firefox4+、Safari5.1+、Opera12+和Chrome。

    每个函数继承的toLocaleString()和toString()方法始终都返回函数的代码。返回代码的格式因浏览器而异。