函数是这样的一段JavaScript代码,它只定义一次,但可能被执行或调用任意次。
- 函数的定义会包括一个称为形参(parameter)的标识符列表,这些参数在函数体中像局部变量一样工作。
- 函数调用会为形参提供实参的值。函数使用它们实参的值来计算返回值,成为该函数调用表达式的值。
- 除了实参之外,每次调用还会拥有另一个值——本次调用的上下文——这就是this关键字的值。
如果函数挂载在一个对象上,作为对象的一个属性,就称它为对象的方法。当通过这个对象来调用函数时,该对象就是此次调用的上下文(context),也就是该函数的this的值。用于初始化一个新创建的对象的函数称为构造函数
在JavaScript里,函数即对象,JavaScript可以把函数赋值给变量,或者作为参数传递给其他函数。因为函数就是对象,所以可以给它们设置属性,甚至调用它们的方法。
JavaScript的函数可以嵌套在其他函数中定义,这样它们就可以访问它们被定义时所处的作用域中的任何变量。这意味着JavaScript函数构成了一个闭包(closure)
1. 函数定义
函数使用function关键字来定义,两种定义形式:有函数定义表达式或者函数声明语句
//输出o的每个属性的名称和值,返回undefined
function printprops(o) {
for (var p in o) console.log(p + ":" + o[p] + "\n");
}
//计算两个笛卡尔坐标(x1,y1)和(x2,y2)之间的距离
function distance(x1, y1, x2, y2) {
var dx = x2 - x1;
var dy = y2 - y1;
return Math.sqrt(dx * dx + dy * dy);
}
//计算阶乘的递归函数(调用自身的函数)
//x!的值是从x到x递减(步长为1)的值的累乘
function factorial(x) {
if (x <= 1) return 1;
return x * factorial(x - 1);
}
//这个函数表达式定义了一个函数用来求传入参数的平方
//注意我们把它赋值给一个变量
var square = function(x) {
return x * x;
};
//函数表达式可以包含名称,这在递归时很有用
var f = function fact(x) {
if (x <= 1) return 1;
else return x * fact(x - 1);
};
//函数表达式也可以作为参数传给其他函数
data.sort(function(a, b) {
return a - b;
});
//函数表达式有时定义后立即调用
var tensquared = (function(x) {
return x * x;
})(10);
注意:以表达式方式定义的函数,函数的名称是可选的。一条函数声明语句实际上声明了一个变量,并把一个函数对象赋值给它。
如果一个函数定义表达式包含名称,函数的局部作用域将会包含一个绑定到函数对象的名称。实际上,函数的名称将成为函数内部的一个局部变量。
函数名称通常是动词或以动词为前缀的词组。通常函数名的第一个字符为小写
return语句导致函数停止执行,并返回它的表达式(如果有的话)的值给调用者。如果return语句没有一个与之相关的表达式,则它返回undefined值。如果一个函数不包含return语句,那它就只执行函数体中的每条语句,并返回undefined值给调用者。
1.1 嵌套函数
在JavaScript里,函数可以嵌套在其他函数里。例如:
function hypotenuse(a, b) {
function square(x) {
return x * x;
}
return Math.sqrt(square(a) + square(b));
}
嵌套函数(内部函数)的有趣之处在于它的变量作用域规则:它们可以访问嵌套它们(或多重嵌套)的函数(即外部函数)的参数和变量。
函数声明语句它们作为顶级语句。它们可以出现在全局代码里,或者内嵌在其他函数中
函数定义表达式可以出现在JavaScript代码的任何地方。
2. 函数调用
有4种方式来调用JavaScript函数:
- 作为函数
- 作为方法
- 作为构造函数
- 通过它们的call()和apply()方法间接调用
2.1 函数调用
使用调用表达式可以进行普通的函数调用也可进行方法调用
一个调用表达式由多个函数表达式组成
如果函数表达式是一个属性访问表达式,即该函数是一个对象的属性或数组中的一个元素,那么它就是一个方法调用表达式。
printprops({ x: 1 });
var total = distance(0, 0, 2, 1) + distance(2, 1, 3, 5);
var probability = factorial(5) / factorial(13);
对于普通的函数调用,函数的返回值成为调用表达式的值。如果该函数返回是因为解释器到达结尾,返回值就是undefined。如果函数返回是因为解释器执行到一条return语句,返回值就是return之后的表达式的值,如果return语句没有值,则返回undefined。
函数调用中,调用上下文(this的值)是全局对象。然而,在严格模式下,调用上下文则是undefined。
2.2 方法调用
一个方法无非是个保存在一个对象的属性里的JavaScript函数。
方法调用和函数调用有一个重要的区别,即:调用上下文。属性访问表达式由两部分组成:一个对象(本例中的o)和属性名称(m)。在像这样的方法调用表达式里,对象o成为调用上下文,函数体可以使用关键字this引用该对象。
var calculator = {
//对象直接量
operand1: 1,
operand2: 1,
add: function() {
//注意this关键字的用法,this指代当前对象
this.result = this.operand1 + this.operand2;
}
};
calculator.add(); //这个方法调用计算1+1的结果
calculator.result; //=>2
使用方括号(的属性访问表达式)也可以进行属性访问操作。
o["m"](x,y);//o.m(x,y)的另外一种写法
a[0](z)//同样是一个方法调用(这里假设a[0]是一个函数)
customer.surname.toUpperCase(); //调用customer.surname的方法
f().m(); //在f()调用结束后继续调用返回值中的方法m()
任何函数只要作为方法调用实际上都会传入一个隐式的实参——这个实参是一个对象,方法调用的母体就是这个对象。
当方法的返回值是一个对象,这个对象还可以再调用它的方法。这种方法调用序列中(通常称为“链”或者“级联”)每次的调用结果都是另外一个表达式的组成部分。
//找到所有的header,取得它们id的映射,转换为数组并对它们进行排序
$(":header").map(function(){return this.id}).get().sort();
当方法并不需要返回值时,最好直接返回this。如果在设计的API中一直采用这种方式(每个方法都返回this),使用API就可以进行“链式调用”
shape.setX(100).setY(100).setSize(50).setOutline("red").setFill("blue").draw();
需要注意的是,this是一个关键字,不是变量,也不是属性名。JavaScript的语法不允许给this赋值。
和变量不同,关键字this没有作用域的限制,嵌套的函数不会从调用它的函数中继承this。如果嵌套函数作为方法调用,其this的值指向调用它的对象。如果嵌套函数作为函数调用,其this值不是全局对象(非严格模式下)就是undefined(严格模式下)。
如果你想访问这个外部函数的this值,需要将this的值保存在一个变量里,这个变量和内部函数都同在一个作用域内。
2.3 构造函数调用
如果函数或者方法调用之前带有关键字new,它就构成构造函数调用
- 构造函数调用创建一个新的空对象,这个对象继承自构造函数的prototype属性。
- 构造函数试图初始化这个新创建的对象,并将这个新对象用做其调用上下文,因此构造函数可以使用this关键字来引用这个新创建的对象。
- 注意,尽管构造函数看起来像一个方法调用,它依然会使用这个新对象作为调用上下文。也就是说,在表达式new o.m()中,调用上下文并不是o,而是创建的新对象。
- 构造函数通常不使用return关键字,它们通常初始化新对象,当构造函数的函数体执行完毕时,它会显式返回。在这种情况下,构造函数调用表达式的计算结果就是这个新对象的值。
- 然而如果构造函数显式地使用return语句返回一个对象,那么调用表达式的值就是这个对象。
- 如果构造函数使用return语句但没有指定返回值,或者返回一个原始值,那么这时将忽略返回值,同时使用这个新对象作为调用结果。
2.4 间接调用
- JavaScript中的函数也是对象,函数对象也可以包含方法。其中的两个方法call()和apply()可以用来间接地调用函数。
- 两个方法都允许显式指定调用所需的this值,也就是说,任何函数可以作为任何对象的方法来调用,哪怕这个函数不是那个对象的方法。
- 两个方法都可以指定调用的实参。
call()方法使用它自有的实参列表作为函数的实参,apply()方法则要求以数组的形式传入参数。
3. 函数的实参和形参
JavaScript中的函数定义并未指定函数形参的类型,函数调用也未对传入的实参值做任何类型检查。JavaScript函数调用甚至不检查传入形参的个数。
3.1 可选形参
当调用函数的时候传入的实参比函数声明时指定的形参个数要少,剩下的形参都将设置为undefined值。 应当给省略的参数赋一个合理的默认值
//将对象o中可枚举的属性名追加至数组a中,并返回这个数组a
//如果省略a,则创建一个新数组并返回这个新数组
function getPropertyNames(o, /*optional*/ a) {
// if (a === undefined) a = []; //如果未定义,则使用新数组
a = a || [];
for (var property in o) a.push(property);
return a;
}
//这个函数调用可以传入1个或2个实参
var a = getPropertyNames(o); //将o的属性存储到一个新数组中
getPropertyNames(p, a); //将p的属性追加至数组a中
需要注意的是,当用这种可选实参来实现函数时,需要将可选实参放在实参列表的最后。调用函数是没办法省略第一个实参并传入第二个实参的,它必须将undefined作为第一个实参显式传入
3.2 可变长的参数列表:实参对象arguments
在函数体内,标识符arguments是指向实参对象的引用,实参对象是一个类数组对象,这样可以通过数字下标就能访问传入函数的实参值,而不用非要通过名字来得到实参。
和真正的数组一样,arguments也包含一个length属性,用以标识其所包含元素的个数。
function f(x, y, z) {
//首先,验证传入实参的个数是否正确
if (arguments.length != 3) {
throw new Error(
"function f called with" +
arguments.length +
"arguments,but it expects 3 arguments."
);
}
//再执行函数的其他逻辑...
}
实参对象有一个重要的用处,就是让函数可以操作任意数量的实参。
//遍历实参,查找并记住最大值
function max(/*...*/) {
var max = Number.NEGATIVE_INFINITY; //负无穷小
for (var i = 0; i < arguments.length; i++)
if (arguments[i] > max) max = arguments[i]; //返回最大值
return max;
}
var largest = max(1, 10, 100, 2, 3, 1000, 4, 5, 10000, 6); //=>10000
记住,arguments并不是真正的数组,它是一个实参对象。每个实参对象都包含以数字为索引的一组元素以及length属性
通过实参名字来修改实参值的话,通过arguments[]数组也可以获取到更改后的值
严格模式中的函数无法使用arguments作为形参名或局部变量名,也不能给arguments赋值。
3.2.1 callee属性
除了数组元素,实参对象还定义了callee属性,在非严格模式下,ECMAScript标准规范规定callee属性指代当前正在执行的函数。比如在匿名函数中通过callee来递归地调用自身。
3.3 将对象属性作函数实参
定义函数的时候,传入的实参都写入一个单独的对象之中,在调用的时候传入一个对象,对象中的名/值对是真正需要的实参数据。
//将原始数组的length元素复制至目标数组
//开始复制原始数组的from_start元素
//并且将其复制至目标数组的to_start中
//要记住实参的顺序并不容易
function arraycopy(
/*array*/ from,
/*index*/ from_start,
/*array*/ to,
/*index*/ to_start,
/*integer*/ length
) {
//逻辑代码
}
//这个版本的实现效率稍微有些低,但你不必再去记住实参的顺序
//并且from_start和to_start都默认为0
function easycopy(args) {
arraycopy(
args.from,
args.from_start || 0, //注意这里设置了默认值
args.to,
args.to_start || 0,
args.length
);
}
//来看如何调用easycopy()
var a = [1, 2, 3, 4], b = [];
easycopy({ from: a, to: b, length: 4 });
3.4 实参类型
下边的例子可以接收任意数量的实参,并可以递归地处理实参是数组的情况,这样的话,它就可以用做不定实参函数或者实参是数组的函数。
4 可以作为值的函数
在JavaScript中,函数不仅是一种语法,也是值,也就是说,可以将函数赋值给变量,存储在对象的属性或数组的元素中,作为参数传入另外一个函数等
除了可以将函数赋值给变量,同样可以将函数赋值给对象的属性。函数甚至不需要带名字,赋值给数组元素
var o = {
square: function(x) {
return x * x;
}
}; //对象直接量
var y = o.square(16); //y等于256
var a = [
function(x) {
return x * x;
},
20
]; //数组直接量
a[0](a[1]); //=>400
//在这里定义一些简单的函数
function add(x, y) {
return x + y;
}
function subtract(x, y) {
return x - y;
}
function multiply(x, y) {
return x * y;
}
function divide(x, y) {
return x / y;
}
//这里的函数以上面的某个函数作为参数
//并给它传入两个操作数然后调用它
function operate(operator, operand1, operand2) {
return operator(operand1, operand2);
}
//这行代码所示的函数调用实际上计算了(2+3)+(4*5)的值
var i = operate(add, operate(add, 2, 3), operate(multiply, 4, 5)); //我们为这个例子重复实现一个简单的函数
//这次实现使用函数直接量,这些函数直接量定义在一个对象直接量中
var operators = {
add: function(x, y) {
return x + y;
},
subtract: function(x, y) {
return x - y;
},
multiply: function(x, y) {
return x * y;
},
divide: function(x, y) {
return x / y;
},
pow: Math.pow //使用预定义的函数
};
//这个函数接收一个名字作为运算符,在对象中查找这个运算符
//然后将它作用于所提供的操作数
//注意这里调用运算符函数的语法
function operate2(operation, operand1, operand2) {
if (typeof operators[operation] === "function")
return operators[operation](operand1, operand2);
else throw "unknown operator";
}
//这样来计算("hello"+""+"world")的值
var j = operate2("add", "hello", operate2("add", "", "world"));
//使用预定义的函数Math.pow()
var k = operate2("pow", 10, 2);
4.1 自定义函数属性
JavaScript中的函数是一种特殊的对象,也就是说,函数可以拥有属性。当函数需要一个“静态”变量来在调用时保持某个值不变,最方便的方式就是给函数定义属性
5 作为命名空间的函数
我们常常简单地定义一个函数用做临时的命名空间,在这个命名空间内定义的变量都不会污染到全局命名空间。
function之前的左圆括号是必需的,因为如果不写这个左圆括号,JavaScript解释器会试图将关键字function解析为函数声明语句。
下面这个匿名函数命名空间用来隐藏一组属性名。
6 闭包
JavaScript也采用词法作用域(lexical scoping),也就是说,函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。
函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为“闭包”
当一个函数嵌套了另外一个函数,外部函数将嵌套的函数对象作为返回值返回的时候
我们将上边函数内的一对圆括号移动到了checkscope()之后。checkscope()现在仅仅返回函数内嵌套的一个函数对象,而不是直接返回结果。
JavaScript函数的执行用到了作用域链,这个作用域链是函数定义的时候创建的。嵌套的函数f()定义在这个作用域链里,其中的变量scope一定是局部变量,不管在何时何地执行函数f(),这种绑定在执行f()时依然有效。因此最后一行代码返回"local scope",而不是"global scope"。
闭包的这个特性强大到让人吃惊:它们可以捕捉到局部变量(和参数)并一直保存下来,看起来像这些变量绑定到了在其中定义它们的外部函数。
函数定义时的作用域链到函数执行时依然有效。
- 我们将作用域链描述为一个对象列表,不是绑定的栈。每次调用JavaScript函数的时候,都会为之创建一个新的对象用来保存局部变量,把这个对象添加至作用域链中。
- 当函数返回的时候,就从作用域链中将这个绑定变量的对象删除。
- 但是如果这个函数定义了嵌套的函数,并将它作为返回值返回或者存储在某处的属性里,这时就会有一个外部引用指向这个嵌套的函数。它就不会被当做垃圾回收,并且它所指向的变量绑定对象也不会被当做垃圾回收
var uniqueInteger = (function() {
//定义函数并立即调用
var counter = 0; //函数的私有状态
return function() {
return counter++;
};
})();
这是一个嵌套的函数,我们将它赋值给变量uniqueInteger,嵌套的函数是可以访问作用域内的变量的,而且可以访问外部函数中定义的counter变量。当外部函数返回之后,其他任何代码都无法访问counter变量,只有内部的函数才能访问到它。
像counter一样的私有变量不是只能用在一个单独的闭包内,在同一个外部函数内定义的多个嵌套函数也可以访问它,这多个嵌套函数都共享一个作用域链
function counter() {
var n = 0;
return {
count: function() {
return n++;
},
reset: function() {
n = 0;
}
};
}
var c = counter(), d = counter(); //创建两个计数器
c.count(); //=>0
d.count(); //=>0:它们互不干扰
c.reset(); //reset()和count()方法共享状态
c.count(); //=>0:因为我们重置了c
d.count(); //=>1:而没有重置d
这两个方法都可以访问私有变量n。再者,每次调用counter()都会创建一个新的作用域链和一个新的私有变量。因此,如果调用counter()两次,则会得到两个计数器对象,而且彼此包含不同的私有变量,调用其中一个计数器对象的count()或reset()不会影响到另外一个对象。
从技术角度看,其实可以将这个闭包合并为属性存取器方法getter和setter。
function counter(n) {
//函数参数n是一个私有变量
return {
//属性getter方法返回并给私有计数器var递增1
get count() {
return n++;
}, //属性setter不允许n递减
set count(m) {
if (m >= n) n = m;
else throw Error("count can only be set to a larger value");
}
};
}
var c = counter(1000);
c.count; //=>1000
c.count; //=>1001
c.count = 2000;
c.count; //=>2000
c.count = 2000; //=>Error!
下面addPrivateProperty()函数定义了一个私有变量,以及两个嵌套的函数用来获取和设置这个私有变量的值。
// 这个函数给对象o增加了属性存取器方法
// 方法名称为get<name>和set<name>。如果提供了一个判定函数
// setter方法就会用它来检测参数的合法性,然后在存储它
// 如果判定函数返回false,setter方法抛出一个异常
// 这个函数有一个非同寻常之处,就是getter和setter函数
// 所操作的属性值并没有存储在对象o中
// 相反,这个值仅仅是保存在函数中的局部变量中
// getter和setter方法同样是局部函数,因此可以访问这个局部变量
// 也就是说,对于两个存取器方法来说这个变量是私有的
// 没有办法绕过存取器方法来设置或修改这个值
function addPrivateProperty(o, name, predicate) {
var value; //这是一个属性值
//getter方法简单地将其返回
o["get" + name] = function() {
return value;
}; //setter方法首先检查值是否合法,若不合法就抛出异常
//否则就将其存储起来
o["set" + name] = function(v) {
if (predicate && !predicate(v))
throw Error("set" + name + ":invalid value" + v);
else value = v;
};
}
//下面的代码展示了addPrivateProperty()方法
var o = {}; //设置一个空对象
//增加属性存取器方法getName()和setName()
//确保只允许字符串值
addPrivateProperty(o, "Name", function(x) {
return typeof x == "string";
});
o.setName("Frank"); //设置属性值
console.log(o.getName()); //得到属性值
o.setName(0); //试图设置一个错误类型的值
利用循环创建很多个闭包:
//这个函数返回一个总是返回v的函数
function constfunc(v) {
return function() {
return v;
};
} //创建一个数组用来存储常数函数
var funcs = [];
for (var i = 0; i < 10; i++) funcs[i] = constfunc(i); //在第5个位置的元素所表示的函数返回值为5
funcs[5](); //=>5
上面这段代码创建了10个闭包,并将它们存储到一个数组中。这些闭包都是在同一个函数调用中定义的,因此它们可以共享变量i。当constfuncs()返回时,变量i的值是10,所有的闭包都共享这一个值,因此,数组中的函数的返回值都是同一个值
嵌套的函数不会将作用域内的私有成员复制一份,也不会对所绑定的变量生成静态快照(static snapshot)
- 书写闭包的时候还需注意,this是JavaScript的关键字,而不是变量。每个函数调用都包含一个this值,如果闭包在外部函数里是无法访问this的,除非外部函数将this转存为一个变量
- arguments并不是一个关键字,但在调用每个函数时都会自动声明它,由于闭包具有自己所绑定的arguments,因此闭包内无法直接访问外部函数的参数数组,除非外部函数将参数数组保存到另外一个变量中
var self=this;//将this保存至一个变量中,以便嵌套的函数能够访问它
var outerArguments=arguments;//保存起来以便嵌套的函数能使用它
7. 函数属性、方法和构造函数
因为函数也是对象,它们也可以拥有属性和方法,就像普通的对象可以拥有属性和方法一样。甚至可以用Function()构造函数来创建新的函数对象。
7.1 函数ength属性
在函数体里,arguments.length表示传入函数的实参的个数。而函数本身的length属性(arguments.callee.length)则是只读属性,它代表在函数定义时给出的实参个数,通常也是在函数调用时期望传入函数的实参个数。
7.2 函数prototype属性
每一个函数都包含一个prototype属性,这个属性是指向一个对象的引用,这个对象称做“原型对象”(prototype object)。每一个函数都包含不同的原型对象。当将函数用做构造函数的时候,新创建的对象会从原型对象上继承属性。
7.3 函数call()方法和apply()方法
我们可以将call()和apply()看做是某个对象的方法,通过调用方法的形式来间接调用函数。
call()和apply()的第一个实参是要调用函数的母对象,它是调用上下文,在函数体内通过this来获得对它的引用。要想以对象o的方法来调用函数f(),可以这样使用call()和apply():
f.call(o);
f.apply(o);
①对于call()来说,第一个调用上下文实参之后的所有实参就是要传入待调用函数的值。比如,以对象o的方法的形式调用函数f(),并传入两个参数,
②apply()方法和call()类似,但传入实参的形式和call()有所不同,它的实参都放入一个数组当中:
f.call(o,1,2);
f.apply(o,[1,2]);
可以将当前函数的arguments数组直接传入(另一个函数的)apply()来调用另一个函数,参照如下代码:
7.4 函数bind()方法
bind()方法将函数绑定至某个对象。当在函数f()上调用bind()方法并传入一个对象o作为参数,这个方法将返回一个新的函数。(以函数调用的方式)调用新的函数将会把原始的函数f()当做o的方法来调用。传入新函数的任何实参都将传入原始函数
bind()不仅仅是将函数绑定至一个对象。除了第一个实参之外,传入bind()的实参也会绑定至this
真正的bind()方法返回一个函数对象,bind()方法可以顺带用做构造函数。如果bind()返回的函数用做构造函数,将忽略传入bind()的t his,原始函数就会以构造函数的形式调用,其实参也已经绑定
7.5 函数toString()方法
- 函数也有toString()方法,ECMAScript规范规定这个方法返回一个字符串,这个字符串和函数声明语句的语法相关。
- 实际上,大多数(非全部)的toString()方法的实现都返回函数的完整源码。
- 内置函数往往返回一个类似"[native code]"的字符串作为函数体。
7.6 Function构造函数
函数还可以通过Function()构造函数来定义,比如:
var f = new Function("x", "y", "x=x*2;return x*y;");
f(3, 6);
就像函数直接量一样,Function()构造函数创建一个匿名函数。
最后一个实参所表示的文本就是函数体;它可以包含任意的JavaScript语句,每两条语句之间用分号分隔。
7.7 可调用对象RegExp
可调用的对象”(callable object)是一个对象,可以在函数调用表达式中调用这个对象。
一个常见的可调用对象是RegExp对象
8 函数式编程
8.1 使用函数处理数组
假设我们想要计算数组元素的平均值和标准差。
//首先定义两个简单的函数
var sum = function(x, y) {
return x + y;
};
var square = function(x) {
return x * x;
}; //然后将这些函数和数组方法配合使用计算出平均数和标准差
var data = [1, 1, 3, 5, 5];
var mean = data.reduce(sum) / data.length;
var deviations = data.map(function(x) {
return x - mean;
});
var stddev = Math.sqrt(deviations.map(square).reduce(sum) / (data.length - 1));
我们可以自定义map()和reduce()函数:
// 对于每个数组元素调用函数f(),并返回一个结果数组
// 如果Array.prototype.map定义了的话,就使用这个方法
var map = Array.prototype.map? function(a, f) {
return a.map(f);
} //如果已经存在map()方法,就直接使用它
: function(a, f) {
//否则,自己实现一个
var results = [];
for (var i = 0, len = a.length; i < len; i++) {
if (i in a) results[i] = f.call(null, a[i], i, a);
}
return results;
};
//使用函数f()和可选的初始值将数组a减至一个值
//如果Array.prototype.reduce存在的话,就使用这个方法
var reduce = Array.prototype.reduce
? function(a, f, initial) {
//如果reduce()方法存在的话
if (arguments.length > 2) return a.reduce(f, initial);
//如果传入了一个初始值
else return a.reduce(f); //否则没有初始值
}
: function(a, f, initial) {
//这个算法来自ES5规范
var i = 0,
len = a.length,
accumulator; //以特定的初始值开始,否则第一个值取自a
if (arguments.length > 2) accumulator = initial;
else {
//找到数组中第一个已定义的索引
if (len == 0) throw TypeError();
while (i < len) {
if (i in a) {
accumulator = a[i++];
break;
} else i++;
}
if (i == len) throw TypeError();
}
//对于数组中剩下的元素依次调用f()
while (i < len) {
if (i in a) accumulator = f.call(undefined, accumulator, a[i], i, a);
i++;
}
return accumulator;
};
8.2 高阶函数
所谓高阶函数(higher-order function)就是操作函数的函数,它接收一个或多个函数作为参数,并返回一个新函数
//这个高阶函数返回一个新的函数,这个新函数将它的实参传入f()
//并返回f的返回值的逻辑非
function not(f) {
return function() {
//返回一个新的函数
var result = f.apply(this, arguments); //调用f()
return !result; //对结果求反
};
}
var even = function(x) {
//判断a是否为偶数的函数
return x % 2 === 0;
};
var odd = not(even); //一个新函数,所做的事情和even()相反
[1, 1, 3, 5, 5].every(odd); //=>true:每个元素都是奇数
//所返回的函数的参数应当是一个实参数组,并对每个数组元素执行函数f()
//并返回所有计算结果组成的数组
//可以对比一下这个函数和上文提到的map()函数
function mapper(f) {
return function(a) {
return map(a, f);
};
}
var increment = function(x) {
return x + 1;
};
var incrementer = mapper(increment);
incrementer([1, 2, 3]); //=>[2,3,4]
//返回一个新的可以计算f(g(...))的函数
//返回的函数h()将它所有的实参传入g(),然后将g()的返回值传入f()
//调用f()和g()时的this值和调用h()时的this值是同一个this
function compose(f, g) {
return function() {
//需要给f()传入一个参数,所以使用f()的call()方法
//需要给g()传入很多参数,所以使用g()的apply()方法
return f.call(this, g.apply(this, arguments));
};
}
var square = function(x) {
return x * x;
};
var sum = function(x, y) {
return x + y;
};
var squareofsum = compose(square, sum);
squareofsum(2, 3); //=>25
8.3 不完全函数
把一次完整的函数调用拆成多次函数调用,每次传入的实参都是完整实参的一部分,每个拆分开的函数叫做不完全函数(partial function)
传入bind()的实参都是放在传入原始函数的实参列表开始的位置,但有时我们期望将传入bind()的实参放在(完整实参列表的)右侧:
//实现一个工具函数将类数组对象(或对象)转换为真正的数组
//在后面的示例代码中用到了这个方法将arguments对象转换为真正的数组
function array(a, n) {
return Array.prototype.slice.call(a, n || 0);
} //这个函数的实参传递至左侧
function partialLeft(f /*,...*/) {
var args = arguments; //保存外部的实参数组
return function() {
//并返回这个函数
var a = array(args, 1); //开始处理外部的第1个args
a = a.concat(array(arguments)); //然后增加所有的内部实参
return f.apply(this, a); //然后基于这个实参列表调用f()
};
}
//这个函数的实参传递至右侧
function partialRight(f /*,...*/) {
var args = arguments; //保存外部实参数组
return function() {
//返回这个函数
var a = array(arguments); //从内部参数开始
a = a.concat(array(args, 1)); //然后从外部第1个args开始添加
return f.apply(this, a); //最后基于这个实参列表调用f()
};
}
//这个函数的实参被用做模板
//实参列表中的undefined值都被填充
function partial(f /*,...*/) {
var args = arguments; //保存外部实参数组
return function() {
var a = array(args, 1); //从外部args开始
var i = 0,
j = 0; //遍历args,从内部实参填充undefined值
for (; i < a.length; i++) if (a[i] === undefined) a[i] = arguments[j++]; //现在将剩下的内部实参都追加进去
a = a.concat(array(arguments, j));
return f.apply(this, a);
};
}
//这个函数带有三个实参
var f = function(x, y, z) {
return x * (y - z);
}; //注意这三个不完全调用之间的区别
partialLeft(f, 2)(3, 4); //=>-2:绑定第一个实参:2*(3-4)
partialRight(f, 2)(3, 4); //=>6:绑定最后一个实参:3*(4-2)
partial(f, undefined, 2)(3, 4); //=>-6:绑定中间的实参:3*(2-4)
8.4 函数记忆
将上次的计算结果缓存起来。在函数式编程当中,这种缓存技巧叫做“记忆”(memorization)
//返回f()的带有记忆功能的版本
//只有当f()的实参的字符串表示都不相同时它才会工作
function memorize(f) {
var cache = {}; //将值保存在闭包内
return function() {
//将实参转换为字符串形式,并将其用做缓存的键
var key = arguments.length + Array.prototype.join.call(arguments, ",");
if (key in cache) return cache[key];
else return cache[key] = f.apply(this, arguments);
};
}
- memorize()函数创建一个新的对象,这个对象被当做缓存(的宿主)并赋值给一个局部变量,因此对于返回的函数来说它是私有的(在闭包中)。
- 所返回的函数将它的实参数组转换成字符串,并将字符串用做缓存对象的属性名。
- 如果在缓存中存在这个值,则直接返回它。
- 否则,就调用既定的函数对实参进行计算,将计算结果缓存起来并返回