【JavaScript语言精粹】读书笔记(三)——函数

来源:互联网 发布:淘宝派发报名任务 编辑:程序博客网 时间:2024/06/06 02:06

所有的过失在未犯以前,都已定下应处的惩罚。——威廉·莎士比亚,《一报还一报(Measure for Measure)》

JavaScript中最好的特性就是它对函数的实现。

函数包含一组语句。它们是JavaScript的基础模块单元,用于代码复用、信息隐藏和组合调用。函数用于指定对象的行为。

一般来说,所谓编程就是将一组需求分解成一组函数数据结构的技能

1. 函数对象

在JavaScript中函数就是对象。对象是“名/值”对的集合并拥有一个连到原型对象的隐藏连接。

对象字面量产生的对象连接到Object.prototype。

函数对象连接到Function.prototype(该原型对象本身连接到Object.prototype)。

每个函数在创建时附带两个附加的隐藏属性:函数的上下文实现函数行为的代码(JavaScript创建一个函数对象是,会给该对象设置一个“调用”属性。当JavaScript调用一个函数是,可理解为调用此函数的“调用”属性,详细可参阅ECMAScript规范13.2 Creating Function Objects)。


每个函数在创建时也随带一个prototype属性。它的值是一个拥有constructor属性且值为该函数的对象。这和隐藏连接到Function.prototype完全不同。之后详解。


因为函数是对象,所以它们可以像任何其他的值一样被使用:

  • 函数可以存放在变量、对象和数组中;
  • 函数可以被当作参数传递给其他函数;
  • 函数可以再返回函数;
  • 函数可以拥有方法。


函数的与众不同之处在于它们可以被调用

2. 函数字面量

函数对象可以通过函数字面量来创建:

// 创建一个名为add的变量,并用来把两个数字相加的函数赋值给它。var add = function (a, b) {return a + b;}

函数字面量包含四部分:

第一部分,保留字function

第二部分,函数名,可以省略。函数可以用名字来递归地调用。此名字也能被调试器可开发工具来识别函数。如果没有给函数命名,则 是匿名函数。

第三部分,包围在圆括号中的一组参数。参数用逗号隔开。这些名称将被定义为函数中的变量。它们不像普通的变量那样将被初始化为undefined,而是在该函数调用时初始化为实际提供的参数的值。

第四部分,包围在花括号中的一组语句。这些语句是函数的主体。它们在函数被调用时执行。


函数字面量可以出现在任何允许表达式出现的地方。函数也可以被定义在其他函数中。一个内部函数自然可以访问自己的参数和变量,同时它也能访问它被嵌套在其中的那个函数的参数与变量。通过函数字面量创建的函数包含一个连到外部上下文的连接。这被称为闭包。它是JavaScript强大表现力的根基。


3. 调用

调用一个函数将暂停当前函数的执行,传递控制权和参数给新函数。

除了声明时定义的形式参数,每一个函数接收两个附加的参数:thisarguments

参数this在面向对象编程中非常重要,它的值取决于调用的模式。

在JavaScript中一共有四种调用模式

  • 方法调用模式
  • 函数调用模式
  • 构造器调用模式
  • apply调用模式

这些模式在如何初始化关键参数this上存在差异


调用运算符是跟在任何产生一个函数值的表达式之后的一对圆括号

圆括号内包含零个或多个用逗号隔开的表达式。每个表达式产生一个参数值。每个参数值被赋予函数声明时定义的形式参数名。当实际参数(arguments)的个数与形式参数(parameters)的个数不匹配时不会导致运行时错误。如果实际参数值过多了,超出的参数值将被忽略。如果实际参数值过少,缺失的值将会被替换成undefined。对参数值不会进行类型检查;任何类型的值都可以被传递给参数。


3.1 方法调用模式

当一个函数被保存为对象的一个属性时,我们称它为一个方法。当一个方法被调用时,this被绑定到该对象。

如果一个调用表达式包含一个属性存取表达式(即一个 .点表达式或[subscript]下标表达式),那么它被当作一个方法来调用。

// 创建myObject。它有一个value属性和一个increment方法。// increment方法接受一个可选的参数。如果参数不是数字,那么默认使用数字1.var myObject = {value : 0,increment : function (inc) {this.value += typeof inc === 'number' ? inc : 1;}};myObject.increment();document.writeln(myObject.value); // 1myObject.increment(2);document.writeln(myObject.value); // 3

方法可以使用this去访问对象,所以他能从对象中取值或修改该对象。this到对象的绑定发生在调用的时候。这个“超级”迟绑定(very late binding)使得函数可以对this高度复用。通过this可取得它们所属对象的上下文的方法称为公共方法


3.2 函数调用模式

当一个函数并非一个对象的属性时,那么它被当作一个函数来调用:

var sum = add(3, 4); // sum的值为7

当函数以此模式调用时,this被绑定到全局对象这是语言设计上的一个错误。倘若语言设计正确,当内部函数被调用时,this应该仍然绑定到外部函数的this变量。这个设计的错误的后果是方法不能利用内部函数来帮助它工作,因为内部函数的this被绑定了错误的值,所以不能共享该方法对对象的访问权。幸运的是,有一个很容易的解决办法:如果该方法定义一个变量并给它赋值为this,那么内部函数就可以通过那个变量访问到this。按照约定,我给那个变量命名为that

// 给myObject增加一个double方法。myObject.double = function () {var that = this; // 解决方法var helper = function () {that.value = add(that.value, that.value);};helper(); // 以函数的形式调用helper。};// 以方法的形式调用double。double作为myObject的一个属性myObject.double();document.writeln(myObject.value); // 6

3.3 构造器调用模式

JavaScript是一门基于原型继承的语言。这意味着对象可以直接从其他对象继承属性。该语言是无类别的。

这偏离了当今编程语言的主流。当今大多数语言都是基于类的语言。尽管原型继承有着强大的表现力,但它并不被广泛理解。

JavaScript本身对其原型的本质也缺乏信息,所以它提供了一套和基于类的语言类似的对象构造语法。

有类型化语言编程经验的程序员们很少有愿意接受原型继承的,并且认为借鉴类型化语言的语法模糊了这门语言真是的原型本质。


如果在一个函数前面带上new来调用,那么将创建一个隐藏连接到该函数的prototype成员的新对象,同时this将会被绑定到那个新对象上


new前缀也会改变return语句的行为,之后详解。

// 创建一个名为Quo的构造器函数,它构造一个带有status属性的对象。var Quo = function (string) {this.status = string;};// 给Quo的所有实例提供一个名为get_status的公共方法。Quo.prototype.get_status = function () {return this.status;};// 构造一个Quo实例var myQuo = new Quo("confused");document.writeln(myQuo.get_status()); // confused

目的就是结合new前缀调用的函数被称为构造器函数。按照约定,它们保存在以大写格式命名的变量里。如果调用构造器函数时没有在前面加上new,可能会发生非常糟糕的事情,既没有编译时警告,也没有运行时警告,所以大写约定非常重要

不推荐使用这种形式的构造器函数,之后有替代方式介绍。

3.4 Apply调用模式

因为JavaScript是一门函数式的面向对象编程的语言,所以函数可以拥有方法。

apply方法让我们构造一个参数数组并用其去调用函数,它允许我们选择this的值

apply方法接受两个参数:

  • 第一个是将被绑定给this的值
  • 第二个是一个参数数组

// 构造一个包含两个数字的数组,并将它们相加。var array = [3, 4];var sum = add.apply(null, array); // 7// 构造一个包含status成员的对象。var statusObject = {status: 'A-OK'};// statusObject并没有继承自Quo.prototype,但我们可以在statusObject上调用// get_status方法,尽管statusObject并没有一个名为get_status的方法。var status = Quo.prototype.get_status.apply(statusObject); // status值为‘A-OK’

说明:将statusObject指定为this的值,用apply调用即返回this.status(statusObject.status)


4. 参数

当函数被调用时,会得到一个“免费”奉送的参数,那就是arguments数组。通过他函数可以访问所有它被调用时传递给它的参数列表。包括那些没有被分配给函数声明时定义的形式参数的多余参数。这是的编写一个无须指定参数个数的函数成为可能

// 构造一个将很多个值相加的函数// 注意该函数内部定义的变量sum不会与函数外部定义的sum产生冲突。// 该函数只会看到内部的那个变量。var sum = function () {var i, sum = 0;for (i = 0; i < arguments.length; i++) {sum += arguments[i];}};document.writeln(sum(4, 8, 15, 16, 23, 42)); // 108

这不是一个特别有用的模式,之后将会看到如何给数组添加一个相似的方法来达到同样的效果。

因为语言的一个设计错误arguments并不是一个真正的数组。它只是一个“类似数组(array-like)”的对象。arguments拥有一个length属性,但它缺少所有的数组方法。之后会看到这个设计错误导致的后果。


5. 返回

当一个函数被调用时,它从第一个语句开始执行,并在遇到关闭函数体的} 时结束。那使得函数把控制权交还给调用该函数的程序部分。

return语句可用来是函数提前返回。当return被执行时,函数立即返回而不再执行余下的语句。

一个函数总会返回一个值,没有指定则返回undefined。

如果函数以在前面加上new前缀的方式来调用,且返回值不是一个对象,则返回this(该新对象)。


6. 异常

JavaScript提供了一套异常处理机制。异常是干扰程序的正常流程的非正常(但并非完全是出乎意料)的事故。当查出这样的事故时,你的程序应该抛出一个异常:

var add = function (a, b) {if (typeof a !== 'number' || typeof b !== 'number') {throw {name: 'TypeError',message: 'add needs numbers'};}return a + b;};

throw语句中断函数的执行。它应该抛出一个exception对象,该对象包含可识别异常类型的name属性和一个描述性的message属性。你也可以添加其他属性。

该exception对象将被传递到一个try语句的catch从句:

// 构造一个try_it函数,用不正确的方式调用之前的add函数var try_it = function () {try {add("seven");} catch (e) {document.writeln(e.name + ': ' + e.message);}};try_it();

如果在try代码块内抛出一个异常,控制权就会跳转到它们的catch从句。

一个try语句只会有一个将捕获所有异常的catch代码块。如果你的处理手段取决于异常的类型,那么异常处理器必须检查异常对象的nam属性以确定异常的类型。


7. 给类型增加方法

JavaScript允许给语言的基本类型增加方法,之前通过给Object.prototype添加方法来使得该方法对所有对象可用。这样的方式对函数、数组、字符串、数字、正则表达式和布尔值同样适用。

举例,我们可以通过给发、Function.prototype增加方法来使得该方法对所有函数可用:

Function.prototype.method = function (name, func) {this.prototype[name] = func;return this;};

通过给Function.prototype增加一个method方法,我们就不必键入prototype这个属性名。这个确定也就被掩盖了。

JavaScript并没有单独的整数类型,因此有时候值提取数字中的整数部分是必要的。JavaScript本身提供的取整方法有些丑陋。我们可以通过给Number.prototype添加一个integer方法来改善它。它会根据数字的正负判断是使用Math.ceiling还是Math.floor。

Number.method('integer', function () {return Math[this < 0 ? 'ceiling' : 'floor'](this);});document.writeln((-10 / 3).integer()); // -3

JavaScript缺少一个移除字符串末端空白的方法。那是一个很容易修复的疏忽:

String.method('trim', function () {return this.replace(/^\s+|\s+$/g, '');});document.writeln('"' + "   neat   ".trim() + '"');

我们的trim方法使用了一个正则表达式。正则表达式之后详解。

通过给基本类增加方法,我们可以大大增加语言的表现力。因为JavaScript原型继承的动态本质,新的方法立刻被赋予到所有的值(对象实例)上,哪怕值(对象实例)是在方法被创建之前就创建好了。

基本类型的原型是公共的结构,所以在类库混用是务必小心。一个保险的做法就是只在确定没有该方法是才添加它。

// 有条件地增加一个方法Function.prototype.method = function (name, func) {if (!this.prototype[name]) {this.prototype[name] = func;}};

另一个要注意的就是for in语句用在原型上时表现糟糕。之前使用:hasOwnProperty方法来筛选出继承而来的属性,或者我们可以查找特定的类型。


8. 递归

// 定义walk_DOM函数,它从某个给的的节点开始,按HTML源码中的顺序访问该树的每个节点。// 它会调用一个函数,并依次传递每个节点给它,walk_DOM调用自身去处理每个子节点。var walk_DOM = function walk(node, func) {func(node);node = node.firstChild;while (node) {walk(node, func);node = node.nextSibling;}};// 定义getElementsByAttribute函数,它取得一个函数名称字符串和一个可选的匹配值。// 它调用walk_DOM,传递一个用来查找节点属性名的函数。// 匹配的节点会累积到一个结果数组中并返回。var getElementsByAttribute = function (att, value) {var results = [];walk_DOM(document.body, function (node) {var actual = node.nodeType === 1 && node.getAttribute(att);if (typeof actual === 'string' &&(actual === value || typeof value !== 'string')) {results.push(node);}});return results;};

9. 作用域

作用域控制着变量与参数的可见性及生命周期。可以较少名称冲突,并提供了自动内存管理。

var foo = function () {var a = 3, b = 5;var bar = function () {var b = 7, c = 11; // a=3, b=7, c=11a += b + c; // a=21, b=7, c=11};bar(); // a=21, b=5};

JavaScript并不支持块级作用域。JavaScript有函数作用域:在函数中的任何位置定义的变量在该函数中的任何地方都可见。

因为缺少块级作用域,最好在函数体的顶部声明函数中可能用到的所有变量。


10. 闭包

作用域的好处是内部函数可以访问定义它们的外部函数的参数和变量(除了this和arguments)。这是一件非常好的事情。

我们的getElementsByAttribute函数可以工作是因为它声明了一个results变量,且传递给walk_DOM的内部函数也可以访问results变量。

一个更有趣的情形是内部函数拥有比它的外部函数更长的生命周期

之前,我们构造了一个myObject对象,它拥有一个value属性和一个increment方法。假定我们希望保护该值不会被非法更改。

和以前对象字面量形式去初始化myObject不同,我们通过调用一个函数的形式去初始化myObject,该函数将返回一个对象字面量。此函数定义了一个value变量。该变量对increment和getValue方法总是可用的,但函数的作用域使得它对其他的程序来说是不可见的。

var myObject = function () {var value = 0;return {increment: function (inc) {value += typeof inc === 'number' ? inc : 1;},getValue: function () {return value;}};}();

注意最后一行的()。我们并没有把一个函数赋值给myObject,而是把调用该函数后返回的结果赋值给它

该函数返回一个包含两个方法的对象,并且这些方法继续享有访问value变量的特权


之前Quo构造器产生出带有status属性和get_status方法的一个对象。但那看起来并不是十分有趣,为什么要用一个getter方法去访问你本可以直接访问的属性呢?如果status是私有属性,它才是更有意义的。所以,让我们定义另一种形式的quo函数来做此事:

// 创建一个名为quo的构造函数// 它构造出带有get_status方法和status私有属性的一个对象var quo = function (status) {return {get_status: function () {return status;}};};// 构造一个quo实例var myQuo = quo("amazed");document.writeln(myQuo.get_status());

对比原来方式:

// 创建一个名为Quo的构造器函数,它构造一个带有status属性的对象。var Quo = function (string) {this.status = string;};// 给Quo的所有实例提供一个名为get_status的公共方法。Quo.prototype.get_status = function () {return this.status;};// 构造一个Quo实例var myQuo = new Quo("confused");document.writeln(myQuo.get_status()); // confused

这个quo函数被设计成无须在前加上new来使用,所以名字也没有首字母大写。当我们调用quo时,它返回包含get_status方法的一个新对象。该对象的一个引用保存在myQuo中。即使quo已经返回了,但get_status方法仍然享有访问quo对象的status属性的特权。get_status方法并不是访问该参数的一个拷贝;它访问的就是该参数本身。这是可能的,因为该函数可以访问它被创建时所处的上下文环境,这被称为闭包

另一个更有用的例子:

// 定义一个函数,它设置一个DOM节点为黄色,然后把它渐变成白色。var fade = function (node) {var level = 1;var step = function () {var hex = level.toStirng(16);node.style.backgroundColor = '#FFFF' + hex + hex;if (level < 15) {level += 1;setTimeout(step, 100);}};setTimeout(step, 100);};fade(document.body);

我们调用fade,把document.body作为参数传递给它,fade函数这是level为1,。它定义了一个step函数;接着调用setTimeout,并传递step函数和一个时间(100毫秒)给它。然后它返回,fade函数结束。

在大约十分之一秒后step函数被调用。它把fade函数的level变量转化为16位字符。接着,它修改fade函数得到的节点的背景颜色。然后查看fade函数的level变量。如果背景色尚未变成白色,那么它增大fade函数的level变量和用setTimeout预定让它自己再次运行。

step函数很快再次被调用。但这次,fade函数的level变量变成2.fade函数在之前已经返回了,但只要fade的内部函数需要,它的变量就会持续保留

为了避免下面的问题,理解内部函数能访问外部函数的实际变量而无需复制是很重要的:

// 糟糕的例子// 构造一个函数,用错误的方式给一个数组中的节点设置事件处理程序。// 当点击一个节点时,按照预想应该弹出一个对话框显示节点的序号// 但它总是会显示节点的数目。var add_handlers = function (nodes) {var i;for (i = 0; i < nodes.length; i++) {nodes[i].onclick = function (e) {alert(i);// 与变量i绑定,i最后的结果是:node.length}}};// 结束糟糕的例子

add_handlers函数目的是给每个时间处理器一个唯一值(i)。它未能达到的目的是因为时间处理器函数绑定了变量i,而不是函数在构造时的变量i的值。

// 更好的例子// 构造一个函数,用正确的方式给一个数组中的节点设置事件处理程序。// 你点击一个节点,将会弹出一个对话框显示节点的序号。var add_handlers = function (nodes) {var i;for (i = 0; i < nodes.length; i++) {nodes[i].onclick = function (i) {return function (e) {alert(i);// 与传入的i的值绑定,每次循环i的值不同};}(i);}};

现在,我们定义了一个函数并立即传递变量i 的值进去执行,而不是把一个函数赋值给onclick。那个函数将返回一个事件处理器。这个事件处理器函数绑定的是传递进去的i的值,而不是定义在add_handlers函数里的变量i。那个被返回的函数被赋值给onclick。

11. 回调

函数可以让不连续时间的处理变得更容易,例如:假定有这么一个序列,由用户交互开始,想向服务器发送请求,最后显示服务器的响应。最纯朴的写法可能会是这样:

request = prepare_request();response = send_request_synchronously(request);display(response);

这种方式的问题在于网络上的同步请求将会导致客户端进入假死状态。如果网络传输或服务器很慢,响应性的降低将是不可接受的。

更好的方式是发起异步请求,提供一个当服务器的响应到达时将会调用的回调函数。异步的函数立即返回,这样客户端不会阻塞。

request = prepare_requset();send_requset_synchronously(request, function (response) {display(response);});

我们传递了一个函数作为参数给send_request_synchronously函数,它将在收到响应时被调用。


12. 模块

我们可以使用函数和闭包来构造模块。模块是一个提供接口却隐藏状态与实现的函数或对象。

通过使用函数去产生模块,我们机会可以完全摒弃全局变量的使用,从而缓解这个JavaScript的最为糟糕的特性之一所带来的影响。

举例,假定我们想要给String增加一个deentityify方法。它的任务是寻找字符串中的HTML字符实体并替换为它们对应的字符。在一个对象中保存字符实体的名字和它们对应的字符是有意义的。但我们该在哪里保存该对象呢?我们可以把它放到一个全局变量中,但是全局变量是魔鬼。我们可以把它定义在该函数本身,但是那有运行时的损耗,因为该函数在每次被执行的时候该字面量都会被求值一次。理想的方式是将其放入一个闭包,而且也许还能提供一个增加更多字符实体的扩展方法:

String.method('deentityify', function () {// 字符实体表。它映射字符实体的名字到对应的字符。var entity = {quot: '"',lt: '<',gt: '>'};// 返回deentityify方法。return function () {// 这才是deentityify方法。它调用字符串replace方法,// 查找‘&’开头和‘;’结束的字符串。如果这些字符串可以再字符实体表中找到,// 那么就将该字符实体替换为映射表中的值。它用到了一个正则表达式,之后详解。return this.repace(/&([^&;]+);/g, function (a, b) {var r = entity[b];return typeof r === 'String' ? r : a;});};}());

请注意随后一行。我们用() 运算符立刻调用我们刚刚构造出来的函数。这个调用所创建并返回的函数才是deentityify方法。

document.writeln('<">'.deentityify()); // <">

模块模式利用了函数作用域和闭包来创建绑定对象与私有成员的关联,在这个例子中,只有deentityify方法有权访问字符实体表这个数据对象。

模块模式的一般形式是

  • 一个定义了私有变量函数的函数;
  • 利用闭包创建可以访问私有变量和函数的特权函数
  • 最后返回这个特权函数,或者把它们保存到一个可以访问的地方。

使用模块模式就可以摒弃全局变量的使用。它促进了信息隐藏和其他优秀的设计实践。对于应用程序的封装,或者构造其他单例对象,模块模式非常有效。

模块模式也可以用来产生安全的对象。假定我们想要构造一个用来产生序列号的对象:

var serial_maker = function () {// 返回一个用来产生唯一字符串的对象。// 唯一字符串由两部分组成: 前缀 + 序列号。// 该对象包含一个设置前缀的方法,一个设置序列号的方法。// 和一个产生唯一序列号的gensym方法var prefix = '';var seq = 0;return {set_prefix: function (p) {prefix = String(p);},set_seq: function (s) {seq = s;},gensym: function () {var result = prefix + seq;seq += 1;return result;}};};var seqer = serial_maker();seqer.set_prefix('Q');seqer.set_seq(1000);var unique = seqer.gensym(); // unique is "Q1000"

seqer包含的方法都没有用到this或that。因此没有办法损害seqer。除非调用对应的方法,否则没法改变prefix或seq的值。seqer对象是可变的,所以它的方法可能会被替换掉,但替换后的方法依然不能访问私有成员。seqer就是一组函数的集合,而且那些函数被授予特权,拥有使用或修改私有状态的能力。

如果我们把seqer.gensym作为一个值传递给第三方函数,那个函数能用它产生唯一字符串,但却不能通过它来改变prefix或seq的值。

13. 级联

有一些方法没有返回值。例如,一些设置或修改对象的某个状态却不返回任何值得方法就是典型的例子。如果我们让这些方法返回this而不是undefined,就可以启用级联。在一个级联中,我们可以再单独一条的语句中一次调用同一个对象的很多方法。一个启用级联的Ajax类库可能允许我们一这样的形式去编码:

getElement('myBoxDiv').    move(250, 150).    width(100).    height(100).    color('red').    border('10px outset').    padding('4px').    appendText("Please stand by").    on('mousedown', function (m) {        this.startDrag(m, this.getNinth(m));    }).    on('mousemove', 'drag').    on('mouseup', 'stopDrag').    later(2000, function () {        this.            color('yellow').            setHTML("What hath God Wraught?").            slide(400, 40, 200, 200);    }).    tip("This box is resizeable");

在这个例子中,getElement函数产生一个对应于id="myBoxDiv"的DOM元素并提供了其他功能的对象。该方法允许我们移动元素,修改它的尺寸和样式,并添加行为。这些方法每一个都返回该对象,所以调用返回的结果可以被下一次调用所用。

级联可以产生出具备很强表现力的接口。它也能帮助控制那种构造试图一次做太多事情的接口的趋势。


14. 套用

函数也是只,从而我们可以用有趣的方式去操作函数值,套用允许我们将函数与传递给它的参数相结合去产生出一个新的函数。

var addl = add.curry(1);document.writeln(addl(6)); // 7

addl是吧1传递给add函数的curry方法后创建的一个函数。addl函数把传递给它的参数的值加1。JavaScript并没有curry方法,但我们可以通过给Function.prototype添加功能来实现:

Function.method('curry', function () {var args = arguments, that = this;return function () {return that.apply(null, args.concat(arguments));};}); // 有些不太对头(函数不对)

curry方法通过创建一个保存着原始函数和被套用的参数的闭包来工作。它返回另一个函数,该函数被调用时,会返回调用原始函数的结果,并传递调用curry时的参数加上当前调用的参数的所有参数,它使用Array的concat方法去连接两个参数数组。

糟糕的是,就像我们先前看到的那样,arguments数组并非一个真正的数组,所以它并没有concat方法。要避开这个问题,我们必须在两个arguments数组上都应用数组的slice方法。这样产生出拥有concat方法的常规数组。

Function.method('curry', function () {var slice = Array.prototype.slice,args = slice.apply(arguments),that = this;return function () {return that.apply(null, args.concat(slice.apply(arguments)));};});
说明:

// concat方法var a = [1,2,3];document.write(a.concat(4,5));// slice方法var arr = new Array(3);arr[0] = "George";arr[1] = "John";arr[2] = "Thomas";document.write(arr.slice(1)); // John,Thomas

15. 记忆

函数可以用对象去记住先前操作的结果,从而能避免无谓的运算。这种优化被称为记忆。JavaScript的对象和数组要实现这种优化是非常方便的。

比如说,我们想要一个递归函数来计算Fibonacci数列,一个Fibonacci数字是之前连个Fibonacci数字之和。最前面的两个数字是0和1。

var fibonacci = function (n) {return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);};for (var i = 0; i <= 10; i++) {document.writeln('// ' + i + ': ' + fibonacci(i));}// 0: 0// 1: 1// 2: 1// 3: 2// 4: 3// 5: 5// 6: 8// 7: 13// 8: 21// 9: 34// 10: 55

这样是可以工作的,但它做了很多无谓的工作。fibonacci函数被调用的453次。我们调用了11次,而它自身调用了442次去计算可能已被刚计算过的值。如果我们让该函数具备记忆功能,就可以显著地减少它的运算量。

我们在一个名为memo的数组里保存我们的存储结果,存储结果可以隐藏在闭包中。当我们的函数被调用时,这个函数首先看是否已经知道存储结果,如果已经知道,就立即返回这个存储结果。

var fibonacci = function () {var memo = [0, 1];var fib = function (n) {var result = memo[n];if (typeof result !== 'number') {result = fib(n - 1) + fib(n - 2);memo[n] = result;}return result;};return fib;};

这个函数返回同样的结果,但它只被调用了29次。我们调用了它11次。它自身调用了18次去取得之前存储的记过。

我们可以把这种形式一般化,编写一个函数来帮助我们构造带记忆功能的函数。memoizer函数将取得一个初始化的memo数组和fundamental函数。它返回一个管理memo存储和在需要时调用的fundamental函数的shell函数。我们传递这个shell函数和该函数的参数给fundamental函数:

var memoizer = function (memo, fundamental) {var shell = function (n) {var result = memo[n];if (typeof result !== 'number') {result = fundamental(shell, n);memo[n] = result;}return result;};return shell;};

现在,我们可以使用memoizer来定义fibonacci函数,提供其初始的memo数组和fundamental函数:
var fibnoacci = memoizer([0, 1], function (shell , n) {return shell(n - 1) + shell(n - 2);});
通过设计能产生出其他函数的函数。可以极大减少我们必须要做的工作。例如:要产生一个可记忆阶乘函数,我们只需提供基本的阶乘公式即可:

var factorial = memoizer([1, 1], function (shell, n) {return n * shell (n - 1);});