JavaScript中的类式继承和原型式继承

时间:2022-08-24 23:08:58

  最近在看《JavaScript设计模式》这本书,虽然内容比较晦涩,但是细品才发现此书内容的强大。刚看完第四章--继承,来做下笔记。

  书中介绍了三种继承方式,类式继承、原型式继承和掺元类继承。类式继承和原型式继承用的比较多,最后一种更像是一种类共享和扩展。本文主要讨论前两者。其实就是讨论如何让一个child对象去继承parent对象的属性和方法。

  1. 类式继承
function Parent(name){
    this.name = name;
    this.sex = "male";
}

Parent.prototype.getName = function(){
    alert(this.name);
}

Parent.prototype.show = function(){
    alert("I'm here.");
}

function Child(age){
    this.age = age;
}

在这里我们看到parent类有name和sex属性,并且有getName方法和show方法,而child类只有age属性。parent和child都是构造函数。

让child具有parent的属性,我们可以用apply或者call来把parent的构造函数绑定到child上,如下:

function Child(age){
    this.age = age;
    Parent.call(this,"Jim")
}

var c = new Child(12);
console.log(c.name)  //Jim
console.log(c.sex)   //male

但是这种方法只是继承了parent的构造函数,并没有得到parent.prototype上定义的方法,因此c.getName()和c.show()会提示c没有该方法。

当然上面提到的方法并不是类式继承,接下来我们看看怎样用prototype模式来实现类式继承。

function Child(age){
    this.age = age;
    Parent.call(this,"Jim");
}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

var c = new Child(12);

通过prototype来继承,能继承父类所有的属性和方法。当然如果没有

Parent.call(this,"Jim")

这句话,虽然c继承了parent的name属性和getName方法,但是返回结果是undefined哦,不过确实是继承到了。

我们把child.prototype作为parent的一个实例,也就是改写了child的prototype对象,任何一个prototype对象都有一个constructor属性,指向它的构造函数。也就是说,child.prototype 这个对象的constructor属性,是指向child的。在这里我们改写了这个prototype对象原来的值,所以新的prototype对象没有constructor属性,需要我们手动加上去,否则后面的”继承链”会出问题。

还有一种方法就是创建一个空对象作为中介来实现,不过这个方法有个问题,请看

function Child(age){
    this.age = age;
}
function F(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
var c = new Child(12);

这里的F()就是我们创建的用来作为中介的空对象,我们来测试下

console.log(c.sex)   //undefined
c.show()    // "I'm here."

问题就在这里,这种方法只继承到了父类原型上的属性和方法,而没有继承父类的构造函数,更别说拥有父类的属性,比如sex。

不过解决方法很简单,只要用到我们前面提到的call或者apply把parent的构造函数绑定到child上就可以了,因此只要在child()构造函数里加上

Parent.call(this,"Jim")

为了简化,我们可以结合apply或者call和空对象的方法,把他们封装为一个extend函数

function extend(subClass, superClass) {
  var F = function() {};
  F.prototype = superClass.prototype;
  subClass.prototype = new F();
  subClass.prototype.constructor = subClass;
}

我们只要这样写 extend(Child,Parent),并且在Child()构造函数中绑定Parent的构造函数即可。不过麻烦的是如果child不想继承parent了,想要继承uncle类了,我们又得修改child构造函数中的代码,把

Parent.call(this,"Jim")

修改为

Uncle.call(this,"Jim")

为解决这个问题,书中把extend函数就行了改进

function extend(subClass, superClass) {
  var F = function() {};
  F.prototype = superClass.prototype;
  subClass.prototype = new F();
  subClass.prototype.constructor = subClass;

  subClass.superclass = superClass.prototype;
  if(superClass.prototype.constructor == Object.prototype.constructor) {  //确保constructor属性正确设置
    superClass.prototype.constructor = superClass;
  }
}

然后在子类child中这样写

Child.superclass.constructor.call(this, “Jim”);

当然那个讨厌的Jim你可以换成name,并通过Child构造函数的实参传入。或者直接换成apply(this,arguments),你只要传入参数即可。

好了,类式继承大概就是这个意思了。其实这个名字我觉得奇怪,而且经常把这些个方法误认为原型式继承,毕竟处处离不开prototype。大概是这种方法比较类似于其他面向对象编程语言的类继承吧。

  2.  原型式继承

  那么原型式继承是怎样的呢?我们先来总结下类式继承的要点:1.首先用一个类的声明定义对象的结构,其实就是构造函数啦;2.接下来实例化改类创建一个新的对象。用这种方式创建的对象都有一套该类的所有实例属性的副本,每一个实例方法都只存在一份,但每个对象都有一个指向它的连接。试想如果该类有很多实例,每个实例都有个各自的属性副本,那得多占用内存啊。

  使用原型式继承的时候我们不需要创建构造函数来定义对象的结构,我们直接创建一个对象,并把需要让子对象继承的属性和方法都写到这个对象里面,这个对象称为原型对象,然后通过一个clone函数让我们的子对象继承这个原型对象。

function clone(object) {
    function F() {}
    F.prototype = object;
    return new F;
}

var Parent = {
    name: "Jim",
    sex: "male",
    getName : function(){
        return this.name;
    }
}

var c = clone(Parent);

书中用clone这个名字我觉得不大合适,毕竟这并没有对原型对象进行拷贝,只是将prototype指向了原型对象,从而通过原型链获得了原型对象的属性和方法。打个比方,一个A对象没有什么权利,但是她找到了干爹,也就是原型对象。这个找干爹的过程就是clone函数中的F.prototype = object。这样A有了靠山,下次自己本身没有能力办到的事情,就可以找她干爹也就是原型对象办了。当然这个比喻有点不搭恰当。总之通过将一个对象的prototype指向某个原型对象,使一个对象拥有了原型对象的属性和方法。因为自身没有的属性和方法,都可以通过原型链去原型对象上找,当然继承之后该对象也可以定义自己的属性和方法,同时也可以对继承来的属性和方法进行改写,成为自己独有的属性和方法。

console.log(c.name);    //Jim
c.name = "Tom";
c.getName();   //Tom

可以发现原型式继承比类式继承要更为简洁,不过该方法有一个问题,那就是读和写的不对等性。var c = clone(Parent)实际上只是创建了一个空对象{},只不过它的prototype指向原型对象而已。因此在我们第一次console.log(c.name)的时候,只是通过原型链去去了原型对象的name属性,而且之后c.name = "Tom"这句话并不是改写从原型对象继承过来的name属性,而是创建了一个自身的新的name属性,我们运行一下console.log(c)看看,得到如下信息

F {age: 12, name: "Tom", name: "Jim", sex: "male", getName: function}
    name: "Tom"
    __proto__: Object
        getName: function (){}
        name: "Jim"
        sex: "male"            

看到有两个name属性,一个是c自身的,一个是原型对象上的。此时c.name是Tom,因为自己已有该属性,不必去原型对象上找。那么此时的c.getName()是多少呢,是Tom哦,因为c拥有了getName方法,而该方法中的this是指向调用该方法的对象的,这里是c,所以是Tom。

所有这里有一个更坑爹问题就是,当原型对象包含数组和对象类型成员的时候,直接看代码吧。

var Parent = {
    name: "Jim",
    sex: "male",
    hobby: ["sports"],
    children:{son:"Jack",daughter:"Lily"},
    getName : function(){
        return this.name;
    }
}

var c = clone(Parent);
c.hobby.push("game");
c.children.son = "James";

console.log(Parent.hobby);   //["sports","game"]
console.log(Parent.children.son);   //"James"

看到了吧,Parent被篡改了。不难理解,因为刚创建的c本身没有hobby和children,只能去改原型对象了。所以要想只修改c上的hobby和children,必须得先创建副本。

var c = clone(Parent);
c.hobby = [];
c.children = {son:"Jack",daughter:"Lily"};
c.hobby.push("game");
c.children.son = "James";

console.log(Parent.hobby);   //["sports"]
console.log(Parent.children.son); //"Jack"
console.log(c.hobby); //["game"]
console.log(c.children.son); //"James"

不过这样的话就要在继承数组和成员对象的的时候就要知道数组和成员对象的结构和原始值,的确比较麻烦。书中也给出了解决方法,大概思路就是通过原型对象中的方法来创建数组或是对象,这样c只要调用原型对象的方法就可以继承数组或是成员对象。

  3. 类式继承和原型式继承的比较

  两种方法都可以很好的达到继承的效果。相比之下,类式继承可能更适合面向对象的编程习惯,而原型式继承是javascript语言特有的,理解起来比较奇怪,尤其是前面提到的读写性不对等,子类的修改甚至会影响到父类和其他继承该父类的子类等。不过原型式继承更为简洁,同时在内存使用方面更为节约,因为都是通过原型对象共享属性和方法的么,不像类式继承中每个实例都有一个副本,比较占用内存。总之两者继承方式各有利弊,怎么取舍看情况而定。

  好了,在博客园的第一篇博文总算码完了,水平有限,如有纰漏还希望大家之中交流。

参考资料:

1.重温Javascript继承机制 http://www.w3cfuns.com/forum.php?mod=viewthread&tid=1086&fromuid=5402010


2.JavaScript设计模式(Ross Harmes / Dustin Diaz) 第四章--继承