【js设计模式笔记---继承】

来源:互联网 发布:c语言putchar 编辑:程序博客网 时间:2024/05/19 16:06

继承

在javascript中继承是一个非常复杂的话题,比其他任何面向对象语言的中的继承都复杂得多。在大多数其他面向对象语言中,继承一个类只需要使用一个关键字即可。与它们不同,在javascript中要想达到传承公用成员的目的,需要采取一系列措施。更有甚者,javascript属于使用原型式继承的少数语言之一。利益于这种语言的灵活性,你既可使用标准的基于类的继承,也可使用更微妙一些的原型式继承。

为什么需要继承

一般来说,在设计类的时候,我们希望能减少重复性的代码,并且尽量弱化对象间的耦合。使用继承符合前一个设计原则的需要。借助这种机制,你可以在现有类的基础上进行设计并充分利用它们已经具备的各种方法,而对设计进行修改也更为轻松。假设你需要让几个类都拥有一个按特定方式输出类结构的toString()方法,当然可以用复制加粘贴的办法把定义toString()方法的代码添加到每一个类中,但这样做的话,每当需要改变这个方法的工作方式时,你将不得不在每一个类中重复同样的修改。反之,如果你提供了一个ToStringProvider类,然后让那些类继承这个类,那么toString这个方法只需在一个地方声明即可。

让一个类继承另一个类可能会导致二者产生强耦合,也即一个类的依赖于另一个类的内部实现。我们将讨论一些有助于避免这种问题的技术,其中包括用掺元类为其他类提供方法这种技术。

 

类式继承

javascript可能被装扮成使用类式继承的语言。通过用函数来声明类、用关键字new来创建实例,javascript中的对象也能惟妙惟肖地模仿java或c++中的对象。下面是javascript中一个简单的类声明:

function Person(name){

     this.name = name;

}

Person.prototype.getName= function(){

     return this.name;

}

 

要创建该类的实例,只需要结合关键字new调用这个构造函数即可:

var reader = new Person(“John Smith”);

reader.getName();

 

原型链

创建继承Person的类则要复杂一些:

/*Class Author*/

function Author(name,books){

   Person.call(this,name); //call thesuperclass’s constructor in the scope of this.

   this.books = books;

}

Author.prototype =new Person(); //Set up the prototype chain.

Author.prototype.constructor= Author; //Set the constructor attribute to Author.

Author.prototype.getBooks= function(){//Add a method to Author

    return this.books;

}

让一个类继承另一个类需要用到许多行代码(不像大多别的面向对象的语言中那样只用一个关键字extend即可)。首先要做的是像前一个示例中那样创建一个构造函数。在构造函数中,调用超类的构造函数,并将name参数传给它。这行代码需要解释一下。在使用new运算符的时候,系统会为你做一些事。它先创建一个空对象,然后调用构造函数,在此过程这个空对象处于作用域链的最前端。而在Author函数中调用超类的构造函数时,你必须手工完成同样的任务。“Person.call(this,name)”这条语句调用了Person构造函数,并且在此过程中让那个空对象(this)处于作用域链的最前端,而name则被作为参数传入。

下一步是设置原型链。尽管相关代码比较简单,但这实际上是一个非常复杂的话题。前面已经说过,javascript没有extends关键字。但是在javascript中每个对象都有一个名为prototype的属性,这个属性要么指向一另一个对象,要么是null(这种说法并不正确,每个对象都有一个原型对象,但这并不意味着每个对象都有一个prototype属性(实际上只有函数才有这个属性)。在创建一个对象时,javascript会自动将原型对象设置为其构造函数的prototype属性所指的对象。应该注意的是,构造函数本身也是一个对象,它也有自己的原型对象,但这个原型对象并不是它的prototype属性所指向的那个对象。函数作为对象,其构造函数是Function。)。

在访问对象的某个成员时,如果这个成员未见于当前对象,那么javascript会在prototype属性所指的对象中查找它。如果在那个对象中也没有找到,那么javascript会沿着原型链向上逐一访问每个原型对象,直到找到这个成员。这意味着为了让一个类继承另一个类,只需将子类的prototype设置为指向超类的一个实例即可。这与其他语言中的继承机制迥然不同,可能会非常令人费解,而且有违直觉。

为了让Author继承Person,必须手工将Author的prototype设置为Person的一个实例,最后一步骤将prototype的constructornt属性重设为Author。

尽管本例中为实现继承需要使用三行代码,但是创建这个新的子类的实例与创建Person的实例没什么不同

var author = [];

author[0] = newAuthor(“Dustin Diza”,[‘javascript desing patterns’]);

author[1] = newAuthor(“Ross Harmes”,[‘javascript desing patterns’]);

author[1].getName();

author[1].getBooks();

 

由此可见,类式继承的所有复杂性只限于类的声明,创建新实例的过程仍然简单。

extend函数

为了简化类的声明,可以把派生子类的整个过程包装在一个名为extend的函数中。它的作用与其他语言中的extned关键字类似,即基于一个给定的类结构创建一个新的类

/*Extend function*/

//更全面的还是之前学的

function  inherit(p){

  if(p==null){throw TypeError();}

  if(Object.create)return Object.create(p);

  var t = type of p;

  if(t!="object" &&t!="function") throw TypeError();

 function f(){};

 f.prototype = p;

 return new f();

}

 

本书介绍的继承

function  extend(subClass,superClass){

var F = funtion(){};

F.prototype = superClass.prototype;

subClass.prototype = new F();

subClass.prototype.constructor = subClass;

}

这个函数所做的事与先前我们手工做的一样。它设置了prototype,然后再将其constructor重设为恰当的值。作为一项改进,它添加了一个空函数F,并将用它创建一个对象实例插入原型链中。这样做可以避免创建超类的新实例,因为它可能会较庞大,而且有时超类的构造函数有一些副作用,或者会执行一些需要大量计算的任务

使用extend函数后,前面的Person/Author例子变成了这个样子:

/*Class Person*/

function  Person(name){

   this.name = name;

}

Person.prototype.getName= function(){

   return this.name;

}

/*Class Author*/

function  Author(name,books){

    Person.call(this.name);

    this.books = books;

}

extend(Author,Person);

Author.prototype.getBooks= function(){

   this.books;

}

上例中的问题是超类Person的名称被固化在Author类的声明中。更好的方法是像下面这样用一种更具普适性的方式来引用父类。

/*改善*/

function  extend(subClass,superClass){

var F = funtion(){};

F.prototype = superClass.prototype;

subClass.prototype = new F();

subClass.prototype.constructor = subClass;

subClass.superclass =superClass.prototype

if(superClass.prototype.constructor==Object.prototype.constructor){

   superClass.prototype.constructor = superClass;

}

}

上例中它提供了superclass属性,这个属性可以用来弱化Author与Person之间的耦合。就可以有superclass属性来调用超类的构造函数

function  Author(name,books){

    Author.superclass.constructor.call(this,name);

   this.books = books;

}

extend(Author,Person);

Author.prototype.getBooks= function(){

   return this.books;

}

有了superClass属性,就可以直接调用超类中的方法。这在既要重定义超类的某方法而又想访问其在超类的中的实现时可以派上用场。例如,为了用一个新的getName方法重定义Person类中的同名方法,你可以先用Author.superclass.getName获取作者名,然后在此基础上添加其他信息

Author.prototype.getName= function(){

    var name = Author.superclass.getName.call(this);

    return name+”, Author of ”+this.getBooks().join(“,“);

}

原型式继承

原型式继承与类式继承截然不同。我们发现在谈到它的时候,最好忘掉关于类和实例的一切知识,只从对象的角度来思考。用基于类的办法来创建对象包含两个步骤:

首先,用一个类的声明定义对象的结构

第二,实例化该类以创建一个新的对象。

用这种方式创建的对象都有一套该类 所有实例属性的副本。每个实例方法都只存在一份,但每个对象都有一个指向它的连接。

使用原型式继承时,并不需要用类来定义对象的结构,只需要直接创建一个对象即可。这个对象随后可以被新的对象重用,这得益于原型链查找的工作机制。这个对象被称为原型对象,这是因为它为其他对象就有的模样提供了一个原型。这正原型式继承这个名称的由来。如下:

/*Person 原型对象*/

var Person = {

   name:”default name”,

   getName:function(){

    return this.name;

   }

}

这里并没有使用一个名为Person的构造函数来定义类的结构,Person现在是一个对象字面量。它是所要创建的其他种种类Person对象的原型对象。其中定义了所有类的Person对象都具备的属性和方法,并为它们提供了默认值。方法的默认值可能不会被改变,而属性的默认值一般都都会被改变:

var reader =clone(Person);

alert(reader.getName());

reader.name = “JohnSmith”;

alert(reader.getName());

clone函数可以用来创建新的Person对象。它会创建一个空对象,而该对象的原型被设置成为Person。这意味着在这个新对象中查找某个方法或属性时,如果找不到,那么查找过程会在其原型对象中继续进行。

你不必为创建Author而定义一个Person子类,只要执行一次克隆即可:

/*Author 原型对象*/

var Author =clone(Person);

Author.books = [];

Author.getBooks =function(){

     return this.books;

}

然后你可以重写义该克隆中的方法和属性。可以修改在Person中提供了默认值,也可以添加新的属性和方法。

这样一来就创建了一新的原型对象,你可以将其用于创建新的类Author对象。

对于继承而来的成员的读和写的不对等性

前面说过,为了有效地使用原型式继承,必须忘记有关类式继承的一切。这里就是一个例子。在类式继承中,Author的每个实例都有一份自己的books数组副本。你可以用代码author.books.push(“new book title”)为其添加新的元素。但是对于使用原型式继承方式创建的类Author对象来说,由于原型链接的工作方式,这种做法并非一开始就能行得通。一个克隆并非原型对象的一份完全独立的副本,它只是一个以那个对象为原型对象的空对象而已。克隆刚被创建时,author.name其实是一个返回最初的Person.name的链接。对于从原型对象继承而来的成员,其读写具有内在不对等性。在读取author.name的值时,如果你还没有直接为author实例定义name属性的话,那么所得到的是其原型对象的同名属性值。而在写入author.name的值时,你是在直接为author对象定义一个新属性。

如下

var authorClone =clone(Author);

alert(authorClone.name);//Linked to the primative Person.name, which is the string ‘default name’;

author.name = ‘newname’; //a new primative is created and added to the authorClone object itself.

alert(authorClone.name);//访问的是authorClone对象的name 属性

//the same follow

authorClone.books.push(“newbooks”); //链接的是Author.books

authorClone.books= []; //创建一个新的数组,并添加到authorClone对象上。

authorClone.books.push(“newbooks”); //现在改的是新数组

这也说明了为什么必须通过引用传递数据类型的属性创建新副本。在上面的例子中,向authorClone.books数组添加新元素实际上是把这个元素添加到Author.books数组中。这可不是什么好事,因为你对那个值的修改不仅会影响Author,而且会影响所有继承了Author但还未改写那个属性的默认值的对象。在这种场合中,可以使用hasOwnProperty方法来区分对象的实际成员和它继承而来的成员

有时原型对象自己也含有子对象。如果想覆盖子对象中的一个属性值,你不得不重新创建整个子对象。这可以通过将该子对象设置为一个空对象字面量,然后对其进行重塑而办到。但这意味着克隆出来的对象必须知道其原型对象的每一个子对象的确切结构和默认值。为了尽量弱化对象之间的耦合,任何复杂的子对象都应该使用方法来创建:

  var CompondObject = {

        string1:'default value',

        childObject:{

            bool:true,

            num:10

        }

    }

 

    var compoundObjectClone =clone(CompondObject);

    //不好的方式

    compoundObjectClone.childObject.num = 5;

    //好一些的,但compoundObjectClone必须知道对象的结构及默认值。这样可以使用CompondObject

    //与compoundObjectClone之解耦。

    compoundObjectClone.childObject = {

        bool:true,

        num:5

    }

在这个例子中,为compoundObjectClone对象添加一个childObject属性,并修改了它所指向的对象num属性。问题在于compoundObjectClone必须知道childObject具有两个默认值分别为true和10的属性。更好的办法是用一个工厂方法来创建childObject:

//best approach.Uses a method to create a new object,with the same structrue and default as theoriginal.

var CompoundObject= {}

CompoundObject.string1= ‘default value’;

CompoundObject.createChildObject= function(){

return {

   bool:true,

   num:10

}

};

CompoundObject.childObject= CompoundObject.createChildObject();

compoundObjectClone.childObject= CompoundObject.createChildObject();

compoundObjectClone.childObject.num= 5;

clone函数

在前面的例子中用来创建克隆对象的奇妙函数空间长什么样?如下:

function clone(object){

    function F(){}

   F.prototype = object;

   return new F;

}

clone函数首先创建了一个新的空函数F,然后将F的prototype属性设置为作参数ojbect传入的原型对象。由此可以体会到javascript最初设计者的用意。prototype属性就是用来指向原型对象的。通过原型链接机制,它提供了到有继承而来的成员链接。该函数最后通过new运算符作用于F创建出一个新对象,然后把这个新对象作为返回值返回。函数所返回的这个克隆结果是一个以给定对象为原型对象的空对象

 

掺元类

有一种重用代码的方法不需要用到严格的继承。如果想把一个函数用到多个类中,可以通过扩充 的方式让这个类共享该函数。其实实际做法大体为:先创建一个包含各种通用方法的类,然后再用它扩充其它类。这种包含通常方法的类称为掺元类。它们通常不会被实例化或直接调用。其存在的目的只是几其他类提供自己的方法。如下:

var Mixin =function(){};

Minix.prototype ={

     serialize:function(){

        var output = [];

        for(var key in this){

            output.push(key+”:”+this[key]);

        }

         return output.join(‘,’);

     }

}

这个方法可能在许多不同类型的类中都会用到,但没有必要让这些类都继承Mixin,把这个方法的代码复制到这个类中也并不明智。最好还是用augment函数把这个方法添加到每一个需要它的类中。

augment(Author,Mixin);

var author = new Author(“Ross Harmes”,[“javascript Design Patterns”]);

var serializedString = author.serialize();

在此我们用Mixin类中的所有方法扩充了Author类。Author类的实例现在就可以调用serialize方法了。这可以被视为多亲继承(multiple inheritance) 在javascript中的一种实例方式。C++和Python这类语言允许子类继承多个超类。这在js中是不允许的,因为一个对象只能拥有一个原型对象。不过由于一个类可以用多个掺元类加以扩展,所以这实际上实现了多继承的效果。

 

function arugment(receivingClass,givingClass){

      for(var methodName in givingClass){

         if(!receivingClass.prototype[methodName]){

             receivingClass.prototype[methodName] =givingClass.prototype[methodName];

          }

      }

  }

0 0
原创粉丝点击