if(arguments[i]>max){
max=arguments[i];
}
}
returnmax;
}
//调用。
varlargest=max(10,45,66,35,21);//=>66
还有重要的一点,如果函数中修改arguments[]元素,同样会影响对应的实参变量。
除以上之外,实参对象还包含了两个属性callee和caller:
callee是ECMAScript标准规范的,它指代当前正在执行的函数。
caller是非标准属性但是大多数浏览器都支持,它指代当前正在执行函数的函数。
//callee可以用来递归匿名函数。
varsum=function(x){
if(x<=1)return1;
returnx+arguments.callee(x-1);
}
//调用函数b,方法a中打印结果为函数b。
vara=function(){
alert(a.caller);
}
varb=function(){
a();
}
注意,在ECMAScript5严格模式下,对这两个属性进行读写会产生一个类型错误。
实参类型
声明JavaScript函数时形参不需要指定类型,在形参传入函数体之前也不会做任何类型检查,但是JavaScript在必要的时候会进行类型转换,例如:
functionmult(a,b){
returna*b;
}
functionconn(x,y){
returnx+y;
}
console.log(mult(3,"2"));//字符串类型自动转为数字类型,输出结果:
6
console.log(conn(3,"2"));//数字类型自动转为字符串类型,输出结果:
"32"
上述的两种类型存在隐式转换关系所以JS可以自动转换,但是还存在其他情况:
比如,一个方法期望它第一个实参为数组,传入一个非数组的值就可能引发问题,这时就应当在函数体中添加实参类型检查逻辑。
4.作为值的函数
开篇提到过,在JavaScript中函数不仅是一种语法,函数即是对象,简单归纳函数具有的几种性质:
1.函数可以被赋值给一个变量;
functionsquare(x){returnx*x;}
vars=square;//现在s和square指代同一个函数对象
square(5);//=>25
s(5);//=>25
2.函数可以保存在对象的属性或数组元素中;
vararray=[function(x){returnx*x;},20];
array[0](array[1]);//=>400
3.函数可以作为参数传入另外一个函数;
//这里定义一些简单函数。
functionadd(x,y){returnx+y;}
functionsubtract(x,y){returnx-y;}
functionmultipty(x,y){returnx*y;}
functiondivide(x,y){returnx/y;}
//这里函数以上面某个函数做参数。
functionoperate(operator,num1,num2){
returnoperator(num1,num2);
}
//调用函数计算(4*5)-(2+3)的值。
varresult=operate(subtract,operate(multipty,4,5),operate(add,2,3));
console.log(result);//=>15
4.函数可以设置属性。
//初始化函数对象的计数器属性。
uniqueInteger.counter=0;
//先返回计数器的值,然后计数器自增1。
functionuniqueInteger(){
returnuniqueInteger.counter+=1;
}
当函数需要一个“静态”变量来在调用时保持某个值不变,最方便的方式就是给函数定义属性,而不是定义全局变量,因为定义全局变量会让命名空间变的杂乱无章。
5.作为命名空间的函数
函数中声明的变量只在函数内部是有定义,不在任何函数内声明的变量是全局变量,它在JavaScript代码中的任何地方都是有定义的。
JavaScript中没有办法声明只在一个代码块内可见的变量的。
基于这个原因,常常需要定义一个函数用作临时的命名空间,在这个命名空间内定义的变量都不会污染到全局变量。
//该函数就可看作一个命名空间。
functionmymodule(){
//该函数下的变量都变成了“mymodule”空间下的局部变量,不会污染全局变量。
}
//最后需要调用命名空间函数。
mymodule();
上段代码还是会暴露出一个全局变量:
mymodule函数。
更为常见的写法是,直接定义一个匿名函数,并在单个表达式中调用它:
//将上面mymodule()函数重写成匿名函数,结束定义并立即调用它。
(function(){
//模块代码。
}());
6.闭包
闭包是JavaScript中的一个难点。
在理解闭包之前先要明白变量作用域和函数作用域链两个概念。
变量作用域:
无非就是两种,全局变量和局部变量。
全局变量拥有全局作用域,在任何地方都是有定义的。
局部变量一般是指在函数内部定义的变量,它们只在函数内部有定义。
函数作用域链:
我们知道JavaScript函数是可以嵌套的,子函数对象会一级一级地向上寻找所有父函数对象的变量。
所以,父函数对象的所有变量,对子函数对象都是可见的,反之则不成立。
需要知道的一点是,函数作用域链是在定义函数的时候创建的。
关于“闭包”的概念书本上定义很具体,但是也很抽象,很难理解。
简单的理解,“闭包”就是定义在一个函数内部的函数(这么说并不准确,应该说闭包是函数的作用域)。
varscope="globalscope";//全局变量
functioncheckscope(){
varscope="localscope";//局部变量
functionf(){returnscope;}//在作用域中返回这个值
returnf();
}
checkscope();//=>"localscope"
上面一段代码就就实现了一个简单的闭包,函数f()就是闭包。
根据输出结果,可以看出闭包可以保存外层函数局部变量,通过闭包可以把函数内的变量暴露在全局作用域下。
闭包有什么作用呢?
下面一段代码是上文利用函数属性定义的一个计数器函数,其实它存在一个问题:
恶意代码可以修改counter属性值,从而让Integer函数计数出错。
//初始化函数对象的计数器属性。
uniqueInteger.counter=0;
//先返回计数器的值,然后计数器自增1。
functionuniqueInteger(){
returnuniqueInteger.counter+=1;
}
闭包可捕捉到单个函数调用的局部变量,并将这些局部变量用作私有状态,故我们可以利用闭包的特性来重写uniqueInteger函数。
//利用闭包重写。
varuniqueInteger=(function(){//定义函数并立即调用
varcounter=0;//函数的私有状态
returnfunction(){
returncounter+=1;
};
})();
//调用。
uniqueInteger();//=>1
uniqueInteger();//=>2
uniqueInteger();//=>3
当外部函数返回后,其他任何代码都无法访问counter变量,只有内部的函数才能访问。
根据输出结果可以看出,闭包会使得函数中的变量都被保存在内存中,内存消耗大,所以要合理使用闭包。
像counter一样的私有变量在多个嵌套函数中都可以访问到它,因为这多个嵌套函数都共享同一个作用域链,看下面一段代码:
functioncounter(){
varn=0;
return{
count:
function(){returnn+=1;},
reset:
function(){n=0;}
};
}
varc=counter(),d=counter();//创建两个计时器
c.count();//=>0
d.count();//=>0能看出它们互不干扰
c.reset();//reset和count方法共享状态
c.count();//=>0因为重置了计数器c
d.count();//=>1而没有重置计数器d
书写闭包的时候还需注意一件事,this是JavaScript的关键字,而不是变量。
因为闭包内的函数只能访问闭包内的变量,所以this必须要赋给that才能引用。
绑定arguments的问题与之类似。
varname="TheWindow";
varobject={
name:
"MyObject",
getName:
function(){
varthat=this;
returnfunction(){
returnthat.name;
};
}
};
console.log(object.getName()());//=>"MyObject"
到这里如果你还不明白我在说什么,这里推荐两篇前辈们写的关于“闭包”的文章。
阮一峰,学习Javascript闭包(Closure)
russj,JavaScript闭包的理解
7.函数属性、方法和构造函数
前文已经介绍过,在JavaScript中函数也是对象,它也可以像普通对象一样拥有属性和方法。
length属性
在函数体里,arguments.length表示传入函数的实参的个数。
而函数本身的length属性表示的则是“形参”,也就是在函数调用时期望传入函数的实参个数。
functioncheck(args){
varactual=args.length;//参数的真实个数
varexpected=args.callee.length;//期望的实参个数
if(actual!
=expected){//如果不同则抛出异常
throwError("Expected"+expected+"args;got"+actual);
}
}
functionf(x,y,z){
check(arguments);//检查实参和形参个数是否一致。
returnx+y+z;
}
prototype属性
每个函数都包含prototype属性,这个属性指向一个对象的引用,这个对象也就是原型对象。
当将函数用作构造函数的时候,新创建的对象会从原型对象上继承属性。
call()方法和apply()方法
上文提到,这两个方法可以用来间接调用函数。
call()和apply()的第一个实参表示要调用函数的母对象,它是调用上下文,在函数内通过this来引用母对象。
假如要想把函数func()以对象obj方法的形式来调用,可以这样:
func.call(obj);
func.apply(obj);
call()和apply()的区别之处是,第一个实参(调用上下文)之后的所有实参传入的方式不同。
func.call(obj,1,2);//实参可以为任意数量
func.apply(obj,[1,2]);//实参都放在了一个数组中
下面看一个有意思的函数,他能将一个对象的方法替换为一个新方法。
这个新方法“包裹”了原始方法,实现了AOP。
//调用原始方法之前和之后记录日志消息
functiontrace(o,m){
varoriginal=o[m];//在闭包中保存原始方法
o[m]=function(){//定义新方法
console.log(newDate(),"Entering:
",m);//输出日志消息
varresult=original.apply(o,arguments);//调用原始方法
console.log(newDate(),"Exiting:
",m);//输出日志消息
returnresult;//返回结果
}
}
这种动态修改已有方法的做法,也被称作“猴子补丁(monkey-patching)”。
bind()方法
bind()方法是ES5中新增的方法,这个方法的主要作用是将函数绑定至某个对象。
该方法会返回一个新的函数,调用这个新的函数会将原始函数当作传入对象的方法来调用。
functionfunc(y){returnthis.x+y;}//待绑定的函数
varo={x:
1};//将要绑定的对象
varf=func.bind(o);//通过调用f()来调用o.func()
f
(2);//=>3
ES3中可以通过下面的代码来实现bind()方法:
if(!
Function.prototype.bind){
Function.prototype.bind=function(o/*,args*/){
//将this和arguments保存在变量中,以便在嵌套函数中使用。
varself=this,boundArgs=arguments;
//bind()方法返回的是一个函数。
returnfunction(){
//创建一个参数列表,将传入bind()的第二个及后续的实参都传入这个函数。
varar