JavaScript精粹读书笔记(5)

第5章 继承

在那些基于类的语言(比如Java)中,继承(inheritance或extends)提供了两个有用的服务。首先,它是代码重用的一种形式。如果一个新的类与一个已存在的类大部分相似,那么你只须具体说明其不同点即可。类继承的另一个好处是它包括了一套类型系统的规范。由于程序员无须编写显式类型转换的代码,他们的工作量将大大减轻,这是一件很好的事情,因为类型转换时会丢失类型系统在安全上的好处。

JavaScript是一门弱类型语言,从不需要类型转换。对象的起源是无关紧要的。对于一个对象来说重要的是它能做什么,而不是它从哪里来。

JavaScript提供了一套更为丰富的代码重用模式。它可以模拟那些基于类的模式,同时它也可以支持其他更具表现力的模式。在JavaScript中可能的继承模式有很多。在本章中,我们将研究几种最为直接的模式。当然还有更多更为复杂的结构,但保持它的简单通常是最好的。

在基于类的语言中,对象 是类实例,并且类可以从另一个类继承。JavaScript是一门基于原型的语言,这意味着对象直接从其他对象继承。

5.1 伪类

当一个函数对象被创建时,Function构造器产生的函数对象会运行类似这样的一些代码:

+展开
-JavaScript
this.prototype={constructor:this};


新函数对象被赋予一个prototype属性,其值是包含一个constructor属性且属性值为该新函数对象。该prototype对象是存放继承特征的地方。因为JavaScript语言没有提供一种方法去确定哪个函数是打算用来作构造器的,所以每个函数都会得到一个prototype对象。constructor属性没什么用。重要的是prototype对象。

当采用构造器调用模式,即使用new前缀去调用一个函数时,这将修改函数执行的方式。如果new运算符是一个方法而不是一个运算符,它可能会像这样执行:

+展开
-JavaScript
Function.method(‘new’,function(){

       //创建一个新对象,它继承自构造器函数的原型对象。

       var that=Object.beget(this.prototype);

       //调用构造器函数,绑定this到新对象上。

       var other = this.apply(that,arguments);

       //如果它的返回值不是一个对象,就返回该新对象

       return (typeof other === ‘object’ && other)||that;

});


我们可以定义一个构造器并扩充它的原型:

+展开
-JavaScript
var Mammal=function(name){

       this.name=name;

};

Mammal.prototype.get_name=function(){

       return this.name;

};

Mammal.prototype.says=function(){

       return this.saying || ‘’;

};


现在,我们可以构造一个实例:

+展开
-JavaScript
var myMammal=new Mammal(‘Herb the Mammal’);

var name=myMammal.get_name();//Herb the Mammal


我们可以构造另一个伪类来继承Mammal,这是通过定义它的constructor函数并替换它的prototype为一个Mammal的实例来实现的:

+展开
-JavaScript
var Cat=function(name){

       this.name=name;

       this.saying=’meow’;

};

//替换Cat.prototype为一个新的Mammal实例

Cat.prototype=new Mammal();

//扩充新原型对象,增加purr和get_name方法。

 

//扩充新原型对象,增加purr和get_name方法。

Cat.prototype.purr=function(n){

       var i,s=’’;

       for(i=0;i<n;i+=1){

              if(s){

                     s+=’-’;

              }

              s+=’r’;

       }

       return s;

};

Cat.prototype.get_name=function(){

       return this.says()+’  ’+this.name+’  ’+this.says();

};

var myCat=new Cat('Henrietta');

var says=myCat.says();//'meow'

var purr=myCat.purr(5);//'r-r-r-r-r'

var name=myCat.get_name();

alert(says);

alert(purr);

alert(name);

//'meow Henrietta meow'


伪类模式本意是想向面向对象靠拢,但它看起来格格不入。我们可以隐藏一些丑陋的细节,这是通过使用method方法定义一个inherits方法来实现的:

+展开
-JavaScript
Function.method('inherits',function(Parent){

   this.prototype=new Parent();

   return this;

});


我们的inherits和method方法都返回this,这将允许我们可以以级联的样式编程。现在可以只用一行语句构造我们的Cat。

+展开
-JavaScript
var Cat=function(name){

   this.name=name;

   this.saying='meow';

}.inherits(Mammal).method('purr',function(n){

   var i,s='';

   for(i=0;i<n;i+=1){

     if(s){

            s+='-';

     }

     s+='r';

   }

   return s;

 }).method('get_name',function(){

   return this.says()+' '+this.name+' '+this.says();

 });


现在看起来没那么怪异了。但我们是否真的有所改善呢?我们现在有了行为像“类”的构造器函数,但仔细去看,它们可能有着令人惊讶的行为:没有私有环境;所有的属性都是公开的。无法访问super(父类)的方法。

更糟糕的是,使用构造器函数存在一个严重的危害。如果你在调用构造器函数时忘记了在前面加上new前缀,那么this将不会绑定到一个新对象上。可悲的是,this将被绑定到全局对象上,所以你不但没有扩充新对象,反而将破坏全局变量。既没有编译时警告,也没有运行时警告。

这是一个严重的语言设计错误。为了降低这个问题带来的风险,所有的构造器函数都约定命名成首字母大写的形式,并有不以首字母大写的形式拼写任何其他的东西。这样我们至少可以通过目视检查去发现是否缺少了new前缀。一个更好的方案就是不要使用伪类。

5.2 对象说明符

有时候,构造器要接受一大串参数。这可能是令人烦恼的,因为要记住参数的顺序可能非常困难。在这种情况下,如果我们在编写构造器时使其接受一个简单的对象说明符可能会更加友好。所以,与其这样写:

+展开
-JavaScript
var myObject=maker(f,l,m,c,s);


不如这么写:

+展开
-JavaScript
var myObject=maker({

       first:f,

       last:l,

       state:s,

       city:c

})


当与JSON(参与附录E)一起工作时,这还可以有一个间接的好处。JSON文本只能描述数据,但有时数据表示的是一个对象,将该数据与它的方法关联起来是有用的。如果构造器取得一个对象说明符,可以容易做到,因为我们可以简单地传递该JSON对象给构造器,而它将返回一个构造完全的对象。

5.3 原型

在一个纯粹的原型模式中,我们会摒弃类,转而专注于对象。基于原型的继承相比基于类的继承在概念上更为简单:一个新对象可以继承一个旧对象的属性。也许你对此感到陌生,但它真的很容易理解。你通过构造一个有用的对象开始,接着可以构造更多和那个对象类似的对象。可以完全避免把一个应用拆解成一系列嵌套抽象类的分类过程。

让我们先定义一个有用的对象:

+展开
-JavaScript
var myMammal={

   name:'Herb the Mammal',

   get_name:function(){

     return this.name;

   },

   says:function(){

     return this.saying||' ';

   }

};


一旦有了一个想要的对象,我们就可以利用第3章中介绍过的Object.beget方法构造出更多的实例来。

+展开
-JavaScript
var myCat=Object.beget(myMammal);

myCat.name='Henrietta';

myCat.saying='meow';

myCat.purr=function(n){

   var i,s='';

   for(i=0;i<n;i+=1){

      if(s){

         s+='-';

      }

      s+='r';

   }

   return s;

};

myCat.get_name=function(){

   return this.says+' '+this.name+' '+this.says;

};

这是一种“差异化继承”。通过定制一个新的对象,我们指明了它与所基于的基本对象的区别。

5.4 函数化

迄今为止,所看到的继承模式的一个弱点就是我们没法保护隐私。对象的所有属性都是可见的。我们没法得到私有变量和私有函数。有时候那不要紧,但有时候却是大麻烦。遇到这些麻烦的时候,一些不知情的程序员接受了一种伪装私有的模式。如果想构造一个私有的属性,我们就给它起一个奇怪的名字,并且希望其他使用代码的用户假装看不到这些奇怪的成员元素。幸运的是,我们有更好的选择,那就是模块模式的应用。

我们从构造一个将产生对象的函数开始,给它起的名字将以一个小写字母开头,因为它并不需要使用new前缀。该函数包括四个步骤。

1.它创建一个新对象。

2.它选择性地定义私有实例变量和方法。这些就是函数中通过var语句定义的普通变量。

3.它给这个新对象扩充方法。那些方法将拥有特权去访问参数,以及在第二步中通过var语句定义的变量。

4.它返回那个新对象。

这里是一个函数化构造器的伪代码模板:

+展开
-JavaScript
var constructor=function(spec,my){

   var that,其他的私有实例变量;

   my=my||{};

   把共享的变量和函数添加到my中

   that=一个新对象

   添加给that的特权方法

   return that;

}


spec对象包含构造器需要构造一个新实例的所有信息。spec的内容可能会被复制到私有变量中,或者被其他函数改变,或者方法可以在需要的时候访问spec的信息。

my对象是一个为继承链中的构造器提供秘密共享的容器。my对象可以选择性地使用。如果没有传入一个my对象,那么会创建一个my对象。

接下来,声明该对象私有的实例变量和方法。通过简单的声明变量就可以做到。构造器的变量和内部函数变成了该实例的私有成员。内部函数可以访问spec,my,that,以及其他私有变量。

接下来,给my对象添加共享的秘密成员。这是通过赋值语句来做的:

+展开
-JavaScript
my.memeber=value;


现在,我们构造了一个新对象并将其赋值给that。有很多方式可以构造一个新对象。

之后,我们扩充that,加入组成该对象接口的特权方法。我们可以分配一个新函数成为that的成员方法。或者,更安全地,我们可以先将函数定义为私有方法,然后再将它们分配给that:

+展开
-JavaScript
var methodical=function(){

    //   ……

};

that.methodical=methodical;


分两步去定义methodical的好处是,如果其他方法想要调用methodical,它们可以直接调用methodical()而不是that.methodical()。如果该实例被三十或篡改,甚至that.methodical被替换掉了,调用methodical的方法将同样会继续工作,因为它们私有的methodical不受该实例修改的影响。

最后,我们返回that。

现在我们将这个模式应用到mammal例子里。此处不需要my,所以我们先抛开它,但将使用一个spec对象。

name和saying属性现在是完全私有的。它们只有通过get_name和says两个特权方法才可以访问。

+展开
-JavaScript
var mammal=function(spec){

       var that={};

       that.get_name=function(){

              return spec.name;

       };

       that.says=function(){

              return spec.saying||’’;

       };

       return that;

};

 

var myMammal=mammal({name:’Herb’});


在伪类模式中,构造器函数Cat不得不重复构造器Mammal已经完成的工作。在函数化模式中那不再需要了,因为构造器Cat将会调用构造器Mammal,让Mammal去做对象创建中的大部分工作,所以Cat只须关注自身的差异即可。

+展开
-JavaScript
var cat=function(spec){

       spec.saying=spec.saying||’meow’;

       var that=mammal(spec);

       that.purr=function(n){

              var I,s=’’;

              for(i=0;i<n;i+=1){

                     if(s){

                            s+=’-’;

                     }

                     s+=’r’;

              }

              return s;

       };

       that.get_name=function(){

              return that.says()+’ ’+spec.name+’ ’+that.says();

       };

       return that;//默然说话:这里原书源代码又是错误的。

};

var myCat=cat({name:’Henrietta’});


函数化模式还给我们提供了一个处理父类方法的方法。我们将构造一个superior方法,它取得一个方法名并返回调用那个方法的函数。该函数将调用原来的方法,尽管属性已经变化了。

+展开
-JavaScript
Object.method('superior',function(name){

       var that=this,

              method=that[name];

       return function(){

              return method.apply(that,arguments);

       };

});


让我们在coolcat上试验一下,coolcat就像cat一样,除了它有一个更酷的调用父类方法的get_name方法。它只需要一点点的准备工作。我们将声明一个super_get_name变量,并且把调用superior方法所返回的结果赋值给它。

+展开
-JavaScript
var coolcat=function(spec){

       var that=cat(spec),

              super_get_name=that.superior('get_name');

       that.get_name=function(n){

              return 'like '+super_get_name()+' baby';

       };

       return that;

};

var myCoolCat=coolcat({name:'Bix'});

var name=myCoolCat.get_name();//’like meow Bix meow baby’


函数化模式有很大的灵活性。它不仅不像伪类模式那样需要很多功夫,还让我们得到更好的封装和信息隐藏,以及访问父类方法的能力。

如果对象的所有状态都是私有的,那么该对象就成为一个“受限(tamper-proof)”对象。该对象的属性可以被替换或删除,但该对象的完整性不会受到损害。如果我们用函数化的样式创建一个对象,并且该对象的所有方法都不使用this或that,那么该对象就是持久性的。一个持久性对象就是一个简单功能函数的集合。

一个持久性的对象不会被损害。访问一个持久性的对象时,除非被方法授权,否则攻击者不能访问对象的内部状态。

5.5 部件

我们可以从一套部件中组合出对象来。例如,我们可以构造一个能添加简单事件处理特性到任何对象上的函数。它会给对象添加一个on方法,一个fire方法和一个私有的事件注册表对象。

+展开
-JavaScript
var eventuality=function(that){

       var registry={};

       that.fire=function (event){

              //在一个对象上触发一个事件。该事件可以是一个包含事件名称的字符串,

              //或者是一个拥有包含事件名称的type属性的对象。

              //通过'on'方法注册的事件处理程序中匹配事件名称的函数将被调用。

              var array,func,handler,i,

                  type=typeof event==='string'?event:event.type;

              //如果这个事件存在一组事件处理程序,那么就遍历它们并按顺序依次执行

              if(registry.hasOwnProperty(type)){

                     array=registry[type];

                     for(i=0;i<array.length;i++){

                            handler=array[i];

                            //每个处理程序包含一个方法和一组可选的参数。

                            //如果该方法是一个字符串形式的名字,那么寻找该函数

                            func=handler.method;

                            if(typeof func==='string'){

                                   func=this[func];

                            }

                            //调用一个处理程序。如果该条目包含参数,那么传递它们过去。否则,传递该事件对象

                            func.apply(this,handler.parameters||[event]);

                     }

              }

              return this;

       };

       that.on=function(type,method,parameters){

              //注册一个事件。构造一条处理程序条目。将它插入到处理程序数组中,

              //如果这种类型的事件还不存在,就构造一个

              var handler={

                  method:method,

                  parameters:parameters

              };

              if(registry.hasOwnProperty(type)){

                     registry[type].push(handler);

              }else{

                 registry[type]=[handler];

              }

              return this;

       };

       return that;

};


我们可以在任何单独的对象上调用eventuality,授予它事件处理方法。我们也可以赶在that被返回前在一个构造器函数中调用它。用这种方式,一个构造器函数可以从一套部件中组装出对象来。


本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/mouyong/archive/2010/01/10/5169396.aspx

加支付宝好友偷能量挖...


评论(0)网络
阅读(77)喜欢(0)JavaScript/Ajax开发技巧