在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代码,第二次是解析传入构造函数中的字符串),从而影响性能。不过,这种语法对于理解“函数是对象,函数名是指针”的概念倒是非常直观的。
由于函数名仅仅是执行函数的指针,因此函数名与包含对象指针的其他变量没有什么不同。换句话说,一个函数可能会有多个名字。
没有重载
将函数名想象为指针,也有助于理解为什么ECMAScript中没有函数重载的概念,声明多个同名的函数,即使传入的参数的个数不一样,该名字也只属于最后一个函数。
函数声明与函数表达式
解析器在向执行环境中加载数据时,会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行,如:
alert(sum(10 , 20));//30
function sum(num1 , num2){
return num1 + num2;
}
//alert(add(10 , 20));//报错,停止向下解析执行
var add =
function(sum1 , sum2){
return sum1 + sum2;
};
在上面的例子中分别使用函数声明和函数表达式定义了两个函数sum(num1 , num2)和add(sum1 , sum2),同样是函数,但是调用的结果却大相径庭。在sum前调用sum函数,能够正常执行,也就是在sum声明代码行前调用该函数,当前环境中即存在该函数;但是对于add函数,在声明代码行对其进行调用却报错了。
接下来将调用代码调整到add函数声明的后面:
var add =
function(sum1 , sum2){
return sum1 + sum2;
};
alert(add(10 , 20));//30
此时的结果就正常了,从上面的对比中可以得出这样的结论,使用第一种方式声明的函数会在代码解析前被解析到当前环境中,而使用第二种方式声明的函数只有在解析器解析到对应的代码行时才会在当前环境中存在,此时看下面的例子就比较容易理解了:
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中没有重载的概念,多个重名的函数声明,该函数名属于最后一个被解析的函数实例。
也可以同时使用函数声明和函数表达式,如varsum = function sum(num1 , num2){},与单独使用函数表达式是等价的。不过,这种语法在Safari中会导致错误。
作为值的函数
因为ECMAScript中的函数名本身就是变量,所以函数也可以作为值来使用。也就是说,不仅可以像传递参数一样把一个函数传递给另一个函数,而且可以将一个函数作为另一个函数的结果返回。如:
//将一个函数作为参数传递给另一个函数
function add(num1 , num2){
return num1 + num2;
}
function raise(add , num1 , num2){
return add(num1 , num2)+1;
}
alert(raise(add, 10 , 10));//21
在Array类型中,其迭代方法的形式与上例中一致,如:
var numbers = [0,1,2,3];
var everyResult = numbers.every(function(item , index , array){
return (item > 2);
});
alert(everyResult);//false
var someResult = numbers.some(function(item , index , array){
return (item > 2);
});
alert(someResult);//true
将函数作为返回值返回是一种极为有用的技术,例如在数组排序时需要项sort()方法中传入一个比较函数,如果想在传入的函数中指定排序的方式,则可以如下进行操作:
function compare(asc){
alert(asc);
if(asc){
return
function(value1 , value2){
if(value1 < value2){
return -1;
} else
if(value1 > value2){
return 1;
}else{
return 0;
}
};
}else{
return
function(value1 , value2){
if(value1 > value2){
return -1;
} else
if(value1 < value2){
return 1;
}else{
return 0;
}
};
}
}
var array = [0,5,1,15,10];
array.sort(compare(false));
alert(array);
在compare函数中根据指定的asc属性判断返回降序排列的函数还是升序排列的函数。
函数内部属性
在函数内部,有两个特殊的对象:arguments和this。其中arguments是一个类数组对象,包含着函数中的所有参数。虽然arguments的主要用途是保存函数参数,但这个对象还有一个名叫callee的属性,该属性是一个指针,指向拥有这个arguments对象的函数,看下面的经典的阶乘函数:
function factorial(num){
if(num <=1){
return 1;
}else{
return num*factorial(num-1);
}
}
alert(factorial(5));//120
定义阶乘函数一般都要用到递归算法;如上面的代码所示,在函数有名字,而且名字以后也不会变的情况下,这样定义没有问题。但问题是这个函数的执行与函数名factorial紧紧耦合在了一起。为了消除这种紧密耦合的现象,可以像下面这样使用arguments.callee:
function factorial(num){
if(num <=1){
return 1;
}else{
return num*arguments.callee(num-1);
}
}
alert(factorial(5));//120
这个重写后的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));//120
alert(factorial(5));//0
在此,变量trueFactorial获得了factorial的值,实际上是在另一个位置上保存了一个函数的指针。然后,我们又将一个简单地返回0的函数赋值给factorial变量。如果像原来的factorial()那样不使用arguments.callee,调用trueFactorial()就会返回0。可是,在解除了函数体内的代码与函数名的耦合状态之后,trueFactorial()仍然能够正常地计算阶乘;至于factorial(),它现在只是一个返回0的函数。
函数内部的另一个特殊对象是this,其行为与Java和C#中的this大致类似。换句话说,this引用的是函数执行的环境对象——或者也可以说是this值(当在网页的全局作用域中调用函数时this对象引用的就是winsow)。如:
console.log(this);//window
window.hello = "hello world";
alert(this.hello);//hello world
ECMAScript 5 也规范化了另一个函数对象的属性:caller。除了Opera的早期版本不支持,其他浏览器都支持这个ECMAScript3并没有定义的属性。这个属性中保存着调用当前函数的函数的引用,如果是在全局作用域中调用当前函数,它的值为null。如:
function outer(){
inner();
}
function inner(){
alert(inner.caller);
}
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属性赋值,否则会导致错误。
函数属性和方法
ECMAScript中的函数是对象,因此函数也有属性和方法。每个函数都包含两个属性length和prototype。其中length属性表示函数希望接收的命名参数的个数。
在ECMAScript核心所定义的全部属性中,最耐人寻味的就要数prototype属性了。对于ECMAScript中的引用类型而言,proptotype是保存它们所有实例方法的真正所在。换句话说,诸如toString()和valueOf()等方法实际上都保存在prototype名下,只不过是通过各自对象的实例访问罢了。在创建自定义引用类型即实现继承时,prototype属性的作用是极为重要的。在ECMAScript5中,prototype属性是不可枚举的,因此使用for-in无法发现。
没个函数都包含两个非继承而来的方法:apply()和call(),这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this对象的值。首先,apply()方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组。其中,第二个参数可以是Array的实例,也可以是arguments对象。如:
function sum(num1 , num2){
return num1 + num2;
}
function callSum1(num1 , num2){
return sum.apply(this , arguments);//传入arguments对象
}
function callSum2(num1 , num2){
return sum.apply(this , [num1 , num2]);//传入数组
}
alert(callSum1(10,10));//20
alert(callSum2(10,10));//20
在上面这个例子中,callSum1()在执行sum()函数时传入了this作为this值(因为是在全局作用域中调用的,所以传入的就是window对象)和arguments对象。而callSum2同样也调用了sum()函数,但它传入的则是this和一个参数数组。这两个函数都会正常执行并返回正确的结果。
在严格模式下,为指定环境对象而调用函数,则this只不会转型为window。除非把函数添加到某个对象或者调用apply()或call(),否则this值将是undefined。
call()方法与apply()方法的作用相同,它们的区别仅在于接收参数的方式不同。对于call()方法而言,第一个参数是this值没有变化,变化的是其余参数都是直接传递给函数。换句话说,在使用call()方法时,传递给函数的参数必须逐个列举出来,如:
function sum(num1 , num2){
return num1 + num2;
}
function callSum1(num1 , num2){
return sum.call(this , num1 , num2);
}
alert(callSum1(10,10));//20
在使用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())来扩充作用域的最大好处就是对象不需要与方法有任何耦合关系。在前面例子的第一个版本vzhong,先将sayColor()函数放到了对象o中,然后再通过o来调用它们的;而在这里重写的例子中,就不需要先前那个多余的步骤了。
ECMAScript5还定义了一个方法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()方法始终都返回函数的代码。返回代码的格式因浏览器而异。