`
zfcejb
  • 浏览: 21095 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

JavaScript:The Good Parts (四)

阅读更多
第4章 函数(Functions)
4.1函数对象(Function Objects)
4.2函数字面量(Function Literal)
4.3调用(Invocation)
4.4参数(Arguments)
4.5返回(Return)
4.6异常(Exceptions)
4.7给类型增加方法(Augmenting Types)
4.8递归(Recursion)
4.9作用域(Scope)
4.10闭包(Closure)
4.11回调(Callbacks)
4.12模块(module)
4.13级联(Cascade)
4.14套用(Curry)
4.15记忆(Memorization)

JS中最好的特性就是它对函数的实现,它几乎无所不能。
函数包含一组语句,它们是JS的基础模块单元,用于代码复用、信息隐藏和组合调用。
函数用于指定对象的行为。一般来说,所谓编程就是将一组需求分解成一组函数与数据结构的技能。
4.1函数对象(Function Objects)
(1)在JS中函数就是对象。对象是“名/值”对的集合并拥有一个连到原型对象的隐藏连接。对象字面量产生的对象连接到Object.prototype,函数对象连接到Function.prototype(该原型对象本身连接到Object.prototype)。每个函数在创建时附有两个附加的属性:函数的上下文和实现函数行为的代码(1.函数的上下文:指的是函数所在的 scope。一个嵌套函数的 scope 是它的外层函数,顶层函数的 scope 是全局。这个 scope 是静态的,不可改变的。一个函数的 scope 一层层向上。http://www.douban.com/group/topic/22693981  2.实现函数行为的代码:JS在创建一个函数对象时,会给该对象设置一个调用属性;当JS调用一个函数时,可以理解为调用此函数的“调用”属性。)
(2)每个函数对象在创建时也随带有一个prototype属性。它的值是一个拥有constructor属性且值即为该函数的对象(http://blog.csdn.net/adwu73/article/details/7219887)。这和隐藏连接到Function.prototype完全不同。
(3)因为函数是对象,所以它们可以像任何其他的值一样被使用。函数可以存放在变量、对象和数组中。函数可以被当作参数传递给其他函数,函数可以再返回函数。而且因为函数是对象,所以函数可以拥有方法。
(4)函数的与众不同之处在于它们可以被调用。
4.2函数字面量(Function Literal)
函数对象可以通过函数字面量来创建:
var add= function(){ //创建一个名为add的变量,并用来把两个数字相加的函数赋值给它。
    return a + b; 
};

函数字面量包含四个部分:
(1)第一个部分是保留字function。
(2)第二个部分是函数名,它可以被省略。函数可以用它的名字来递归地调用自己。此名字也能被调试器和开发工具用来识别函数。如果没有给函数命名,比如上面这个例子,它会被认为是匿名函数。
(3)函数第三部分是包围在圆括号中的一组参数。其中每个参数用逗号分隔,这些名称将被定义为函数中的变量。它们不像普通的变量那样将被初始化为undefined,而是在该函数被调用时初始化为实际提供的参数的值。
(4)第四部分是包围在花括号中的一组语句。这些预计是函数的主体。它们在函数被调用时执行。
函数字面量可以出现在任何允许表达式出现的地方。函数也可以被定义在其他函数中。一个内部函数自然可以访问自己的参数和变量,同时它也能方便地访问它被嵌套在其中的那个函数(该函数对应的外部函数)的参数与变量。通过函数字面量创建的函数对象包含一个连到外部上下文的连接。这称为闭包(http://www.nowamagic.net/program/program_HowToUnderstandClosure.php)
4.3调用(Invocation)
调用一个函数将暂停当前函数的执行,传递控制权和参数给新函数。除了声明时定义的形式参数,每个函数接收两个附加的参数:[color=red]thisarguments。参数this在面向对象编程中非常重要,它的值取决于调用模式。[/color]在JS中一共有四种调用模式:方法调用模式、函数调用模式、构造器调用模式和apply调用模式。这些模式在初始化关键参数this上存在差异。
调用运算符是跟在任何产生一个函数值的表达式之后的一对圆括号。圆括号内可包含零个或者多个用都后隔开的表达式,每个表达式产生一个参数值。每个参数值被赋予函数声明时定义的形式参数名。当实际参数值过多了,超出的参数值将被忽略。如果实际参数值过少,缺少的值将会被替换为undefined。对参数值不会进行类型检查:任何类型的值都可以被传递给参数。
(1)方法调用模式(The Method Invocation Pattern)
当一个函数被保存为对象的一个属性时,我们称他为一个方法。
当一个方法被调用时,this被绑定到该对象。如果一个调用表达式包含一个属性存取表达式(即一个.点表达式或[subscript]下标表达式),那么它被当作一个方法来调用。
var myObject= {    //创建myObject。它有一个value属性和一个increment方法
    value:0;
    increment: function(inc){  //increment方法接收一个可选参数,如果参数不是数字那么默认使用数字1。
        this.value += typeof inc=='number' ? inc : 1;  
    }    
};
myObject.increment();
document.writeln(myObject.value);    //1
myObject.increment(2);
document.writeln(myObject.value);    //3

方法可以使用this去访问对象,所以它能从对象中取值或修改该对象。this到对象的绑定发生在调用的时候。这个延迟绑定(very late binding)使得函数可以对this高度复用。通过this可取得它们所属对象的上下文的方法称为公共方法。
(2)函数调用模式(The Function Invocation Pattern)
当一个函数并非一个对象的属性时,那么它被当作一个函数来调用:
var sum= add(3,4);    //sum 的值为7

当函数以此模式被调用时,this被绑定到全局变量。这是语言设计上的一个错误。倘若语言设计正确,当内部函数被调用时,this应该仍然绑定到外部函数的this变量。这个设计错误的后果是方法不能利用内部函数来帮助它工作,因为内部函数的this被绑定了错误的值,所以不能共享该方法对对象的访问权。解决方案:如果该方法定义一个变量并给他赋值this,那么内部函数就可以通过那个变量访问到this。
myObject.double= function(){
    var that= this;    //按照约定,作者给这个变量命名为that
    
    var helper= function(){
        that.value= add(that.value, that.value);
    };
    helper();    //以函数的形式调用helper
};
myObject.double();    //以方法的形式调用double
document.writeln(myObject.getValue());    //6

(3)构造器调用模式(The Constructor Invocation Pattern)
JS是一门基于原型继承的语言。这意味着对象可以直接从其他对象继承属性。该语言是无类别的。
JS本身又对其原型的本质缺乏信心,所以它提供了一套和基于类的语言类似的对象构建方法。如果在一个对象前面带上new来调用,那么将创建一个隐藏连接到该函数的prototype成员的新对象,同时this将会绑定到那个新对象上。
var Quo= function(string){    //创建一个名为Quo的构造器函数。它构造一个带有status属性的对象
    this.status= string;
};
Quo.prototype.getStatus= function(){    //给Quo所有实例提供一个名为getStatus的公共方法
    return this.status;
};

var myQuo= new Quo('confused');    //构造一个Quo实例
doucument.writeln(myQuo.getStatus());    //'confused'

目的就是结合new前缀调用的函数被称为构造器函数。按照约定,它们保存在以大写格式的命名变量里。如果调用构造器函数时没有在前面加上new,可能会发生糟糕的事情,既没有编译时警告,也没有运行时警告,所以大写约定非常重要。
作者不推荐使用构造器函数。
(4)Apply调用模式(The Apply Invocation Pattern)
JS是一门函数式面向对象编程语言,所以函数可以拥有方法。
Apply方法允许我们构建一个参数数组并用其去调用函数。它也允许我们选择this的值。
Appy方法接收2个参数:第一个是将被绑定个给this的值,第二个就是这个参数数组。
var array= [3,4];
var sum= add.apply(null,array);    //sum值为7

var statusObject= {    //构建一个包含status成员的对象
    status: 'A-OK';
};
//statusObject并没有继承Quo.prototype,但我们可以在statusObject上调用getStatus方法,尽管statusObject并没有一个getStatus方法
var status= Quo.prototype.getStatus.apply(statusObject);    //status值为'A-OK'


obj1.method1.call(obj2,argument1,argument2);
obj1.method1.apply(obj2,[arguments]);

①两个方法等价 ②如果没有提供obj2,global对象被当作obj2
③简单的说就是把obj1的方法放到obj2上使用,后面的argument1..([argumentsArr])这些做为参数传入

4.4参数(Arguments)
(1)当函数被调用时,会得到一个“免费”奉送的参数,那就是Arguments数组。
(2)通过它函数可以访问所有它被调用时传递给它的参数列表,包括那些没有分配给函数声明时定义的形式参数的多余参数。
(3)这使得编写一个无需指定参数个数的函数成为可能:
//构造一个将很多个值相加的函数
//注意该函数内部定义的变量sum不会与外部定义的sum产生冲突
//该函数只会看到内部的那个变量
var sum= function(){
    var i,sum=0;
    for(i=0; i<arguments.length; i += 1){
        sum+ =arguments[i];
    }
    return sum;
};
doucument.writeln(sum(4,8,15,16,23,42));    //108

(4)这不是一个特别有用的模式。在第6章,作者将会给数组添加一个相似的方法来达到同样的效果。
(5)因为语言的一个设计错误,arguments并不是一个真正额数组。它只是一个“类似数组(array-like)”的对象。arguments拥有一个length属性,但它缺少所有的数组方法。
4.5返回(Return)
(1)当一个函数被调用时,它从第一个语句开始执行,并在遇到关闭函数体}时结束。那使得函数把控制权交还给调用该函数的程序部分。
(2)return语句可用来使函数提前返回。当return被执行时,函数立即返回而不再执行余下的语句。
(3)一个函数总是会返回一个值。如果没有返回值,则返回undefined。
(4)如果函数以在前面加上new前缀的方式来调用,且返回值不是一个对象,则返回this(该新对象)。
4.6异常(Exceptions)
(1)JS提供了一套一次处理机制。异常是干扰程序的正常流程的非正常(但并非完全是出乎意料)的事故。
(2)当查出这样的事故时,程序应该抛出一个异常:
var add= function(a, b){
    if(typeof a !== 'number' || typeof b !== 'number'){
        throw{
            name: 'TypeError',
            message: 'add needs numbers'
        };  
    }
    return a + b;
}

throw语句中断函数的执行。它应该抛出一个exceptions对象,该对象包含可识别异常类型的name属性和一个描述性的message属性;也可以添加其他的属性。
(3)该exception对象将被传递到一个try语句的catch从句:
var try_it= function(){
    try{
        add('seven');
    } catch(e) {
        document.writeln(e.name + ':' + e.message);   
    }
}
try_it();

如果try代码块内抛出一个异常,控制权将会跳转到它的catch从句。
一个try语句只会有一个将捕获所有异常的catch代码块。如果你的处理手段取决于异常的类型,那么异常处理必须检查异常对象的name属性以确定异常的类型。
4.7给类型增加方法(Augmenting Types)
(1)JS允许给语言的基本类型增加方法。
在第3章中我们已经看到,通过给Object.prototype添加方法来使得该方法对所有对象都可用。这样的方式对函数、数组、字符串、数字、正则表达式和布尔值同样适用。
(2)我们通过给Function.prototype增加方法来使得该方法对所有函数可用:
Function.prototype.method= function(name, func){
    this.prototype[name]= func;
    return this;
};

通过给Function.prototype增加一个method方法,我们就不必键入prototype这个属性名。这个缺点就被掩盖了(有点不明白)。
(2)基本类型的原型是公共的结构,所以在使用类库时务必小心。
//要注意构建方法时, 给基本类型添加方法时, 新的方法会赋予到所有的值,(包括存在的)  
//所以要尽量避免添加到已存在的值, 安全的做法就是要检测是否有要添加的属性  
Function.prototype.method = function ( name, func ){  
    if( ! this.prototype[name] ){  
        this.prototype[name] = func;  
    }    
    return this;  
} 

(3)JS没有单独的整数类型, 有的时候提取数字忠的整数部分是必要的。 
//通过给Number.prototype(不必键入prototype属性名)添加一个integer方法,它会根据数字的正负来判断是使用Math.celling还是Math,floor 
Number.method('integer', function(){  
        return Math[this < 0 ? 'ceil' : 'floor'](this);
});  
document.writeln((-10/3).integer());    //-3

(4)JS缺少一个移除字符串末端空白的方法。
String.method('trim', function(){
    return this.replace(/^\s+|\s+$/g, '');
});
document.writeln('"' + "  neat  ".trim() + '"');  //"neat"

通过给基本类型增加方法,可以大大提高语言的表现力。因为JS原型动态本质,新的方法被立刻赋予到所有的值(对象实例)上,哪怕值(对象实例)是在方法被创建之前就创建好了。
4.8递归(Recursion)
(1)递归函数会直接或间接地调用自身的一种函数。递归是一种强大的编程技术,它将一个问题分解为一组相似的子问题,没一个都有一个寻常解去解决。一般来说一个递归函数调用自身去解决它的子问题。
(2)“汉诺塔”问题。
问题描述:塔的设备包括三根柱子和一套直径各不相同的空心圆盘。开始时源柱子上的所有圆盘都按照较小的圆盘放在较大的圆盘之上的顺序堆叠。目标是通过每次移动一个圆盘到另一根柱子,最终将一堆圆盘移动到目标柱子上,过程中不可以将大的圆盘放置在较小的圆盘之上。
问题寻常解:
var hanoi= function(disc, src, aux, dst){
    if(disc> 0){
        hanoi(disc-1, src, dst, aux);  //①
        document.writeln('Move disc '+disc+' from '+src+' to '+dst);  //②
        hanoi(disc-1, aux, src, dst);  //③
    }
}
hanoi(3, 'src', 'Aux', 'Dst');

圆盘数量为3时它返回这样的解法:
Move disc 1 from Src to Dst
Move disc 2 from Src to Aux
Move disc 1 from Dst to Aux
Move disc 3 from Src to Dst
Move disc 1 from Aux to Src
Move disc 2 from Aux to Dst
Move disc 1 from Src to Dst

函数描述:hanoi函数把一堆圆盘从一根柱子移动到另一根柱子,必须要使用辅助柱子。它把该问题分解成三个子问题(上面的函数注释①②③)
①首先,它移动一对圆盘中较小的圆盘到辅助柱子上,从而露出底下较大的圆盘。
②然后它将移动底下的圆盘到目标柱子上。
③最后它将刚才较小的圆盘从辅助柱子上移动到目标柱子上。
通过递归地调用自身去处理一对圆盘的移动,从而解决那些子问题。
(3)递归函数操作树形结构(比如浏览器文档对象模型(DOM)),每次递归调用时处理给定树的一小段。
//定义walk_the_DOM函数,它从某个给定的节点开始,按HTML源码中的顺序访问该树的每个节点。
//它会调用一个函数,并依次传递每个节点给它。walk_the_DOM调用自身去处理每一个节点。
var walk_the_DOM= function(node, func){
    func(node);  //操作节点
    node= node.firstChild;  //节点赋值为第一个孩子节点
    while(node){  //当节点有第一个孩子节点
        walk(node, func);  //递归调用自己,直到该节点没孩子节点
        node= node.nextsibling;  //下一个孩子节点
    }
};

//定义一个getElementsByAttribute函数。它取得一个属性名称字符串attr和一个可选的匹配值value。
//它调用wal_the_DOM,传递一个用来查找节点属性名的函数,匹配的节点会累积到一个结果数组中。
var getElementsByAttribute= function(att, value){
    var results= [];

    walk_the_DOM(document.body, function(node){
        var actual= node.nodeType===1 && node.getAttribute(attr);
        if(typeof actual==='string' && (actual==='value' || typeof value!==='string')){
            results.push(node);
        
        }
    });
    return results;
};

(4)尾递归(http://baike.baidu.com/view/1439396.htm)
一些语言提供了尾递归优化。这意味着如果一个函数返回自身递归调用的结果,那么调用的过程会被替换为一个循环,它可以显著提高速度。遗憾的是,JavaScript当前并没有提供尾递归优化。深度递归的函数可能会因为返回堆栈溢出而运行失败。
//构建一个带为递归的函数,因为它返回自身调用的结果,所以它是尾递归。
//JS当前没有对这种形式的递归做出优化。
var factorial= function factorial(i, a){
    a= a || 1;
    if(i< 2){
        return a;
    }
    return factorial(i-1, a*i);
};
document.writeln(factorial(4));  //24

4.9作用域(Scope)
(1)在编程语言中,作用域控制着变量与参数的可见性与生命周期。对程序员来说这是一个重要的帮助,因为它减少了名称冲突,并且提供了自动内存管理。
var foo= function(){
    var a= 3,b= 5;
    
    var bar= function(){
        var b= 7,c= 11;    //此时,a为3,b为7,c为11
        a+= b+c;    //此时,a为21,b为7,c为11
    };
    //此时,a为3,b为5,c没有定义
    bar();
    //此时,a为21,b为5
};

(2)大多数使用c语言语法的语言都拥有块级作用域。在一个代码块中定义的所有变量在代码块的外部是不可见的。定义在代码块中的变量在代码块执行结束后会被释放掉。
(3)尽管代码块的语法似乎表现出它支持块级作用域,但实际上JS并不支持。
(4)JS确实有函数作用域。那意味着定义在函数中的参数和变量在函数外部都是不可见的,而且在一个函数中的任何位置定义的变量在该函数中任何地方都是可见的。
(5)很多语言推荐尽可能迟地声明变量。因为JS缺少块级作用域,所以,做好的做法是在函数体的顶部声明函数中可能用到的所有变量。
4.10闭包(Closure)
(1)作用域的好处是内部函数可以访问定义它们的外部函数的参数和变量(除了this和arguments,this绑定到全局变量)。我们的getElementsByAttribute函数可以工作是因为它申明了一个results变量,且传递给walk_the_DOM的内部函数也可以访问results变量。
(2)一个更有趣的情形是内部函数可以拥有比外部函数更长的生命周期。
(3)之前,我们构造了一个myObject对象,它有一个value属性和一个increment方法。假定我们希望保护该值不会被非法更改。
和以前对象字面量去初始化myObject不同,我们通过调用一个函数的形式去初始化myObject,该函数返回一个对象字面量。此函数定义了一个value变量。改变了对increment和getValue方法总是可用的,但函数的作用域使得它对其他的程序来说是不可见的。
    var myObject= function(){
        var value=0;
        
        return {
            incrment:function(inc){
                value += typeof inc=== 'number'? inc: 1;
            },
            getValue:function(){
                return value;
            }
        }
    }();

我们并没有把一个函数赋值给myObject。我们是把调用该函数后返回的结果赋值给它,注意最后一行的()。该函数返回一个包含两个方法的对象,并且这些方法继续享有访问value变量的特权。
(4)我们定义另一种形式的quo函数。
//创建一个名为quo的构造函数。
//它构造出带有get_status方法和status私有属性的一个对象。
var quo= function (status){
    return {
        get_status: function(){
            return status;
        }
    };
};
var myQuo= quo("amazed");  //构造一个quo实例
dcument.writeln(myQuo.get_status());

当调用quo时,它返回包含get_status方法的一个新对象。该对象的一个引用保存在myQuo中。即使quo已经返回了,但get_status方法仍然享有访问quo对象的status属性的特权。get_status方法并不是访问参数的一个拷贝;它访问的是该参数本身。函数可以访问它被创建时所处的上下文环境,这称之为闭包。
(5)示例1
var fade= function(node){
    var level= 1;
    var step= function(){
        var hex= level.toString(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函数和一个时间给它。然后它返回,fade函数结束。
在大约0.1秒后step函数被调用。它把fade函数的level变量转化为16位字符。接着,它修改fade函数得到的节点背景颜色。然后查看fade函数的level变量。如果北京尚未变成白色,那么它增大fade函数的level变量和用setTimeout预订让它自己再次运行。step函数很快再次被调用。但这次,fade函数的level变量值变成2。fade函数之前已经返回了,但只要fade函数内部需要,它的变量就会持续保持。
(6)示例2
为避免下面的问题,理解内部函数能访问外部函数的实际变量而无需复制是很重要的:
//糟糕的例子
//构造一个函数,用错误的方式给一个数组中的节点设置事件处理程序。
//当点击一个节点时,按照预想应该弹出一个对话框显示节点的序号,但它怎会显示节点的数目。
var add_the_handlers= function(nodes){
    var i;
    for(i= 0;i< nodes.length; i += 1){
        nodes[i].onclick= function(i){
            alert(i);
        }
    }
};  //http://www.f2es.com/discover-javscript-closure-mistake/

add_the_handlers函数目的是给每个事件处理器一个唯一值(i)。它未能达到目的的是因为事件处理器函数绑定了变量i,而不是函数在构造时的变量i的值。
var add_the_handlers= function(nodes){
    var i;
    for(i=0; i< nodes.length; i+= 1){
        nodes[i].onclick= function(i){
            return function(e){
                alert(i);
            };
        }(i);
    }
};

现在,我们定义了一个函数并立即传递i进去执行,而不把一个函数值赋给onclick。那个函数将返回一个事件处理器函数。这个事件处理器函数绑定的是传递进去的i的值。而不是定义在add_the_handlers函数里的i的值。那个被返回的函数被赋值给onclick。
4.11回调(Callbacks)
函数可以让不连续的事件处理变的容易。例如:假定有这么一个序列,由用户交互开始,向服务器发送请求,最终显示服务器响应。
最纯朴的写法可能是这样的:
request= prepare_the_request();
response= send_request_synchronously(request);
display(response);

这种方式的问题在于网络上的同步请求将会导致客户端进入假死状态。如果网络传输或服务器很慢,响应性的降低将是不可接受的。
更好的方式是发起异步请求,提供一个当服务器响应到达时将被调用的回调函数。异步的函数立即返回,这样客户端不会被阻塞。
request= prepare_the_request();
send_request_asynchronously(request, function(response){
    display(response);
});

我们传递了一个函数作为参数给send_request_asynchronously函数,它将在收到响应时被调用。
4.12模块(module)
(1)模块是一个提供接口却隐藏状态与实现的函数或对象。
我们可以通过使用函数和闭包来构造模块。通过使用函数去产生模块,我们几乎可以完全摒弃全局变量的使用,从而缓解这个JS最为糟糕的一个特性之一所带来的影响。
(2)示例
假定我们想要给String增加一个deentityify方法。它的任务是寻找字符串中的html字符实体并替换为它们对应的字符。
在一个对象中保存字符实体的名字和它们对应的字符是有意义的。但我们该在哪里保存该对象呢?我们可以把它放在一个全局变量中,但全局变量是魔鬼。
我们可以把它定义在该函数本身,但是那有运行时的损耗,因为该函数在每次被执行的时候该字面量都会被求职一次。
理想的方式是将它放入一个闭包,而且还能提供一个增加更多字符实体的扩展方法:
String.method('deentityify',function(){
    //字符实体表。它映射字符实体的名字和对应的字符。
    var entity= {
        quot: '"',
        lt: '<',
        gt: '>'
    };
    return function(){  //返回deentityify方法。
        //这才是deentityify方法。它调用字符串的replace方法,查找'&'开头和';'结束的子字符串。
        //如果这些字符可以在字符实体表中找到,那么就将该字符实体替换为映射表中的值。
        return this.replace(/&([^&;]/g, function(a,b){
            var r= entity[b];
            return typeof r==='String' ? r: a;
        });
    }
}());

请注意最后一行。我们用()运算法立刻调用我们刚刚构造出来的函数。这个调用所创建并返回的函数才是deentityify方法。
document.writeln('&lt;&quot;&gt;'.deentityify());    //<">

(3)模块模式利用了函数作用域和闭包来创建绑定对象与私有成员的关联,在这个例子中,只有deentityify方法有权访问字符实体表这个数据对象。
模块模式的一般形式是:一个定义了私有变量和函数的函数;利用闭包创建可以访问私有变量和函数的特权函数;最后返回这个特权函数,或者把它保存到一个可以访问的地方。
使用模块模式可以摒弃全局变量的使用。它促进了信息隐藏和其他优秀设计实践。对于应用程序的封装,或者构造其他单例对象,模块模式非常有效。
(4)示例2
模块模式也可以用来产生安全的对象。假定我们要构造一个用来产生序列号的对象。
var serial_maker= function(){
    //返回一个用来产生唯一字符串的对象。唯一字符串由2部分组成:前缀+序列号。
    //该对象包含一个设置前缀的方法和一个设置序列号的方法,和一个产生唯一字符串的gensym方法
    var prefix= '';
    var seq= 0;
    return {
        set_prefix: function(p){
            prefix= String(p);
        },
        set_seq: function(){
            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的值。
4.13级联(Cascade)
如果我们让方法返回this而不是undefined,就可以启用级联。在一个级联中,我们可以在单独一条的语句中依次调用同一个对象的很多方法。
getElement('myBoxDiv').
    move(350,150).
    width(100).
    height(100).
    color('red')...

在这个例子中,getElement函数产生一个对应于id='myBpxDiv'的DOM元素并提供了其他功能的对象。该方法允许我们移动元素,修改尺寸和样式,并添加行为。
级联可以产生出具备很强表现力的接口。
4.14套用(Curry)
(1)套用允许我们将函数与传递给它的参数相结合产生出一个新的函数。
var add1= add.curry(1);
document.writeln(add1(6));  //7

add1是把1传递给add函数的curry方法后创建的一个函数。add1函数把传递给它的参数的值加1。
(2)JS并没有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方法去连接2个参数数组。
(3)arguments数组并非一个真正的数组,所以它并没有concat方法,要避开这个问题,我们必须在两个arguments数组上都应用数组的slice方法。
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)));
    };
});

4.15记忆(Memorization)
(1)函数可以用对象去记住先前操作的结果,从而能避免无谓的运算。这种优化被称为记忆。
(2)比如说我们想要一个递归函数来计算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('// ' + ': ' + fabonacci(i));
}
// 0: 0
// 1: 1
// 2: 1
// 3: 2
// 4: 3
// 5: 5
// 6: 8
// 7: 13
// 8: 21
// 9: 34
// 10: 55

这样是可以工作的,但是它做了很多无谓的工作。fabonacci函数被调用了453次。我们调用了11次,而它自身调用了442次去计算可能已被刚计算过的值。如果我们让该函数具备记忆功能,就可以显著地减少它的计算量。
我们在一个名为memo的数组里保存我们的存储结果,存储结果可以隐藏在闭包中。当我们的函数被调用时,这个函数首先看是否已经知道存储结果,如果已经知道,就立即返回这个存储结果。
var fabonacci= 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次去取之前的结果。
(3)我们可以把这种形式一般化,编写一个函数来帮助我们构造带记忆功能的函数。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 fibonacci= 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);
});
分享到:
评论

相关推荐

    JavaScript语言精粹.修订版 Javascript:The Good Parts 中英 pdf

    修订版",即《Javascript: The Good Parts》,是由知名的计算机科学家Douglas Crockford所著,这本书深入探讨了JavaScript的核心概念和最佳实践,为开发者提供了宝贵的洞察力。 书中的内容主要分为以下几个部分: ...

    JavaScript: The Good Parts

    Most programming languages contain good and bad parts, but JavaScript has more than its share of the bad, having been developed and released in a hurry before it could be refined. This authoritative ...

    JavaScript:The Good Parts May

    《JavaScript:The Good Parts May》是一本由Douglas Crockford撰写的经典著作,主要探讨了JavaScript编程语言中的一些精华部分。这本书对于理解和掌握JavaScript的核心概念、语法以及最佳实践提供了宝贵的指导,...

    JavaScript: The Good Parts 读书笔记(五)

    JavaScript: The Good Parts 是 Douglas Crockford 著名的一本书,它深入探讨了JavaScript语言的精华部分,帮助开发者避开语言中的陷阱并充分利用其优势。这篇读书笔记将聚焦于书中的核心概念和重要知识点。 首先,...

    JavaScript the good parts

    《JavaScript the Good Parts》是著名的计算机科学家Douglas Crockford所著的一本书,它深入探讨了JavaScript编程语言中最有价值、最稳定的部分,旨在帮助开发者避开语言中的陷阱,充分利用其优势。这本书对于理解和...

    JavaScript语言精粹 修订版 中文高清PDF 带书签 JavaScript The Good Parts .rar

    JavaScript语言精粹(修订版)是一本介绍JavaScript语言本质的权威书籍,值得任何正在或准备从事JavaScript开发的人阅读,并且需要反复阅读。学习、理解,实践大师的思想,我们才可能站在巨人的肩上,才有机会超越大师...

    JavaScript.The.Good.Parts

    《JavaScript.The.Good.Parts》是一本深受程序员喜爱的经典JavaScript指南,由著名的计算机科学家Douglas Crockford撰写。这本书深入浅出地介绍了JavaScript的核心概念、语法特性以及最佳实践,旨在帮助开发者掌握...

    JavaScript: The Good Parts 英文版 适用于Kindle

    英文原版,适用于Amazon Kindle and Amazon Kindle for PC

    JavaScript the good parts 经典课程

    "JavaScript the Good Parts"是一本由Douglas Crockford编写的经典书籍,它深入浅出地讲解了JavaScript的精华部分,帮助开发者理解并掌握这门语言的精髓。在这个经典课程中,我们将探讨以下几个关键知识点: 1. **...

    javascript the good parts

    《JavaScript: The Good Parts》是JavaScript领域的经典之作,由知名编程专家Douglas Crockford撰写。这本书主要聚焦在JavaScript语言中那些优秀的特性上,旨在帮助开发者挖掘并利用这些特性来编写更加可靠、可读...

Global site tag (gtag.js) - Google Analytics