函数原型
如果我们点开了我们喜欢的浏览器+JavaScript控制台,让我们看一下Function.prototype对象的属性:
Object.getOwnPropertyNames(Function.prototype)
//=> ["length", "name", "arguments", "caller",
// "constructor", "bind", "toString", "call", "apply"]
这里的输出依赖于你使用的浏览器和JavaScript版本。(我用的是Chrome 33), 我们看到一些我们感兴趣的几个属性。鉴于这篇文章的目的,我会讨论下这几个:
Function.prototype.length
Function.prototype.call
Function.prototype.apply
第一个是个属性,另外两个是方法。除了这三个,我还会愿意讨论下这个特殊的变量arguments,它和Function.prototype.arguments(已被弃用)稍有不同。
首先,我将定义一个“tester”函数来帮助我们弄清楚发生了什么。
var tester = function (a, b, c){
console.log({
this: this,
a: a,
b: b,
c: c
});
};
这个函数简单记录了输入参数的值,和“上下文变量”,即this的值。 现在,让我们尝试一些事情:
tester("a");
//=> {this: Window, a: "a", b: (undefined), c: (undefined)}
tester("this", "is", "cool");
//=> {this: Window, a: "this", b: "is", c: "cool"}
我们注意到如果我们不输入第2、3个参数,程序将会显示它们为undefined(未定义)。此外,我们注意到这个函数默认的“上下文”是全局对象window。
使用Function.prototype.call
一个函数的 .call 方法以这样的方式调用这个函数,它把上下文变量this设置为第一个输入参数的值,然后其他的的参数一个跟一个的也传进函数。
语法:
fn.call(thisArg[, arg1[, arg2[, ...]]])
因此,下面这两行是等效的:
tester("this", "is", "cool");
tester.call(window, "this", "is", "cool");
当然,我们能够随需传入任何参数:
tester.call("this", "is", "even", "cooler");
//=> {this: "this", a: "is", b: "even", c: "cooler"}
这个方法主要的功能是设置你所调用函数的this变量的值。
使用Function.prototype.apply
函数的.apply方法比.call更实用一些。和.call类似,.apply的调用方式也是把上下文变量this设置为输入参数序列中的第一个参数的值。输入参数序列的第二个参数也是最后一个,以数组(或者类数组对象)的方式传入。
语法:
fun.apply(thisArg, [argsArray])
因此,下面三行全部等效:
tester("this", "is", "cool");
tester.call(window, "this", "is", "cool");
tester.apply(window, ["this", "is", "cool"]);
能够以数组的方式指定一个参数列表在多数时候非常有用(我们会发现这样做的好处的)。例如,Math.max是一个可变参数函数(一个函数可以接受任意数目的参数)。
Math.max(1,3,2);
//=> 3
Math.max(2,1);
//=> 2
这样,如果我有一个数值数组,并且我需要利用Math.max函数找出其中最大的那个,我怎么用一行代码来做这个事儿呢?
var numbers = [3, 8, 7, 3, 1];
Math.max.apply(null, numbers);
//=> 8
apply方法真正开始显示出它的重要是当配上特殊参数:Arguments对象。
每个函数表达式在它的作用域中都有一个特殊的、可使用的局部变量:arguments。为了研究它的属性,让我们创建另一个tester函数:
var tester = function(a, b, c) {
console.log(Object.getOwnPropertyNames(arguments));
};
注:在这种情况下我们必须像上面这样使用Object.getOwnPropertyNames,因为arguments有一些属性没有标记为可以被枚举的,于是如果仅仅使用console.log(arguments)这种方式它们将不会被显示出来。
现在我们按照老办法,通过调用tester函数来测试下:
tester("a", "b", "c");
//=> ["0", "1", "2", "length", "callee"]
tester.apply(null, ["a"]);
//=> ["0", "length", "callee"]
arguments变量的属性中包括了对应于传入函数的每个参数的属性,这些和.length属性、.callee属性没什么不同。
.callee属性提供了调用当前函数的函数的引用,但是这并不被所有的浏览器支持。就目前而言,我们忽略这个属性。
让我们重新定义一下我们的tester函数,让它丰富一点:
var tester = function() {
console.log({
'this': this,
'arguments': arguments,
'length': arguments.length
});
};
tester.apply(null, ["a", "b", "c"]);
//=> { this: null, arguments: { 0: "a", 1: "b", 2: "c" }, length: 3 }
Arguments:是对象还是数组?
我们看得出,arguments完全不是一个数组,虽然多多少少有点像。在很多情况下,尽管不是,我们还是希望把它当作数组来处理。把arguments转换成一个数组,这有个非常不错的快捷小函数:
function toArray(args) {
return Array.prototype.slice.call(args);
}
var example = function(){
console.log(arguments);
console.log(toArray(arguments));
};
example("a", "b", "c");
//=> { 0: "a", 1: "b", 2: "c" }
//=> ["a", "b", "c"]
这里我们利用Array.prototype.slice方法把类数组对象转换成数组。因为这个,在与.apply同时使用的时候arguments对象最终会极其有用。
一些例子:
// old version
var logWrapper = function (f) {
return function (a) {
console.log('calling "' + f.name + '" with argument "' + a);
return f(a);
};
};
当然了,我们既有的知识让我们能够构建一个可以服务于任何函数的logWrapper函数:
// new version
var logWrapper = function (f) {
return function () {
console.log('calling "' + f.name + '"', arguments);
return f.apply(this, arguments);
};
};
通过调用
f.apply(this, arguments);
我们确定这个函数f会在和它之前完全相同的上下文中被调用。于是,如果我们愿意用新的”wrapped”版本替换掉我们的代码中的那些日志记录函数是完全理所当然没有唐突感
把原生的prototype方法放到公共函数库中
浏览器有大量超有用的方法我们可以“借用”到我们的代码里。方法常常把this变量作为“data”来处理。在函数式编程,我们没有this变量,但是我们无论如何要使用函数的!
var demethodize = function(fn){
return function(){
var args = [].slice.call(arguments, 1);
return fn.apply(arguments[0], args);
};
};
一些别的例子:
// String.prototype
var split = demethodize(String.prototype.split);
var slice = demethodize(String.prototype.slice);
var indexOfStr = demethodize(String.prototype.indexOf);
var toLowerCase = demethodize(String.prototype.toLowerCase);
// Array.prototype
var join = demethodize(Array.prototype.join);
var forEach = demethodize(Array.prototype.forEach);
var map = demethodize(Array.prototype.map);
当然,许多许多。来看看这些是怎么执行的:
("abc,def").split(",");
//=> ["abc","def"]
split("abc,def", ",");
//=> ["abc","def"]
["a","b","c"].join(" ");
//=> "a b c"
join(["a","b","c"], " ");
// => "a b c"
管理参数顺序
// shift the parameters of a function by one
var ignoreFirstArg = function (f) {
return function(){
var args = [].slice.call(arguments,1);
return f.apply(this, args);
};
};
// reverse the order that a function accepts arguments
var reverseArgs = function (f) {
return function(){
return f.apply(this, toArray(arguments).reverse());
};
};
组合函数
在函数式编程世界里组合函数到一起是极其重要的。通常的想法是创建小的、可测试的函数来表现一个“单元逻辑”,这些可以组装到一个更大的可以做更复杂工作的“结构”。
// compose(f1, f2, f3..., fn)(args) == f1(f2(f3(...(fn(args...)))))
var compose = function (/* f1, f2, ..., fn */) {
var fns = arguments,
length = arguments.length;
return function () {
var i = length;
// we need to go in reverse order
while ( --i >= 0 ) {
arguments = [fns[i].apply(this, arguments)];
}
return arguments[0];
};
};
// sequence(f1, f2, f3..., fn)(args...) == fn(...(f3(f2(f1(args...)))))
var sequence = function (/* f1, f2, ..., fn */) {
var fns = arguments,
length = arguments.length;
return function () {
var i = 0;
// we need to go in normal order here
while ( i++ < length ) {
arguments = [fns[i].apply(this, arguments)];
}
return arguments[0];
};
};
常用实例
a、
这个例子中的意思就是用 add 来替换 sub,add.call(sub,3,1) == add(3,1) ,所以运行结果为:alert(4); // 注意:js 中的函数其实是对象,函数名是对 Function 对象的引用。
b、
call 的意思是把 animal 的方法放到cat上执行,原来cat是没有showName() 方法,现在是把animal 的showName()方法放到 cat上来执行,所以this.name 应该是 Cat
c、实现继承
Animal.call(this) 的意思就是使用 Animal对象代替this对象,那么 Cat中不就有Animal的所有属性和方法了吗,Cat对象就能够直接调用Animal的方法以及属性了.
d、多重继承
很简单,使用两个 call 就实现多重继承了
当然,js的继承还有其他方法,例如使用原型链,这个不属于本文的范畴,只是在此说明call 的用法。说了call ,当然还有 apply,这两个方法基本上是一个意思,区别在于 call 的第二个参数可以是任意类型,而apply的第二个参数必须是数组,也可以是arguments