现在的位置: 首页 > 综合 > 正文

JavaScript精粹读书笔记(4)

2013年01月07日 ⁄ 综合 ⁄ 共 8017字 ⁄ 字号 评论关闭

 

JavaScript中最好的特性就是它对函数的实现。它几乎无所不能。但是,想必你也能预料到,函数在JavaScript里也并非万能药。
函数包含一组语句,它们是JavaScript的基础模块单元,用于代码复用、信息隐藏和组合调用。函数用于指定对象的行为。一般来说,所谓编程就是将一组需求分解成一组函数与数据结构的技能。
在JavaScript中函数就是对象。对象是“名:值”对的集合并拥有一个连到原型对象的隐藏连接。对象定义产生的对象连接到Object.prototype。函数对象连接到Function.prototype(该原型对象本身连接到Object.prototype)。每个函数在创建时附有两个附加的隐藏属性:函数的上下文和实现函数行为的代码(JavaScript创建一个函数对象时,会给该对象设置一个“调用”属性。当JavaScript调用一个函数时,可理解为执行了此函数的“调用”属性。具体参阅ECMAScript规范的13.2 Creating Function Object)。
每个函数对象在创建时也随带有一个prototype属性。它的值是一个拥有constructor属性且值即为该函数的对象。这和隐藏连接到Function.prototype完全不同。这个令人费解的构造过程的意义将会在下个章节中揭示。
因为函数是对象,所以它们可以像任何其他的值一样被使用。函数可以存放在变量,对象和数组中,函数可以被当作参数传递给其他函数,函数也可以再返回函数。而且,因为函数是对象,所以函数可以拥有方法。
函数对象可以通过函数定义来创建:
//创建一个名为add的变量,并用来把两个数字相加的函数赋值给它。
var add=function(a,b){
         return a+b;
};
函数定义包括四个部分。第一个部分是关键字function
第二部分是函数名,它可以被省略。函数可以用它的名字来递归地调用自己。此名字也能被调试器和开发工具来识别函数。如果没有给函数命名,比如上面这个例子,它会被认为是匿名函数。
函数的第三部分是包围在圆括号中的一组参数。其中每个参数用逗号分隔。这些名称将被定义为函数中的变量。它们不像普通的变量那样将被初始化为undefined,而是在该函数被调用时初始化为实际提供的参数的值。
第四部分是包围在花括号中的一组语句。这些语句是函数的主体。它们在函数被调用时执行。
函数定义可以出现在任何允许表达式出现的地方。函数也可以被定义在其他函数中。一个内部函数自然可以访问自己的参数和变量,同时它也能方便地访问它被嵌套在其中的那个函数的参数与变量。通过函数定义创建的函数对象包含一个连到外部上下文的连接。这被称为闭包。它是JavaScript强大表现力的根基。
调用一个函数将暂停当前函数的执行,传递控制权和参数给新函数。除了声明时定义的形式参数,每个函数接收两个附加的参数:this和arguments。参数this在面向对象编程中非常重要,它的值取决于调用的模式。在JavaScript中一共有四种调用模式:方法调用模式、函数调用模式、构造器调用模式和apply调用模式。这些模式在如何初始化关键参数this上存在差异。
调用运算符是一对圆括号。圆括号内可包含零个或多个用逗号隔开的表达式。每个表达式产生一个参数值。每个参数值被赋予函数声明时定义的形式参数名。当实际参数个数与形式参数的个数不匹配时不会导致运行时错误。如果实际参数过多,超出的参数值将被忽略。如果实际参数值过少,缺失的值将会被替换为undefined。对参数值不会进行类型检查:任何类型的值都可以被传递给参数。
方法调用模式
当一个函数被保存为对象的一个属性时,我们称它为一个方法。当一个方法被调用时,this被绑定到该对象。如果一个调用表达式包含一个属性存取表达式(即一个点(.)表达式或下标([])表达式),那么它被当作一个方法来调用。
//创建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);                     //1
 
myObject.increment(2);
document.writeln(myObject.value);                     //3
方法可以使用this去访问对象,所以它能从对象中取值或修改该对象。this到对象的绑定发生在调用的时候。这个“超级”迟绑定使得函数可以对this高度复用。通过this可取得它们所属对象的上下文的方法称为公共方法
函数调用模式
当一个函数并非一个对象的属性时,那么它被当作一个函数来调用:
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
myObject.double();
document.writeln(myObject.getValue());                   //6
构造器调用模式
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,可能会发生非常糟糕的事情,既没有编译时警告,也没有运行时警告。
个人不推荐使用这种形式的构造器函数。下一章会看到更好的替代方式。
apply调用模式
因为JavaScript是一门函数式的面向对象编程语言,所以函数可以拥有方法。
apply方法让我们构建一个参数数组并用其去调用函数。它也允许我们选择this的值,apply方法接收两个参数,第一个是将被绑定给this的值,第二个就是一个参数数组。
var array=[3,4];
var sum=add.apply(null,array);                          //sum的值为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’
当函数被调用时,会得到一个“免费”奉送的参数,那就是arguments数组。通过它函数可以访问所有它被调用时传递给它的参数列表,包括那些没有被分配给函数声明时定义的形式参数的多余参数。这使得编写一个无须指定参数个数的函数成为可能:
//构造一个将很多个值相加的函数
//注意该函数内部定义的变量sum不会与函数外部定义的sum产生冲突。
//该函数只会看到内部的那个变量。
var sum=function(){
       var i,sum=0;
       for(i=0;i<arguments.length;i++){
              sum+=arguments[i];
       }
       return sum;
};
document.writeln(sum(4,8,15,16,23,42));                     //108
这不是一个特别有用的模式。在第6章中,我们将会看到如何给数组添加一个相似的方法来达到同样的效果。
因为语言的一个设计错误,arguments并不是一个真正的数组。它只是一个“类似数组”的对象。arguments拥有一个length属性,但它缺少所有的数组方法。我们将在本章结尾看到这个设计错误导致的后果。
当一个函数被调用时,它从第一个语句开始执行,并在遇到关闭函数体的}时结束。函数把控制权交还给调用该函数的程序部分。
return语句可用来使函数提前返回。当return被执行时,函数立即返回而不再执行余下的语句。
一个函数总是会返回一个值。如果没有指定返回值,则返回undefined。
如果函数以new前缀的形式来调用,且返回值不是一个对象,则返回this(该新对象)。
JavaScript提供了一套异常处理机制。异常是干扰程序正常流程的非正常(但并非完全是出乎意料)的事故。当查出这样的事故时,你的程序应该抛出一个异常:
var add=function(a,b){
       if(typeof(a)!==’number’ || typeof(b)!==’number’){
              throw{
                     name:’TypeError’,
                     message:’加法需要数字’
              };
       }
       return a+b;
}
throw语句中断函数的执行。它应该抛出一个exception对象,该对象包含可识别异常类型的name属性和一个描述性的message属性。你也可以添加其他的属性。
该exception对象将被传递到一个try语句的catch从句:
//构造一个try_if函数,用不正确的方式调用之前的add函数
var try_it=function(){
         try{
                   add(“seven”);
         }catch(e){
                   document.writeln(e.name+”:”+e.message);
         }
}
Try_It();
如果在try代码块内抛出了一个异常,控制权就会跳转到它的catch从句。
一个try语句只会有一个将捕获所有异常的catch代码块。如果你的处理手段取决于异常的类型,那么异常处理器必须检查异常对象的name属性以确定异常的类型。
JavaScript允许给语言的基本类型增加方法。在第3章中,我们已经看到,通过给Object.prototype添加方法来使得该方法对所有对象可用。这样的方式对函数、数组、字符串、数字、正则表达式和布尔值同样适用。
举例来说,我们可以通过给Function.prototype增加方法来使得该方法对所有函数可用:
Function.prototype.method=function(name,func){
       this.prototype[name]=func;
       return this;
}
通过给Function.prototype增加一个emthod方法,我们就不必键入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,’’);
});
通过给基本类型增加方法,我们可以大大提高语言的表现力。因为JavaScript原型继承的动态本质,新的方法立刻被赋予到所有的值(对象实例)上,哪怕值(对象实例)是在方法被创建之前就创建好了。
基本类型的原型是公共的结构,所以在类库混用时务必小心。一个保险的做法就是只在确定没有该方法时才添加它。
//有条件地增加一个方法
Function.prototype.method=function(name,func){
       if(!this.prototype[name]){
              this.prototype[name]=func;
       }
};
递归是一种强大的编程技术,它将一个问题分解为一组相似的子问题,每一个都用一个寻常解去解决。
“汉诺塔”是一个著名的难题。塔的设备包括三根柱子和一套直径各不相同的空心圆盘。开始时源柱子上的所有圆盘都按照较小的圆盘放在较大的圆盘之上的顺序堆叠。目标是通过每次移动一个圆盘到另一根柱子上,最终将一堆圆盘移动到目标柱子上,过程中不可以将大的圆盘放置在较小的圆盘之上。这个难题有一个寻常解:
var hanoi=function(disc,src,sux,dst){
       if(disc>0){
              hanoi(disc-1,src,dst,aux);
              document.writenln(“移动盘子”+disc+”:从”+src+”到”+dst);
              hanoi(disc-1,aux,src,dst);
       }
}
       hanoi(3,'源','中间','目的');
圆盘数量为3时它返回这样的解法:
移动盘子1:从源到目的
移动盘子2:从源到中间
移动盘子1:从目的到中间
移动盘子3:从源到目的
移动盘子1:从中间到源
移动盘子2:从中间到目的
移动盘子1:从源到目的
hanoi函数把一堆圆盘从一根柱子移到另一根柱子,必要时使用辅助的柱子。它把该问题分解成三个子问题。首先,它移动一对圆盘中较小的圆盘到辅助柱子上,从而露出底下较大的圆盘。然后它就移动底下的圆盘到目标柱子上。最后,它将刚才较小的圆盘从辅助柱子上再移动到目标柱子上。通过递归地调用自身去处理一对圆盘的移动,从而解决那些子问题。
传递给hanoi函数的参数包括当前移动的圆盘编号和它将要用到的三根柱子。当它调用自身的时候,它去处理当前正在处理的圆盘之上的圆盘。最终,它会以一个不存在的圆盘编号去调用。在那样的情况下,它不执行任何操作。由于该函数对非法值不予理会,我们也就不必担心它会导致死循环。
递归函数可以非常高效地操作树形结构,比如浏览器端的文档对象模型(DOM)。每次递归调用时处理给定树的一小段。
var walk_the_DOM=function walk(node,func){
       func(node);
       node=node.firstChild;
       while(node){
              walk(node,func);
              node=node.nextSibling;
       }
};
 
//定义getElementsByAttribute函数。它取得一个属性名称字符串
//和一个可选的匹配值。
//它调用walk_the_DOM,传递一个用来查找节点属性点的函数
//匹配的节点会累积到一个结果数组中
var getElementsByAttribute=function(att,value){
       var results=[];
 
       walk_the_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;
};
 

一些语言提供了尾递归优化。即如果函数返回自身递归调用的结果,那么调用的过程会被替换为一个循环,它可以显著提高速度。遗憾的是,JavaScript当前并没有提供尾递归优化。深度递归的函数可能会因为返回堆栈溢出而运行失败。

抱歉!评论已关闭.