函数
JavaScript中,函数指只定义一次,但可以多次被多次执行或调用的一段JavaScript代码。与数组类似,JavaScript中函数是特殊的对象,拥有自身属性和方法
每个函数对象都有
prototype
和length
属性,bind
、apply()
、call()
方法。函数的特殊性在于:可以通过函数调用执行函数体中的语句。函数是对象,所以可以赋值给变量、作为参数传递进其他函数、挂载到对象上作为方法
1 函数定义
函数定义总共有三种方法:函数定义表达式、函数声明语句和new Function()
。
但是
new Function()
使用很少,因为通过它创建的函数不使用词法作用域,创建的函数都在全局作用域被调用。-
函数定义表达式和函数声明语句都利用关键字
function
来定义函数// 函数声明语句 function funcName([arg1 [, arg2] [..., argn]]) { statements } //函数定义表达式 var funcName = function([arg1 [, arg2] [..., argn]]) { statements }
函数名标识符
funcName
:引用新定义的函数对象参数列表:函数中的参数与函数体中的局部变量相同,
function(x)
相当于在函数体中var x;
{ statments }
:构成函数体的语句,调用函数后执行的语句
1.1 变量提升
JavaScript中由var
关键字声明的变量存在变量提升:将变量声明提升到作用域的顶部,但赋值仍保留在原处。所以函数声明语句和函数定义表达式有本质的区别
函数声明语句:将函数声明和函数的赋值都提升到作用域的顶部,在同一个作用域中可以出现调用在函数定义之前;
ECMAScript允许函数声明语句作为顶级语句,可以出现在全局作用域中、也可以出现在嵌套函数中,但不能出现在循环、判断、
try-catch-finally
和with
语句中。函数定义表达式没有限制
-
函数定义表达式:与
var
声明的普通变量相同,只是将变量声明提升到作用域顶部,但赋值仍然保留在原处,不能在定义前使用//没有显式指明返回值的函数,默认返回undefined //输出对象o的每个属性的名称 function printPrps(o) { for(var prop in o) { console.log(prop + ": " + o[prop] + "\n"); } } //计算笛卡尔坐标系中两点间的距离 function distance(x1, y1, x2, y2) { var dx = x2 - x1; var dy = y2 - y1; return Math.sqrt(dx * dx + dy * dy); } // 计算阶乘的递归函数,x!是x到1间(步长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;} return x * fact(x -1); }; // 函数表达式可以作为参数传递给其他函数 data.sort(function(a, b) {return a - b;}); //定义后立即调用函数表达式 var tensquare = (function(x) {return x * x;})(10);
1.2 嵌套函数
JavaScript中,函数可以嵌套在其他函数中。内部函数可以访问外部函数的局部变量和参数。
// 内部函数square可以访问到外部函数的参数a、b和局部变量c
function hypotenuse(a, b) {
var c = 10;
function square(x) {return x * x;}
return Math.sqrt(square(a) + square(b) + square(c));
}
2 函数的调用
在定义函数时,函数体中的代码不会执行,只有在调用函数时,才执行函数体中的语句。有四种方式可以调用函数:
作为普通函数
作为对象的方法
作为构造器函数
使用函数的
call()
和apply()
方法间接调用
2.1 调用函数
使用调用表达式来调用普通函数,每个调用表达式由多个函数表达式组成。每个函数表达式包括函数对象、括号和传入的实参组成。
每次调用会拥有本次调用的上下文
this
;在ES5非严格模式下,普通函数的this
值是全局对象;在严格模式下是undefined
以函数形式调用的函数通常不使用
this
关键字如果函数没有显式
return
语句返回一个值,默认返回undefined
-
传入的实参是由逗号分隔的0个或多个函数表达式
// 调用printProps()函数,传入对象作为实参即可 printPrps({x: 1}); // 调用distance()函数 var total = distance(0, 0, 2, 1) + distance(2, 1, 3, 5); // 调用factorial()函数 var probability = factorial(5) / factorial(13);
2.2 方法调用
方法是保存在JavaScript对象属性中的函数。
对方法调用的参数和返回值处理与函数调用相同
方法调用由两个部分组成:
对象.属性名()
,其中属性名是值为函数的属性-
方法调用中:
调用上下文
指调用方法的对象,使用this
关键字引用printProps({x: 1}); var total = distance(0, 0, 2, 1) + distance(2, 1, 3, 5); var probability = factorial(5) / factorial(13); var calculator = { //对象字面量 operand1: 1, operand2: 2, add: function() { //用this关键字指代当前对象calculator this.result = this.operand1 + this.operand2; } }; calculator.add(); //调用其add方法,使calculator对象获得result属性 calculator.result; // ==> 3
方法和this
关键字是面向对象的核心,任何函数作为方法调用时会传入一个隐式实参(指代调用方法的对象this
),基于this
的方法可以执行多种操作。
this
是一个关键字,不是变量名、属性名,JavaScript不允许为this
赋值,但是可以将其赋值给其他变量-
this
没有作用域限制,但是嵌套的函数不会从调用它的函数中继承this
嵌套函数如果作为方法调用,
this
的值指向调用它的对象;嵌套函数如果作为函数调用,
this
不是全局变量(ES5非严格模式),就是undefined
(ES5严格模式)嵌套函数的
this
并不指向调用它的外层函数的上下文
-
在外层函数中使用变量将外层函数的
this
对象及arguments
属性保存下来,在嵌套函数中便可以访问var o = { m: function() { var self = this; // 保存this(指向o对象)在变量self中 console.log(this === o); // ==> true,this指向o对象 f(); //将f()作为函数调用 function f() { console.log(this); // ==> window严格模式下,嵌套函数作为函数来调用,其this是undefined;非严格模式下是全局对象 console.log(this === o); //false,此处的this指向全局对象或undefined console.log(self === o); //true,self指向外部函数的this值 } } }; o.m(); //调用对象o的方法m()
2.3 构造函数调用
如果函数或方法调用前有关键字new
,函数或者方法便作为构造函数来调用。构造函数会创建一个新对象,新对象继承构造函数的prototype
属性。
作为构造器函数的调用,会将新创建的对象作为其调用上下文(
this
指向新创建的对象),在构造器函数中使用this
引用新创建的对象。
2.4 间接调用call()
和apply()
函数是对象,每个函数都有call()
和apply()
两个方法,作用是改变函数运行时的上下文context
--改变函数体内部this
的指向
,因为JavaScript中有函数定义时上下文
、函数运行时上下文
和函数中上下文可以改变
的概念。
call()
和apply()
作用都是动态改变函数体内this
指向,只是接受参数形式不太一样。call()
需要将参数按顺序传递进函数,并且知道参数的数量(参数数量确定时使用)apply()
将参数放在数组中传进函数(参数数量不确定时使用)
call()
和apply()
存在的意义
在JavaScriptOOP
中,使用原型实现继承,call()
和apply()
是用于不同对象间的方法复用。当一个object
没有某个方法,但是另一个objAnother
对象有,可以借助call()
和apply()
使object
可以操作objAnother
对象的方法。
function Cat() {}
function Dog() {}
Cat.prototype = {
food: "fish",
say: function () {
console.log("I love " + this.food);
}
};
Dog.prototype = {food: "bone"};
var bCat = new Cat();
var bDog = new Dog();
bCat.say(); // ==> "I love fish"
bCat.say.call(bDog); //==>"I love bone",bDog对象使用bCat对象的say方法,输出自身的`this.food`属性
3 函数的实参和形参
实参和形参是相对的概念,在函数定义时指定的参数叫做形参;在函数调用时传入的参数叫做实参。对于需要省略的实参,可以使用null
或undefined`作为占位符。
3.1 参数默认值
如果调用函数时,传入的实参个数arguments.length
小于定义时形参的个数arguments.callee.length
,剩余的形参都被设置为undefined
。对可以省略的值应该赋一个合理的默认值。
// 将对象obj中可枚举的自身属性追加到数组a中,并返回数组a
// 如果省略a,则创建一个新数组,并返回这个新数组
function getPropertyNames(obj, /*optional*/ a) {
if(!a) { a = []; } //如果未传入a,则使用新数组。
// a = a || [];代替写法更有语义
for(var prop in obj) {
if(!obj.hasOwnProperty(prop)) {continue;}
a.push(prop);
}
return a;
}
// 调用,出入一个参数或两个参数
var a = getPropertyNames(obj); //将obj的属性存储到一个新数组中
getPropertyNames(obj, arr); //将obj的属性追加到arr数组中
函数中的参数等同于函数体内的局部变量,具有函数的作用域。
3.2 参数对象
函数体内,标识符arguments
指向实参对象的引用,实参对象是一个类数组对象,可以通过下标访问每个传入的参数。
arguments
仅是一个标识符,严格模式下不能赋值;-
应用场景:函数包含固定个数的必须参数,随后包含不定数量的可选参数
// 可以接收任意个数的实参, // 接收任意数量的实参,返回传入实参的最大值,内置的Math.max()方法功能类似 function max(/*...optional*/) { //实参个数不能为0 var maxNum = Number.NEGATIVE_INFINITY; //将保存最大值的变量初始化 for(var i in arguments) { maxNum = (arguments[i] > maxNum) ? arguments[i] : maxNum; } return maxNum; }
3.3 callee
和caller
属性
callee
是ECMAScript规范中arguments
对象的属性:代表当前正在执行的函数。caller
是非标准的,只是浏览器基本都实现了这个属性:带表调用当前函数的函数。-
在严格模式中,对这两个属性读写都会产生错误
// arguments的callee属性用在匿名函数的递归实现 var factorial = function(x) { if(x <= 1) {return 1;} return x * arguments.callee(x - 1); }
3.4 将对象属性作为参数
在定义一个函数时,如果传入的参数多于3个,在调用时按顺序传入会变得很麻烦。一种解决方式是传入key/value
形式的参数,无需关注参数的顺序。
在定义函数时,形参指定为一个对象;
-
调用函数时,将整个对象传入函数,无需关心每个属性的顺序。(性能会差,参数需要在对象中去查找值)
// 将原始数组的length复制到目标数组 // 开始复制原始数组的from_start元素 // 并且将其复制至目标数组的to_start中 // 参数复杂,调用时顺序难以控制 function arrayCopy(array, from_start, target_arr, to_start, length) { // (原始数组, index, 目标数组, index, length) { // 实现逻辑 } } // 无需关心参数顺序的版本,效率略低 // from_start和to_start默认为0 function easyCopy(args) { arrayCopy(args.array, args.from_start || 0, args.target_arr, args.to_start || 0, args.length); } // easyCopy()的调用 var a = [1, 2, 3, 4]; var b = []; easyCopy({array: a, target_arr: b, length: 4});
3.5 实参类型
JavaScript在定义函数时并未声明形参类型,形参整体传入函数体前不会做类型检查,如果对传入的实参有某种限制,最好在函数体内增加类型检查的代码。
// 返回数组或类数组a中元素的累加和
// 数组a中的元素必须是数字,null和undefined被忽略
// 类型检查严格,但是灵活性很差
function sum(a) {
if(isArrayLike(a)) { // a是数组或类数组
var result = 0;
for(var i in a) {
var element = a[i];
if(element == null) {continue;} //跳过null和undefined
if(isFinite(element)) {
result += element;
} else {
throw new Error("sum(): elements must be finite number");
}
}
return result;
} else {
throw new Error("sum(): arguments must be array-like");
}
}
4 函数作为值
函数定义及调用是JavaScript中的词法特性;同时JavaScript中函数是一个对象:
可以赋值给变量
存储在对象的属性中或者数组的元素中
-
作为参数传入另一个函数:例如
Array.sort()
方法,用来对数组元素进行排序。但是排序的规则有很多中,将具体规则封装在函数中,传入sort()
。函数实现对任意两个值都返回一个值,指定它们在排序好数组中的先后顺序// 简单函数 function add(x, y) {return x + y;} function subtract(x, y) {return x - y;} function mutiply(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, 2, 3) + operate(mutiply, 4, 5); // ==>25 // 另外一种实现 var operators = { add: function(x, y) {return x + y;}, subtrack: function(x, y) {return x + y;}, mutiply: function(x, y) {return x + y;}, divide: function(x, y) {return x + y;}, pow: Math.pow }; function operate2(operator, operand1, operand2) { if(typeof operators[operator] === 'function') { return operators[operator](operand1, operand2); } else { throw "unknown operator"; } } // 计算("hello" + " " + "world")的值 operate2("add", "hello", operate2("add", " ", "world")); // ==> "hello world" operate2("pow", 10, 2); // ==> 100
自定义属性
函数是对象,可以拥有属性。对于函数中的静态变量,可以直接存入函数的属性中。
// 初始化函数对象的计数器属性,函数声明会被提前,所以可以先给他的属性赋值
uniqueInteger.counter = 0;
// 每次调用这个函数,都会返回一个不同的整数,使用counter属性保存下次要返回的值
function uniqueInteger() {
return uniqueInteger.counter++; // 先返回计数器的值,再自增1
}
5 函数作为命名空间
JavaScript中只存在函数作用域和全局作用域,没有块级作用域。可以使用自执行函数用作临时命名空间,这样不会污染全局变量。
(function() {/* 模块代码 */})(); //注意调用括号的位置,两种写法均可
(function() {/* 模块代码 */} ());
6 闭包
编程界崇尚优雅简洁唯美,很多时候如果你觉得一个概念很复杂,那么可能是你理错了
闭包在JavaScript中,指内部函数总是可以访问其所在的外部函数中声明的变量和参数,即使外部函数被返回(调用结束)。
Closure使JavaScript使当前作用域能够访问到外部作用域中的变量;
函数是JavaScript中唯一拥有自身作用域的结构,所以Closure的创建依赖于函数
6.1 如何理解
var scope = "global scope";
function checkScope() {
var scope = "local scope";
function f() {return scope;}
return f; //将函数对象返回
}
checkScope()(); // ==> "local scope"
-
在JavaScript中,每个函数在定义时会创建一个与之相关的作用域链,并且在程序执行期间一直存在
外部函数
checkScope
有自身的作用域链,内部函数f
有自身单独的的作用域链)
-
每次调用函数会创建一个新对象来保存参数和局部变量,并将其添加到作用域链。
当函数返回时,将绑定的新对象从作用域链上删除。如果没有其他变量引用该对象、或该对象没有保存在某个对象的属性中,它会被当做垃圾回收。
如果没有外部变量引用
checkScope
调用函数时创建的临时对象,函数return
后便被垃圾回收
-
如果
checkScope
定义有嵌套函数f
,并将f
作为返回值或保存在某个对象的属性中。相当于有一个外部引用指向嵌套函数。f
有自身的作用域链和保存参数与局部变量的对象f
在checkScope
函数体内,可以访问外部函数中所有的变量和参数
综上所述:JavaScript中的函数,通过作用域链和词法作用域两者的特性,将该函数定义时的所处的作用域中的相关函数进行捕获和保存,从而可以在完全不同的上下文中进行引用
6.2 注意点
每个函数调用都有一个
this
值和arguments
对象,需要在外部函数中用变量保存this
值和arguments
对象,Closure才可以访问到外部函数的这两个值。that = this
,outerArguments = arguments
-
Closure是通过调用外部函数返回内部嵌套函数创建的,每次调用外部函数都会创建一个
Closure
。但是每个Closure共享外部函数声明的变量,不会为每个Closure单独创建一份外部作用域的副本// 函数返回一个返回v的函数 function constFunc(v) { return function() {return v;}; } //创建一个数组用来保存常数 var funcs = []; for(var i=0; i<10; i++) { funcs[i] = constFunc(i); // 创建了10个Closure,每个Closure的值不同,因为每次传入外层函数constFunc的值不同 } console.log(funcs[6]()); // ==> 6 function constFuncs() { var funcs = []; for(var i=0; i<10; i++) { funcs[i] = function() {return i;}; // 创建10个Closure,但10个Closure在同一个外层函数constFuncs内,共享它的局部变量。 } // 10个Closure创建完毕后,i的值变为0,所以每个Closure返回的值都是0 return funcs; } var foo = constFuncs(); console.log(foo[4]()); // ==> 10
CLosure中部分资源不能自动释放,容易造成内存泄漏
内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存(即不再利用的值或对象依然占据内存空间)
7 函数的属性、方法和构造函数
JavaScript中函数是对象,每个函数都有lenght
和prototype
属性;每个函数都有call()
、apply()
、bind()
方法,并且可以利用函数的构造函数Function()
来创建函对象。
7.1 length
属性
函数对象的length
属性是只读的,用于获取定义函数时指定的形参个数。可以用来检验定义的参数与传入的参数是否相同。
// arguments.callee不能在严格模式下工作
function check(args) {
var actual = args.length;
var expected = args.callee.length; // arguments.callee指代函数本身
if(expected !== actual) {
throw Error("Expected:" + expected + " args; got " + actual + "args;");
}
}
// 测试函数,只有传入三个函数才不会报错
function f(x, y, z) {
check(arguments);
return x + y + z;
}
7.2 prototype
属性
每个函数都有一个prototype
属性,指向一个原型对象的引用,每个函数的原型对象都不同。
将函数用作创建对象的构造器函数使用时,新创建的对象会从函数的原型对象上继承属性
7.3 call()
和apply()
call()
和apply()
用于动态改变this
的指向,使对象的方法可以借用给别的对象使用。
7.4 bind()
bind()
方法的作用是将函数绑定至某个对象,bind()
方法的返回值是一个新的函数对象
-
将
f()
函数调用bind()
方法绑定至对象o
,用变量g
来接收bind()
返回的函数,(以函数调用形式)调用g
时,会将原始函数f
当做对象o
的方法来使用。var f = function(y) {return this.x + y;}; var o = {x: 2}; var g = f.bind(o); // 将f()绑定到o对象上 console.log(g(6)); // ==> 以函数调用的方式调用g(x),相当于调用o.f(x) // 实现bind()绑定 function bind(f, o) { if(f.bind) { //如果bind()方法存在,使用bind()方法 return f.bind(o); } else { return function() { //利用apply()使o对象来调用f()方法,并且传入类数组对象参数arguments return f.apply(o, arguments); //arguments是调用绑定函数时传入的参数 }; } }
-
bind()
第一个实参是要绑定方法的对象(本质是将函数的this
指向改为传入的对象),同时后面的实参也会绑定至this
,函数式编程中的currying
柯里化。var sum = function(x, y) {return x + y;}; // 创建一个类似sum的新函数,但是this绑定到null // 并且第一个参数绑定为1,新的函数只期望传入一个参数 var g = sum.bind(null, 1); // 将sum的第一个参数x绑定为1 console.log(g(3)); // ==> 4,因为x绑定为1,将3作为参数传入y function f(y, z) {return this.x + y + z;} var g = f.bind({x: 2}, 3); // 将f函数绑定到对象{x: 2},将3绑定到函数的第一个参数y,新创建的函数传入一个参数 console.log(g(1)); // ==>6
-
模拟实现
bind()
方法:bind()
方法返回的是一个Closureif(!Function.prototype.bind) { //不支持bind方法 Function.prototype.bind = function (o) { var self = this; // 保存bind()中的this与arguments,便于在嵌套函数中使用 var boundArgs = arguments; // bind()方法返回一个函数对象 return function() { // 创建一个实参列表,将传入bind()的第二个及以后的实参都传入这个函数 var args = []; // 传入bind()函数的参数处理,从第二位开始 for(var i=1; i<boundArgs.length; i++) {args.push(boundArgs[i]);} // 将调用新函数时传入的参数继续添加到args中 for (var j=0; j < arguments.length; j++) {args.push(arguments[j]);} // 将self作为o的方法来调用 return self.apply(o, args); }; }; }
注意点
bind()
方法的某些特性是上述模拟方法不能替代的。
-
bind()
方法返回一个真正的函数对象,函数对象的length
属性是绑定函数的形参个数减去绑定的实参个数(length
的值不能小于0
)function f(y, z) {return this.x + y + z;} // 绑定函数f的形参个数时2 var g = f.bind({x: 2}, 3); // 绑定的实参个数是1(从第二位开始是传入绑定函数的实参),即将3传递给f的第一个参数y g(1); // ==> 6,继续将1传递给函数f的形参z
ES5的
bind()
方法可以顺带做构造函数,此时将会忽略传入bind()
方法的this
,原始函数以构造函数的形式调用,其实参已经绑定。-
bind()
方法返回的函数并不包含prototype
属性(普通函数的固有prototype
属性是不能删除的);并且将绑定的函数用作构造器函数时所创建的对象,从原始为绑定的构造器函数中继承prototype
如果将
g()
作为构造函数,其创建的对象与直接利用f
当做构造函数创建的对象原型是同一个prototype
7.5 toString()
方法
根据ECMAScript规范,函数的toString()
方法返回一个字符串,字符串与函数声明语句的语法有关。
大多数函数的
toString()
方法都返回函数的完整源码内置函数的
toString()
方法返回一个类似"[native code]"
的字符串作为函数体
7.6 Function()
构造函数
Function()
构造函数运行JavaScript在运行时动态创建并编译函数每次调用
Function()
构造函数都会解析函数体,并创建新的函数对象。如果在循环中执行Function()
,会影响效率;Function()
创建的函数不使用词法作用域,函数体的代码编译总在全局作用域执行
Function()
在实际编程中使用很少。
8 函数式编程
JavaScript并非函数式编程语言,但JavaScript中函数是对象,可以像对象一样操控,所以可以应用函数式编程技术
8.1 使用函数处理数组
假设有一个数组,元素都是数字,要计算所有元素的平均值与标准差。
-
非函数式编程风格
var data = [1, 1, 3, 5, 5]; var total = 0; //平均数是所有元素的和除以元素的个数 data.forEach(function(value) { total += value; }); var mean = total / data.length; //标准差:先计算每个元素与平均值的差的平方的和 var sum = 0; data.forEach(function(value) { var tmp = value - mean; sum += tmp * tmp; }); //标准差stddev var stddev = Math.sqrt(sum / data.length-1);
-
函数式编程风格,利用
map()
和reduce()
来实现,抽象出两个过程:求平均值和标准差会用到求一个数组中所有元素的和:使用
reduce()
-
求数组中每个元素的平方:使用
map()
// 定义求和、求积两个过程 var add = function(x, y) {return x + y;}; var square = function(x) {return x * x;}; var data = [1, 1, 3, 5, 5]; // reduc()实现数组求和 var avg = data.reduce(add) / data.length; // map()实现差的平方,返回操作后的数组,再调用reduce() var sum = data.map(function(value) {return value - avg;}); var stddev = Math.sqrt(sum.map(square).reduce(add) / (data.length - 1));
8.2 高阶函数
高阶函数higher-order function
指操作函数的函数,接收一个或多个函数作为参数,并返回一个新函数。
// 高阶函数not()返回一个新函数,新函数将它的实参传入f()
function not(f) {
return function() { // 返回一个新函数
var result = f.apply(this, arguments); // 调用f()
return !result; // 对结果求反
};
}
var even = function (x) { //判断一个数是否是偶数
return x % 2 === 0;
};
var odd = not(even); // 一个新函数,所做的事情与even()相反
[1, 1, 3, 5, 5].every(odd); // ==> true每个元素都是奇数
// mapper()返回的函数的参数是数组,对每个元素执行函数f()
// 返回所有计算结果组成的数组
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]);
8.3 不完全函数
将一次完整的函数调用拆分为多次函数调用,每次传入的实参都是完整实参的一部分,每个拆分开的函数叫做不完全函数partial function
,每次函数调用叫做不完全函数调用partial application
。特点是每次调用都返回一个函数,知道得到最终运行结果为止。
if(!Function.prototype.bind) { //不支持bind方法
Function.prototype.bind = function (o) {
var self = this; // 保存bind()中的this与arguments,便于在嵌套函数中使用
var boundArgs = arguments;
// bind()方法返回一个函数对象
return function() {
// 创建一个实参列表,将传入bind()的第二个及以后的实参都传入这个函数
var args = [];
// 传入bind()函数的参数处理,从第二位开始
for(var i=1; i<boundArgs.length; i++) {args.push(boundArgs[i]);}
// 将调用新函数时传入的参数继续添加到args中
for (var j=0; j < arguments.length; j++) {args.push(arguments[j]);}
// 将self作为o的方法来调用
return self.apply(o, args);
};
};
}
函数
f()
的bind()
方法返回一个新函数,给新函数传入特定的上下文和一组指定的参数,然后调用函数f()
。传入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); // 开始处理外部的第一个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)); //然后从外部第一个args开始添加
return f.apply(this, a); // 基于这个实参列表调用f()
};
}
// 这个函数的实参被用作模板,实参列表中的undefined值都被填充
function partial(f) {
var args = arguments;
return function() {
var a = array(args, 1);
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 记忆
在函数式编程中,把将上次计算记过缓存的技术叫做记忆memerization
。
本质上是牺牲算法的空间复杂度以换取更优的时间复杂度。因为在客户端中JavaScript代码的执行速度往往成为瓶颈。
// 返回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()创建新对象cache并将其保存在局部变量中,对于返回的函数来说它是私有的(在闭包中)。
// 返回的函数将它的实参数组转化为字符串,并将字符串用作缓存对象的属性名。如果在缓存中有这个值,则直接返回
// 如果没有,调用既定函数对实参进行计算,将计算结果缓存并返回
// 返回两个整数的最大公约数
function gcd(a, b) {
var t;
if(a < b) { t= b; b = a; a = t; }
while(b !== 0) {
t = b;
b = a % b;
a = t;
}
return a;
}
var gcdmemo = memorize(gcd);
gcdmemo(85, 187); // ==> 17
//注意写一个递归函数时,往往需要记忆功能
// 调用实现了记忆功能的递归函数
var factorial = memorize(function(n) {
return (n <= 1)? 1 : n * factorial(n - 1);
});
factorial(5); // ==> 120,同时缓存了1~4的值。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。