内容要点:
在JS程序中,函数是值。对函数执行typeof运算会返回字符串 "function",但是函数是JS中特殊的对象。因为函数也是对象,它们也可以拥有属性和方法,就像普通的对象可以拥有属性和方法一样。甚至可以用Function()构造函数来创建新的函数对象。
一.length属性
在函数体内,arguments.length表示传入函数的实参的个数。
而函数本身的length属性则有着不同含义。函数length属性是只读属性,它代表函数实参的数量,这里的参数指的是"形参"而非"实参",也就是在函数定义时给出的实参个数,通常也是在函数调用时期望传入函数的实参个数。
例:check()函数判断传入的实参个数是否正确
//从另外一个函数给它传入argument数组,它比较arguments.length(实际传入的实参个数)和arguments.callee.length(期望传入的实参个数)来判断所传入的实参个数是否正确。如果个数不正确,则抛出异常。
//这个函数使用arguments.callee,因此它不能再严格模式下工作
function check(args){
var actual = args.length; //实参的真实个数
var expected = args.callee.length //期望的实参个数
if(actual !==expected) throw Error("Expected"+expected+"args;got"+actual);
}
function f(x,y,z){
check(arguments); //检查实参个数和期望的实参个数是否一致
return x+y+z; //再执行函数的后续逻辑
}
二.prototype属性
每一个函数都包含一个prototype属性,这个属性是指向一个对象的引用,这个对象称做 "原型对象"(prototype object)。每一个函数都包含不同的原型对象。当将函数用做构造函数的时候,新创建的对象会从原型对象上继承属性。
三.call()方法和apply()方法
1.可以将call()和apply()看做是某个对象的方法,通过调用方法的形式来间接调用函数。
例如:Object.prototype.toString.call(o).slice(8,-1); 使用了call()方法来调用了一个对象的Object.prototype.toString方法,用以输出对象的类。
call()和apply()的第一个实参是调用函数的母对象,它是调用上下文,在函数体内通过this来获得对它的引用。
以对象o的方法来调用函数f(),这样使用call()和apply():
f.call(o);
f.apply(o);
上述代码和下面代码的功能类似:
o.m=f; //将f存储为o的临时方法
o.m(); //调用它,不传入参数
delete o.m; //将临时方法删除
在ES5的严格模式中,call()和apply()的第一个实参都会变成this的值,哪怕传入的实参是原始值甚至是null和undefined。在ES3和非严格模式中,传入的null和undefined都会被全局对象代替,而其他原始值则会被相应的包装对象(wrapper object)所替代。
2. 对于call()来说,第一个调用上下文实参之后的所有实参就是要传入待调用函数的值。比如,以对象o的方法的形式调用函数f(),并传入两个参数:
f.call(o,1,2);
对于apply()方法和call()类似,但传入实参的形式和call()有所不同,它的实参都放入一个数组中:
f.apply(o,[1,2]);
如果一个函数的实参可以是任意数量,给apply()传入的参数数组可以是任意长度的。比如,为了找出数组中最大的数组元素,调用Math.max()方法的时候可以给apply()传入一个包含任意个元素的数组:
var biggest = Math.max.apply(Math,array_of_numbers);
需要注意的是,传入apply()的参数数组可以是类数组对象也可以是真实数组。实际上,可以将当前函数的arguments数组直接传入(另一个函数的)apply()来调用另一个函数。
//将对象o中名为m()的方法替换为另一个方法,可以在调用原始的方法之前和之后记录日志消息
function trace(o,m){
var original = o[m]; //在闭包中保持原始方法
o[m] = function(){
console.log(new Date(),"Entering",m); //输出日志消息
console.log(arguments); //[3]
arguments[0]=arguments[0]*arguments[0];
var result = original.apply(this,arguments); //调用原始函数
console.log(new Date(),"Exiting",m); //输出日志消息
return result;
};
}
var user = {name:"hanxuming",age:"23",add:function(x){ return x+x;}};
trace(user,"add");
console.log(user["add"](3)); //18
trace()函数接收两个参数,一个对象和一个方法名,它将指定的方法替换为一个新方法,这个新方法是"包裹"原始方法的另一个泛函数(泛函数也叫泛函,在这里特指一种变换,以函数为输入,输出可以是值也可以是另一个函数)这种动态修改已有方法的做法有时称作"monkey-patching".
四.bind()方法
1.bind()是在ES5中新增的方法,但在ES3中可以轻易模拟bind()。
这个方法的主要作用就是将函数绑定某个对象。当在函数f()上调用bind()方法并传入一个对象0作为对象,这个方法返回一个新的函数。
以函数调用的方式调用新的函数将会把原始的函数f()当作o的方法来调用。传入新函数的任何实参都将传入原始函数。
例如:
function f(y){ return this.x+y; } //这个是待绑定的函数
var o = {x:1} //将要绑定的对象
var g = f.bind(o); //通过调用g(x)来调用o.f(x)
g(2) //=>3
可以通过如下代码轻易地实现这种绑定:
//返回一个函数,通过调用它来调用o中的方法f(),传递它所有的实参
function bind(f,o){
if(f.bind) return f.bind(o); //如果bind()方法存在的话,使用bind()方法
else return function(){
return f.apply(o,arguments);
};
}
2.ES5中的bind()方法不仅仅是将函数绑定至一个对象,它还附带一些其他应用:
除了第一个实参之外,传入bind()的实参也会绑定至this,这个附带的应用是一种常见的函数式编程技术,有时也称为 "柯里化"。
例如:
var sum = function(x,y){ return x+y }; //返回两个实参和值
//创建一个类似sum的新函数,但this的值绑定到null
//并且第一个参数绑定到1,这个新的函数期望只传入一个实参。
var succ = sum.bind(null,1);
succ(2) //=>3:x绑定到1,并传入2作为实参y
function f(y,z){ return this.x+y+z }; //另外一个做累加计算的函数
var g = f.bind({x:1},2); //绑定x和y
g(3) //=>6:this.x绑定到1,y绑定到2,z绑定到3
注意,我们将这个方法另存为Function.prototype.bind,以便所有的函数对象都继承它,
ES3版本的Function.bind()方法:
if(!Function.prototype.bind){
Function.prototype.bind = function(o/*,args*/){
var self =this,boundArgs = arguments; //将this和arguments的值保存至变量中,以便在后面嵌套的函数中使用它们
//bind()方法的返回值是一个函数
return function(){
var args = [],i;
for( i=0;i<boundArgs.length;i++ ) args.push(boundArgs[i]);
for( i=0;i<arguments.length;i++ ) args.push(arguments[i]);
return self.apply(o,args); //现在将self作为o的方法来调用,传入这些实参
};
};
}
ES5定义的bind()方法也有一些特性是上述ES3代码无法模拟的:
首先,真正的bind()方法返回一个函数对象,这个函数对象的length属性是绑定函数形参个数减去绑定实参的个数(length的值不能为零)。
再者,ES5的bind()方法可以顺带用做构造函数,如果bing()返回的函数用做构造函数,将忽略传入bind()的this,原始函数就会以构造函数的形式调用,其实参也已经绑定。(意思是在运行时将bind()所返回的函数用做构造函数时,所传入实参会原封不动的传入原始函数。)
由bind()方法所返回的函数并不包含prototype属性(普通函数固有的prototype属性是不能删除的),并且将这些 绑定的函数 用做 构造函数时 所创建的对象从 原始的未绑定的构造函数 中继承prototype。
同样,在使用instanceof运算符时,绑定构造函数和未绑定构造函数并无两样。
五.toString()方法
ES规范规定这个方法返回一个字符串,这个字符串和函数声明语句的语法相关。实际上,大多数(非全部)的toString()方法的实现都返回函数的完整源码。内置函数往往返回一个类似"[native code]"的字符串作为函数体。
六.Function构造函数
不管是通过函数定义语句还是函数直接量表达式,函数的定义都要使用function关键字。但函数还可以通过Function()构造函数来定义,
例如:
var f = new Function("x","y","return x*y;");
这一行代码创建一个新的函数,这个函数和通过下面代码定义的函数几乎等价:
var f = function(x,y){ return x*y };
Function()构造函数可以传入任意数量的字符串实参,最后一个实参所表示的文本就是函数体;它可以包含任意的JS语句,每两条语句之间用分号分隔。传入构造函数的其他所有的实参字符串是指定函数的形参名字的字符串。如果定义的函数不包含任何参数,只须给构造函数简单地传入一个字符串--函数体--即可。
注意,Function()构造函数并不需要通过传入实参以指定函数名。就像函数直接量一样,Function()构造函数创建一个匿名函数。
关于Function()构造函数有几点需要特别注意:
Function()构造函数允许JS在运行时动态地创建并编译函数
每次调用Function()构造函数都会解析函数体,并创建新的函数对象。如果是一个循环或者多次调用的函数中执行这个构造函数,执行效率会受影响。相比之下,循环中的嵌套函数和函数定义表达式则不会每次执行时都重新编译。
最后一点,也是关于Function()构造函数非常重要的一点,就是它所创建的函数并不是使用词法作用域,相反,函数体代码的编译总是会在顶层函数(也就是全局作用域)执行。
例如:
var scope = "global";
function constructFunction(){
var scope = "local";
return new Function(" return scope"); //无法捕获局部作用域
}
//这一行代码返回global,因为通过Function()构造函数,所返回的函数使用的不是局部作用域
constructFunction()(); //=>"global"
我们可以将Function()构造函数认为是在全局作用域中执行的eval(),eval()可以在自己的私有作用域内定义新变量和函数,Function()构造函数在实际编程过程中很少会用到。
七.可调用的对象
"可调用的对象"(callable object)是一个对象,可以在函数调用表达式中调用这个对象。所有的函数都是可调用的,但并非所有的可调用对象都是函数。、
截至目前,可调用对象在两个JS实现中不能算作函数。首先IE Web浏览器(IE8及以前的版本)实现了客户端方法(诸如window.alert()和Document.getElementById()),使用了可调用的宿主对象,而不是内置函数对象。IE中的这些方法在其他浏览器中也都存在,但它们本质上不是Function对象。IE9将它们实现为真正的函数,因此这类可调用的对象将越来越罕见。
另外一个常见的可调用对象是RegExp对象(在众多浏览器中均有实现),可以直接调用RegExp对象,这比调用它的exec()方法更快捷一些。
对RegExp执行typeof运算的结果并不统一,在有些浏览器中返回 "function",在有些中返回 "object"
如果检测一个对象是否是真正的函数对象(并且具有函数方法):
function isFunction(x){
return Object.prototype.toString.call(x) === "[object Function]";
}