一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

javascript面向对象之继承的笔记

时间:2014-03-19 编辑:简简单单 来源:一聚教程网

是的我们要理解继承模式首先 来了解下原型链这个东西。
一:原型链
其基本的思想就是让一个引用类型继承另一个引用类型的属性和方法。要理解原型链我们可以先回顾下构造函数,原型和实例三者的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。
我们假如让原型对象等于另一个类型的实例,那么此时的原型对象将包含一个指向另一个原型的指针,而相应的另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系仍然成立。这差不多就是所谓的原型链概念。
我们通过代码看下三者的关系吧:
1:函数与原型的关系
a.每个构造函数都有一原型对象
function fun(){

}
console.log(fun.prototype); //fun{} 说明prototype是一个原型对象

b.原型对象(即prototype属性)会自动获得一个constructor属性,constructor属性包含了prototype属性所在函数(fun)的指针,用来标识实例化对象的类型
如果构造函数中定义了原型中的同名属性方法时,那么实例就会调用重新定义的属性与方法了
function fun(name){
console.log(fun.prototype.name == this.name);//true(yjh)
this.name = “yjh1″;
console.log(this.name);//yjh1
console.log(fun.prototype.name == this.name);//false(yjh,yjh1)
}
fun.prototype = {
constructor: fun,
name: “yjh”
}
var fun1 = new fun();
console.log(fun.prototype.constructor == fun); //true
console.log(fun1.constructor == fun); //true

2:实例化对象与原型的关系
a.当函数使用new操作符实例化一个对象时,那么对象就包含了一个内在的指向原型的__proto__属性,它只存在实例对象与原型对象之间
如:
function fun(name,age){
this.name = name;
this.age = age;
this.sayName = function(){
alert(this.name);
}
}
fun.prototype = {
constructor: fun,
age: 22,
sayName: function(){
alert(this.age);
}
}
var fun1 = new fun(“yjh”,”23″);
console.log(fun1.__proto__) //fun { age=”22″, sayAge=function()}
console.log(fun1.__proto__ == fun.prototype); //true

3:实例对象与函数(构造函数)的关系
a.当函数使用new操作符实例化一个对象时,构造函数中的this对象为实例对象,实例对象获取了绑定到this中的属性和方法
如:
function fun(name,age){
this.name = name;
this.age = age;
this.sayName = function(){
alert(this.name);
}
}
var fun1 = new fun(“yjh”,”23″);
fun1.sayName(); //yjh

4:函数(构造函数)与实例对象、原型三者之间的关系
在调用实例化对象属性,方法时,那么首先会搜索实例对象自身定义的属性和方法,如果没有的话,会继续搜索原型
function fun(name,age){
this.name = name;
this.age = age;
this.sayName = function(){
alert(this.name);
}
}
fun.prototype.age = “22″;
fun.prototype.sayAge = function(){
alert(this.age);
}
var fun1 = new fun(“yjh”,”23″);
fun1.age = 24;
fun1.sayAge(); //24 ,调用了原型中的方法;
原型链很强大,可以用它来实现继承,不过在用原型链实现继承时,不能使用对象字面量创建原型方法,因为这样会重写原型链。如:
function SuperType(){
this.property = true;

}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subprotype = false;
}
SubType.protopyte = new SuperType();
SubType.protopyte = {
getSubValue:function(){
return this.subproperty;
},
someOtherMethod:function(){
return false;
}
};
var instance = new SubType();
alert(instance.getSupervalue()); //error

原型链实现继承的主要问题来自包含引用类型值的原型。因为在通过原型来实现继承时,原型实际上会变成另一个类型的实例,于是原型的实例属性就变成了现在的原型属性。还有一个问题就是在创建子类型的实例时,不能向超类型的构造函数中传递参数。

二:借用构造函数
借用构造函数技术有时候也叫伪造对象或经典继承。其基本思想就是在子类型构造函数的内部调用超类型构造函数。我们知道函数只是在特定环境中执行代码的对象,因此可以通过使用apply()和call()方法也可以在新创建的对象上执行构造函数。如下:
function SuperType(){
this.color = ["red","blue","green"];

}
function SubType(){
SuperType.call(this);
}
var instance1 = new SubType();
instancel.colors.push(“black”);
alert(instancel.colors); //”red,blue,green,balck”
var instance2 = new SubType();
alert(instance2.colors); //”red,blue,green”
上面就是通过call()方法在新创建的SubType实例环境下调用了SuperType构造函数,这样就会在新的SubType对象上执行SuperType()函数中定义的所有对象初始化代码。
传递参数:
前面说了,原型链不能传递参数,而构造函数刚好有这个优势。
举个例子:
function SuperType(nmae){
this.name = name;
}
function SubType(){
SuperType.call(this,”Nicholas”);
this.age = 29;
}
var instance = new SubType();
alert(instance.name); //”Nicholas”;
alert(instance.age); //29
如果仅仅使用借用构造函数来继承,那么也有点不足的地方,那就是 方法都在哦构造函数中定义,函数就起不到复用的效果。
三:组合继承
组合继承也叫作伪经典继承,主要是指将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其基本思路就是通过使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。
如下:
function SuperType(){
this.name = name;
this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
}
function SubType(name,age){
SuperType.call(this,name); //继承属性
this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge =function(){
alert(this.age);
}
var instance1 = new SubType(“Nicholas”,29);
instance1.colors.push(“black”);
alert(instance1.colors); //”red,blue,green,black”
instance1.sayName(); //”Nicholas”
instance1.sayAge(); //29

var instance2 = new SubType(“Greg”,27);
alert(instance2.colors); //”red,blue,green”
instance2.sayName(); //”Greg”
instance2.sayAge(); //27
组合继承是JavaScript中最常用的一种继承模式,而且instanceof和isPrototypeof()也能够识别基于组合继承创建的对象。
四:原型式继承
这种方法并没有使用严格意义上的构造函数,其基本思想是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。其实这本质上来讲是一个浅复制(浅拷贝)。如下:
var person = {
name:”Nicholas”;
friends:["Shelby","Court","Van"]

};
var anotherPerson = object(person);
anotherPerson.name = “Greg”;
anotherPerson.friends.push(“Rob”);

var yetAnotherPerson =object(person);
yetAnotherPerson.name = “Linda”;
yetAnotherPerson.friends.push(“Barbie”);
alert(person.friends); //”Shelby,Court,Van,Rob,Barbie”
这种继承模式就是要求你有一个对象可以作为另一个对象的基础,如果有这么一个对象的话,可以把它传递给object()函数,然后再根据具体需求对得到的对象加以修改即可。
在ECMAScript5里通过新增Object.creat()方法规范了原型式继承。这个方法接受两个参数:一个用作新对象原型的对象和一个为新对象定义额外属性的对象,(后者可选)。不过这个方法只是所有的浏览器都支持,比如ie9以下爱的都不支持。
五:寄生式继承
这个继承方式是与原型式继承紧密相关的一种思路。其基本思想即:创建一个仅用于封装继承过程的函数,该函数内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。如下:
function creatAnother(original){
var clone = object(original); //通过调用函数创建一个新对象
clone.sayHi = function(){ //以某种方式来增强这个对象
alert(“hi”);
};
return clone; //返回这个对象
}
这个例子里,creatAnother()函数接收了一个参数,这个参数就是将要作为新对象基础的对象。然后把这个对象(original)传递给object()函数,将返回的结果赋值给clone.再为clone对象添加一个新方法sayHi(),最后返回给clone.
ps:使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率。
六:寄生组合式继承
组合继承是最常用的一种继承模式,不过它也有不足。其问题就是无论什么情况下都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。
看一个组合继承的例子:
function SuperType(name){
this.name = name;
this.clone = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
}
function SubType(name,age){
SuperType.call(this,name);
this.age = age;
}
SybType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
}
所谓的寄生式组合继承就是通过借用构造函数来继承属性,通过原型链的混成形式来继承方法;基本模式如下:
function inheritPrototype(subType,SuperType){
var prototype = object(superType.prototype); //创建对象
prototype.constructor = SubType; //增强对象
SubType.prototype = prototype; //指定对象
}
这个例子inheritPrototype()函数实现了寄生式组合最简单的形式,这个函数接收两个参数:子类型构造函数和超类型构造函数。

构造函数的继承


对象之间的"继承"的五种方法。

比如,现在有一个"动物"对象的构造函数。


  function Animal(){

    this.species = "动物";

  }

还有一个"猫"对象的构造函数。


  function Cat(name,color){

    this.name = name;

    this.color = color;

  }

怎样才能使"猫"继承"动物"呢?

一、 构造函数绑定

第一种方法也是最简单的方法,使用call或apply方法,将父对象的构造函数绑定在子对象上,即在子对象构造函数中加一行:

  function Cat(name,color){

    Animal.apply(this, arguments);

    this.name = name;

    this.color = color;

  }

  var cat1 = new Cat("大毛","黄色");

  alert(cat1.species); // 动物

二、 prototype模式

第二种方法更常见,使用prototype属性。

如果"猫"的prototype对象,指向一个Animal的实例,那么所有"猫"的实例,就能继承Animal了。

  Cat.prototype = new Animal();

  Cat.prototype.constructor = Cat;

  var cat1 = new Cat("大毛","黄色");

  alert(cat1.species); // 动物

代码的第一行,我们将Cat的prototype对象指向一个Animal的实例。

  Cat.prototype = new Animal();

它相当于完全删除了prototype 对象原先的值,然后赋予一个新值。但是,第二行又是什么意思呢?

  Cat.prototype.constructor = Cat;

原来,任何一个prototype对象都有一个constructor属性,指向它的构造函数。如果没有"Cat.prototype = new Animal();"这一行,Cat.prototype.constructor是指向Cat的;加了这一行以后,Cat.prototype.constructor指向Animal。

  alert(Cat.prototype.constructor == Animal); //true

更重要的是,每一个实例也有一个constructor属性,默认调用prototype对象的constructor属性。

  alert(cat1.constructor == Cat.prototype.constructor); // true

因此,在运行"Cat.prototype = new Animal();"这一行之后,cat1.constructor也指向Animal!

  alert(cat1.constructor == Animal); // true

这显然会导致继承链的紊乱(cat1明明是用构造函数Cat生成的),因此我们必须手动纠正,将Cat.prototype对象的constructor值改为Cat。这就是第二行的意思。

这是很重要的一点,编程时务必要遵守。下文都遵循这一点,即如果替换了prototype对象,

  o.prototype = {};

那么,下一步必然是为新的prototype对象加上constructor属性,并将这个属性指回原来的构造函数。

  o.prototype.constructor = o;

三、 直接继承prototype

第三种方法是对第二种方法的改进。由于Animal对象中,不变的属性都可以直接写入Animal.prototype。所以,我们也可以让Cat()跳过 Animal(),直接继承Animal.prototype。

现在,我们先将Animal对象改写:

  function Animal(){ }

  Animal.prototype.species = "动物";

然后,将Cat的prototype对象,然后指向Animal的prototype对象,这样就完成了继承。

  Cat.prototype = Animal.prototype;

  Cat.prototype.constructor = Cat;

  var cat1 = new Cat("大毛","黄色");

  alert(cat1.species); // 动物

与前一种方法相比,这样做的优点是效率比较高(不用执行和建立Animal的实例了),比较省内存。缺点是 Cat.prototype和Animal.prototype现在指向了同一个对象,那么任何对Cat.prototype的修改,都会反映到Animal.prototype。

所以,上面这一段代码其实是有问题的。请看第二行

  Cat.prototype.constructor = Cat;

这一句实际上把Animal.prototype对象的constructor属性也改掉了!

  alert(Animal.prototype.constructor); // Cat

四、 利用空对象作为中介

由于"直接继承prototype"存在上述的缺点,所以就有第四种方法,利用一个空对象作为中介。

  var F = function(){};

  F.prototype = Animal.prototype;

  Cat.prototype = new F();

  Cat.prototype.constructor = Cat;

F是空对象,所以几乎不占内存。这时,修改Cat的prototype对象,就不会影响到Animal的prototype对象。

  alert(Animal.prototype.constructor); // Animal

我们将上面的方法,封装成一个函数,便于使用。

  function extend(Child, Parent) {

    var F = function(){};

    F.prototype = Parent.prototype;

    Child.prototype = new F();

    Child.prototype.constructor = Child;

    Child.uber = Parent.prototype;

  }

使用的时候,方法如下

  extend(Cat,Animal);

  var cat1 = new Cat("大毛","黄色");

  alert(cat1.species); // 动物

这个extend函数,就是YUI库如何实现继承的方法。

另外,说明一点,函数体最后一行

  Child.uber = Parent.prototype;

意思是为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。(uber是一个德语词,意思是"向上"、"上一层"。)这等于在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。

五、 拷贝继承

上面是采用prototype对象,实现继承。我们也可以换一种思路,纯粹采用"拷贝"方法实现继承。简单说,如果把父对象的所有属性和方法,拷贝进子对象,不也能够实现继承吗?这样我们就有了第五种方法。

首先,还是把Animal的所有不变属性,都放到它的prototype对象上。

  function Animal(){}

  Animal.prototype.species = "动物";

然后,再写一个函数,实现属性拷贝的目的。

  function extend2(Child, Parent) {

    var p = Parent.prototype;

    var c = Child.prototype;

    for (var i in p) {

      c[i] = p[i];

      }

    c.uber = p;

  }

这个函数的作用,就是将父对象的prototype对象中的属性,一一拷贝给Child对象的prototype对象。

使用的时候,这样写:

  extend2(Cat, Animal);

  var cat1 = new Cat("大毛","黄色");

  alert(cat1.species); // 动物


非构造函数的继承


一、什么是"非构造函数"的继承?

比如,现在有一个对象,叫做"中国人"。

  var Chinese = {
    nation:'中国'
  };

还有一个对象,叫做"医生"。

  var Doctor ={
    career:'医生'
  }

请问怎样才能让"医生"去继承"中国人",也就是说,我怎样才能生成一个"中国医生"的对象?

这里要注意,这两个对象都是普通对象,不是构造函数,无法使用构造函数方法实现"继承"。

二、object()方法

json格式的发明人Douglas Crockford,提出了一个object()函数,可以做到这一点。

  function object(o) {

    function F() {}

    F.prototype = o;

    return new F();

  }

这个object()函数,其实只做一件事,就是把子对象的prototype属性,指向父对象,从而使得子对象与父对象连在一起。

使用的时候,第一步先在父对象的基础上,生成子对象:

  var Doctor = object(Chinese);

然后,再加上子对象本身的属性:

  Doctor.career = '医生';

这时,子对象已经继承了父对象的属性了。

  alert(Doctor.nation); //中国

三、浅拷贝

除了使用"prototype链"以外,还有另一种思路:把父对象的属性,全部拷贝给子对象,也能实现继承。

下面这个函数,就是在做拷贝:

  function extendCopy(p) {

    var c = {};

    for (var i in p) {
      c[i] = p[i];
    }

    c.uber = p;

    return c;
  }

使用的时候,这样写:

  var Doctor = extendCopy(Chinese);

  Doctor.career = '医生';

  alert(Doctor.nation); // 中国

但是,这样的拷贝有一个问题。那就是,如果父对象的属性等于数组或另一个对象,那么实际上,子对象获得的只是一个内存地址,而不是真正拷贝,因此存在父对象被篡改的可能。

请看,现在给Chinese添加一个"出生地"属性,它的值是一个数组。

  Chinese.birthPlaces = ['北京','上海','香港'];

通过extendCopy()函数,Doctor继承了Chinese。

  var Doctor = extendCopy(Chinese);

然后,我们为Doctor的"出生地"添加一个城市:

  Doctor.birthPlaces.push('厦门');

发生了什么事?Chinese的"出生地"也被改掉了!

  alert(Doctor.birthPlaces); //北京, 上海, 香港, 厦门

  alert(Chinese.birthPlaces); //北京, 上海, 香港, 厦门

所以,extendCopy()只是拷贝基本类型的数据,我们把这种拷贝叫做"浅拷贝"。这是早期jQuery实现继承的方式。

四、深拷贝

所谓"深拷贝",就是能够实现真正意义上的数组和对象的拷贝。它的实现并不难,只要递归调用"浅拷贝"就行了。

  function deepCopy(p, c) {

    var c = c || {};

    for (var i in p) {

      if (typeof p[i] === 'object') {

        c[i] = (p[i].constructor === Array) ? [] : {};

        deepCopy(p[i], c[i]);

      } else {

         c[i] = p[i];

      }
    }

    return c;
  }

使用的时候这样写:

  var Doctor = deepCopy(Chinese);

现在,给父对象加一个属性,值为数组。然后,在子对象上修改这个属性:

  Chinese.birthPlaces = ['北京','上海','香港'];

  Doctor.birthPlaces.push('厦门');

这时,父对象就不会受到影响了。

  alert(Doctor.birthPlaces); //北京, 上海, 香港, 厦门

  alert(Chinese.birthPlaces); //北京, 上海, 香港

目前,jQuery库使用的就是这种继承方法。

 

热门栏目