分类

Javascript面向对象编程(二):构造函数的继承

作者: 阮一峰

日期: 2010年5月23日

这个系列的第一部分,主要介绍了如何"封装"数据和方法,以及如何从原型对象生成实例。

今天要介绍的是,如何生成一个"继承"多个对象的实例。

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


  function Animal(){

    this.species = "动物";

  }

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


  function Cat(name,color){

    this.name = name;

    this.color = color;

  }

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

1. 构造函数绑定

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

  function Cat(name,color){

    Animal.apply(this, arguments);

    this.name = name;

    this.color = color;

  }

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

  alert(cat1.species); // 动物

2. 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 这个对象的constructor属性,是指向Cat的。

我们在前一步已经删除了这个prototype对象原来的值,所以新的prototype对象没有constructor属性,所以我们必须手动加上去,否则后面的"继承链"会出问题。这就是第二行的意思。

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

  o.prototype = {};

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

  o.prototype.constructor = o;

3. 直接继承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

4. 利用空对象作为中介

由于"直接继承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

5. prototype模式的封装函数

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

  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属性。这等于是在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。

6. 拷贝继承

上面是采用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); // 动物

未完,请继续阅读第三部分《非构造函数的继承》

(完)

功能链接

留言(31条)

关注很久了,第一次沙发,顺便带上自己的链接

我还是不知道为什么要做
o.prototype.constructor = o;
是为了维护正确的继承回朔链?来保证形如this.constructor.prototype.constructor.prototype....这类回朔的正确性吗?
那么是不是说如果程序本来就不打算回朔的话其实也就没必要加这个了?

引用RedNax的发言:

o.prototype.constructor = o;
是为了维护正确的继承回朔链?

基本上就是这个目的,还有就是instanceof运算符能返回正确的结果。

看这个系列,我应该可以温习一下javascript

在原型对象绑定中

Animal.apply(this, arguments);

这一句里面的arguments 好像应该是 Animal?

另外,我觉得第二种 prototype 的方式看上去比较简洁有效,后面的几种有其他的优点么?

很棒~学习了~
深入浅出,容易理解~

个人倾向于“5. prototype模式的封装函数”这个方法~觉得还蛮优雅的~

受教了。
第5种方法确实不错,构造函数应该是(根据参数)为实例添加特定成员而存在的。new出来作为prototype的话,父类的构造函数就已经跑过一次了,结果子类构造的时候如果必要还要在跑一次,就显得浪费了。
不过debug和用代码回朔的时候可能会比较麻烦,两个prototype中间会隔着一个空类。

啊,抱歉想错了。
第5种方法不会增加空类的,那个new F()和new Parent()地位一样,只是不跑构造函数而已。
好方法……

  function extend(Child, Parent) {

    var F = function(){};
//这里应该是new Parent()吧
    F.prototype = Parent.prototype;

    Child.prototype = new F();

    Child.prototype.constructor = Child;

    Child.uber = Parent.prototype;

  }

不错,支持!

 function extend(Child, Parent) {

    var F = function(){};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
    Child.uber = Parent.prototype;
  }

这个好像parent必须实现prototype方法,

引用RedNax的发言:

啊,抱歉想错了。
第5种方法不会增加空类的,那个new F()和new Parent()地位一样,只是不跑构造函数而已。
好方法……

new F() 这里不还是 new 出一个实例么, 按作者的意思, 这里因为是 new 一个空对象, 资源消耗相对较小.

最后一种通过拷贝的方法来实现继承是有问题的. 这里博主用的是浅拷贝的方法, c[i] 是指向到 p[i], 而非赋值, 这样如果改动到 c[i], 会影响到 p[i]. 例如:

function fn1() {};
function fn2() {};
fn2.prototype.arr = [1,2];
var c = fn1.prototype, p = fn2.prototype;
for(var i in p) {c[i] = p[i]};
fn1.prototype.arr.push(3);
fn2.prototype.arr // [1,2,3]

建议用深拷贝的方法来实现:

(function(o) {
if(typeof o != "object") return o;
var obj = {};
for(var p in o) {
obj[p] = arguments.callee(o[p]);
return obj;
}
})();

不好意思, 上面这样写当遇到数组时是不行的, 参考峰兄的代码, 修改了一下:

var obj = (o.construcotr === Array)?[]:();

期待峰兄能写一本js的书籍出来,现在市面上即便是翻译的国外的书籍,依然是有些晦涩难懂。但是感觉看峰兄的描述,感觉像在读一个故事,很容易理顺。

引用Ruan YiFeng的发言:

基本上就是这个目的,还有就是instanceof运算符能返回正确的结果。

将Cat的prototype对象指向一个Animal的实例(它相当于完全删除了Cat prototype对象原先的所有值,然后赋予它新的值,新值即Animal的属性,而Animal它自己本身有constructor属性,所以说Cat又继承得到了constructor,能否这样理解呢??

看书跟看你的文章还是有豁然开朗的感觉啊

5.prototype模式的封装函数

不知是作者的表达有误,还是确实没有经过测验,

按第五种的模式,父级是必须要实现prototype方法,相当于公共方法,可供子级访问

或重写!
extend(Cat,Animal);
var cat1 = new Cat("大毛","黄色");
alert(cat1.species); // 动物。。。。。。实际会弹出undefined!

不过还是感谢作者的阐述!!

大侠们,我也想深入学习下js面向对象思想,谁给推介本书啊。

感觉把面向对象的东西在js上面实现一遍, 增大代码了维护的难度, 而不是减小了维护的难度. 正在思索, 这么做是不是有必要呢?

js的面向对象写法是在是有点不好选择。在平时的项目中我还是倾向于函数式编程

我们在前一步已经删除了这个prototype对象原来的值,所以新的prototype对象没有constructor属性,所以我们必须手动加上去,否则后面的"继承链"会出问题。这就是第二行的意思。

“所以新的prototype对象没有constructor属性”这句应该不对吧,Animal的实例也是有constuctor属性的,指向Animal,所以
Cat.prototype = new Animal();
这时的constructor应该是Animal

你好,我想问一下,关于第5种继承方法。子类继承了父类的数据和prototype方法,但之后子类貌似无法再使用自己的prototype方法了,因为prototype方法在extend()函数中已经被改写了。
莫非是父类使用prototype方法后,子类就无法使用?难道这种情况下,子类只能通过this添加新方法?这样的话我new多个子类就会造成代码重复吧?求解答。

function Parent() {
this.a = 'a';
}
Parent.prototype = {
print: function() {
alert(this.a);
}
}
function Child() {
Parent.apply(this, arguments);
}
Child.prototype = {
print2: function() {
alert('d');
}
}
extend(Child, Parent);
var obj = new Child();
obj.print();//输出'a'
obj.print2();//报错,找不到function

Douglas Crockford的专著《Javascript语言精粹》这本书没被你翻译真是一大悲剧,这段时间网上下了个《Javascript语言精粹》PDF电子版的感觉翻译的人翻译的真的挺差。

Child.uber = Parent.prototype;
这样写有什么好处... 不是很理解

引用Jun的发言:

Child.uber = Parent.prototype;
这样写有什么好处...不是很理解

因为当请求一个方法时,先会从本地对象搜索,没有的话,就遵从原型链寻找,直到最后对象是Object时还找不到就会返回undefined。 当知道属性是继承而来的,用uber方法调用比普通调用效率高。(少了一次遍历过程)

function Animal() {
this.species = "动物";
}
function Cat(name, color) {
this.name = name;
this.color = color;
}
function extend(Child, Parent) {
var F = function() { };
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
}
extend(Cat, Animal);
var cat1 = new Cat("大毛", "黄色");
alert(cat1.species); // 结果是undefined\
取不到"动物" 值

这是另一个教程里面的例子,没有Child.prototype.constructor=Child,但后面看到依然是Child的实例。。。为什么会这样?
http://ejohn.org/apps/learn/#76

function Person(){}
Person.prototype.dance = function(){};

function Ninja(){}

// Achieve similar, but non-inheritable, results
Ninja.prototype = Person.prototype;
Ninja.prototype = { dance: Person.prototype.dance };

assert( (new Ninja()) instanceof Person, "Will fail with bad prototype chain." );

// Only this maintains the prototype chain
Ninja.prototype = new Person();

var ninja = new Ninja();
assert( ninja instanceof Ninja, "ninja receives functionality from the Ninja prototype" );
assert( ninja instanceof Person, "... and the Person prototype" );
assert( ninja instanceof Object, "... and the Object prototype" );

引用dindog的发言:


因为当请求一个方法时,先会从本地对象搜索,没有的话,就遵从原型链寻找,直到最后对象是Object时还找不到就会返回undefined。
当知道属性是继承而来的,用uber方法调用比普通调用效率高。(少了一次遍历过程)

除了上面说的,想想,因为子对象当另外定义了同名方法时,要调用父对象的方法,也只能通过这样(Firefox下支持非标准方法__proto__去调用上级对象)

弱弱的问一句,第五种方法明显有问题。但怎么改呢?我是初学者不知道,求教啊!

引用Jun的发言:

取不到"动物" 值

原因在这里:
function Animal() {
this.species = "动物";
}

应该写为:
function Animal(){}
Animal.prototype.speies="动物";


你理解错了作者第五种方法的意思,第五种是是从第三种和第四种方法延伸而来的。

我要发表看法

«-必填

«-必填,不公开

«-我信任你,不会填写广告链接