Javascript 继承-原型的陷阱

注:本文为翻译文章,原文为"JavaScript Inheritance – How To Shoot Yourself In the Foot With Prototypes!"javascript

在学习javascript的过程当中,许多新手发现很难弄明白javascript复杂的的原型继承工做机制。在这篇文章中我谈谈在经过父函数的原型继承模型中如何实现实例属性。java

一个简单的Widget 对象

在下面的代码中,咱们有个一父类 Widget,父类有个属性 messages和父类为Widget的SubWidget类。在这种状况下咱们想让SubWidget的每一个实例在初始化的时候一个空的消息数组:web

var Widget = function( name ){
   this.messages = [];
};

Widget.prototype.type='Widget';

var SubWidget = function( name ){
  this.name = name;
  Widget.apply( this, Array.prototype.slice.call( arguments ) );
};

SubWidget.prototype = new Widget();

在咱们设置SubWidget 的原型为Widget的一个实例以前,对象的关系图以下:编程

代码最后一行将SubWidget的父类设置为Widget类的一个实例,"new"关键字背后,建立了继承树而且绑定了对象的原型链,如今咱们的对象关系图看起来像下面这样:数组

你看出问题所在了吗?让咱们建立子类的实例凸显问题:app

var sub1 = new SubWidget( 'foo' );
var sub2 = new SubWidget( 'bar' );

sub1.messages.push( 'foo' ); 
sub2.messages.push( 'bar' );

如今咱们的对象关系图看起来像这样:函数

在谈论真正的问题以前,我想一想退一步,先谈谈widget构造函数中的属性(type),若是在实例初始化过程当中没有初始化属性(type)那实际上这个属性存在widget构造函数中(实际上存在wedget的实例中,也就是subwidget实例的原型中)。然而,一旦在(子类实例)初始化过程当中属性被赋予新值,如 sub1.type = 'Fuzzy Bunny'它将变成实例的属性,如图所示:学习

思考问题

咱们的bug开始变得很清晰,让咱们输出sub1和sub2的messages数组:this

var Widget = function(){
   this.messages = [];
};

Widget.prototype.type='Widget';

var SubWidget = function( name ){
  this.name = name;
};

SubWidget.prototype = new Widget();

var sub1 = new SubWidget( 'foo' );
var sub2 = new SubWidget( 'bar' );

sub1.messages.push( 'foo' ); 
sub2.messages.push( 'bar' );

console.log( sub1.messages ); //[ 'foo', 'bar' ]
console.log( sub2.messages ); //[ 'foo', 'bar' ]

若是你运行这段代码,在你的控制台将出现2个重复 ["foo", "bar"]。每一个对象共享相同的messages数组。spa

解决问题

最容易想到的办法,咱们能够给SubWidget构造函数添加新属性,以下所示:

var SubWidget = function( name ){
  this.name = name;
  this.messages = [];
};

然而,若是咱们想建立其余继承自Widget的对象呢?新对象也要添加消息数组。很快维护和扩展咱们的代码将变成一场噩梦。另外,若是咱们想给Widget构造函数添加其余属性,咱们如何将这些属性编程子类的实例属性?这种方法是不可重用的和不够灵活。

为了妥善解决这个问题,须要给咱们的SubWidget构造函数添加一行代码,调用Widget构造函数而且传入SubWidget构造函数的做用域。为此咱们要用apply()方法,能够灵活的无反作用的将SubWidget构造函数的arguments传入Widget构造函数中。

var Widget = function(){
   this.messages = [];
};

Widget.prototype.type='Widget';

var SubWidget = function( name ){

  this.name = name;

  Widget.apply( this, Array.prototype.slice.call(arguments) );
};

SubWidget.prototype = new Widget();

apply()方法可让咱们能够将messages数字的做用域更改成SubWidget的实例。如今咱们建立的每个实例对象都有一个实例messages 数组。

var Widget = function( ){
   this.messages = [];
};

Widget.prototype.type='Widget';

var SubWidget = function( name ){

  this.name = name;
  Widget.apply( this, Array.prototype.slice.call( arguments ) );
};

SubWidget.prototype = new Widget();

var sub1 = new SubWidget( 'foo' );
var sub2 = new SubWidget( 'bar' );

sub1.messages.push( 'foo' );

sub2.messages.push( 'bar' );

console.log(sub1.messages); // ['foo']
console.log(sub2.messages); // ['bar']

运行上面的代码,你将看见 ["foo"] 和 ["bar"] ,由于咱们的对象实例如今有本身的messages数组属性。

如今咱们的对象关系图以下:

译者补充

上面的继承方式是借用构造函数模式,《javascript patterns》中有详细介绍,做者写的很详细了,但有2个小问题在此补充:

1

var SubWidget = function( name ){

  this.name = name;
  Widget.apply( this, Array.prototype.slice.call( arguments ) );
};

做者的代码中父类会覆盖子类的属性,这有悖于重构的概念,稍加改变便可,在子类构造函数中先调用父类构造函数,这至关于java中的super:

var SubWidget = function( name ){
  Widget.apply( this, Array.prototype.slice.call( arguments ) );
  this.name = name;
};

2

var SubWidget = function( name ){

  this.name = name;
  Widget.apply( this, Array.prototype.slice.call( arguments ) );
};

SubWidget.prototype = new Widget();

父类的属性被初始化了2次,一次是借用构造函数,一次是new Widget(),形成浪费,稍加改变便可:

var SubWidget = function( name ){

  this.name = name;
  Widget.apply( this, Array.prototype.slice.call( arguments ) );
};

SubWidget.prototype = Widget.prototype;

若是你以为我翻译的不错,能够在这里关注个人微博

相关文章
相关标签/搜索