在JavaScript中从经典继承切换到原型继承:模式的变更

4
有着Java背景的我在转向JavaScript时,懒惰地试图坚持我所了解的面向对象编程方式,即经典继承。我正在开发一个Web应用程序(由我自己制作),并使用这种类型的继承。然而,我考虑改变我的代码,并重写OOP部分以进行原型继承(两个原因:我读到很多人说它更好,其次,我需要尝试另一种方式以更好地理解它)。
我的应用程序创建数据可视化(使用D3js,但这不是主题),并按以下方式组织我的代码:
function SVGBuilder( dataset ) {
  this.data = dataset;
  this.xCoord;
  this.startDisplaying = function() {
    // stuff
    this.displayElement();
  }
}

displayElement()方法在继承自SVGBuilder的“类”中定义(SVGBuilder更或多或少是一个抽象类)。然后我有:

function SpiralBuilder( dataset ) {
  SVGBuilder.call( this, dataset );
  this.displayElement = function() {
    // stuff
  };
}
SpiralBuilder.inheritsFrom( SVGBuilder );

我有几个基于相同结构的“构建器”。
调用构建器的脚本看起来像这样(它更加复杂,因为它根据用户输入选择正确的构造函数):
var builder = new SpiralBuilder( data );
builder.startDisplaying();

现在是“转换部分”。我阅读了很多相关的内容,从Douglas Crockford的文章,到《Eloquent Javascript》的部分内容。在Aadit M Shah评论中,他提出了一个结构,看起来像这样:
var svgBuilder = {
  data: [],
  xCoord: 0,  // ?
  create: function( dataset ) {
    var svgBuilder= Object.create(this);
    svgBuilder.data = dataset;
    return svgBuilder;
  },
  startDisplaying: function() {
    // stuff
  }
}

然而,此时我卡住了。首先(技术问题),我能否在这种模式下声明变量(data、xCoord)而不初始化它们?就像 this.data; 这样的方式?其次,我应该如何创建继承关系?我只需手动将相应函数放入原型中吗?类似这样的操作:
var spiralBuilder = builder.create( dataset );
spiralBuilder.prototype.displayElements = function() {
  // Code to display the elements of a spiral
};
spiralBuilder.displayElements();

如果我理解正确的话,这意味着在调用构建器的脚本中,我将不再选择正确的构造函数(因为它不再存在),而是需要添加/修改单个构建器实例的原型中的方法。这就是应该做的吗?
还是说我应该尝试以完全不同的方式设计我的代码?如果是这样的话,你能给我一些建议/参考资料吗?

在svgBuilder中,成员xCoord表明它应该是一个实例成员(对于您创建的每个实例都是唯一的),但它会将其添加到原型上,因此它是共享的。如果这个成员是基本类型(数字、字符串、布尔值),那么它是不可变的,你只能通过重新分配来改变它,所以它基本上创建了一个多余的变量。如果这个成员是一个数组,那么你就有麻烦了aInstance.someArray.push("fromA"); bInstance.someArray将会是["fromA"] - HMR
有一些构造器和辅助函数可以让你在创建实例时混合继承。这有点违背了面向对象编程的初衷,因为当某个东西发生变化时,你的代码中有很多地方需要被检查,以适应对象的变化。如果你理解构造函数的基本原理,那么使用工厂方法可能比让创建实例的代码决定对象类型更好。我已经更新了我的答案,以回答你关于svgBuilder中实例变量的问题。 - HMR
1个回答

1
我能在这种模式下声明变量(data, xCoord)而不初始化吗?
var svgBuilder = {
  //removed data here as it's only going to shadowed
  // on creation, defaults on prototype can be done
  // if value is immutable and it's usually not shadowed later
  create: function( dataset, xcoord ) {
    var svgBuilder= Object.create(this);
    svgBuilder.data = dataset;//instance variable
    svgBuilder.xcoord = xcoord;//instance variable
    return svgBuilder;
  },
  startDisplaying: function() {
    // stuff
  },
  constructor : svgBuilder.create
};

我知道在我的示例中很少这样做,但是创建实例或调用函数时,通常最好传递参数对象。

在某个时刻,您可能会在此处或那里更改某些内容,而您不希望在代码中更改许多地方。

在前几个示例中,您根本没有使用原型。每个成员都在构造函数中声明为this.something,因此是特定于实例的成员。

可以使用构建器,但是当您熟悉声明构造函数、原型、混合和可能的静态成员时,您所需的只是继承和混合的辅助函数。

有关原型的介绍可以在这里找到。它还介绍了继承、混合、覆盖、调用super和this变量。以下是介绍的副本:

构造函数介绍

您可以使用函数作为构造函数来创建对象,如果构造函数命名为Person,则使用该构造函数创建的对象是Person的实例。

var Person = function(name){
  this.name = name;
};
Person.prototype.walk=function(){
  this.step().step().step();
};
var bob = new Person("Bob");

Person是构造函数,因为它是一个对象(像JavaScript中的大多数其他内容一样),您也可以像这样给它添加属性:Person.static="something",这对于与Person相关的静态成员非常有用,例如:

 Person.HOMETOWN=22;
 var ben = new Person("Ben");
 ben.set(Person.HOMETOWN,"NY");
 ben.get(Person.HOMETOWN);//generic get function what do you thing it'll get for ben?
 ben.get(22);//maybe gets the same thing but difficult to guess

当您使用Person创建实例时,必须使用new关键字:
var bob = new Person("Bob");console.log(bob.name);//=Bob
var ben = new Person("Ben");console.log(bob.name);//=Ben

属性/成员name是实例特定的,对于Bob和Ben来说是不同的。

成员walk是所有实例共享的,Bob和Ben都是Person类的实例,因此他们共享walk成员(bob.walk === ben.walk)。

bob.walk();ben.walk();

因为JavaScript在bob对象中找不到walk()函数,所以它会在Person.prototype中查找,因为这是bob的构造函数。如果在那里找不到它,它就会在Function.prototype上查找,因为Person的构造函数是Function。Function的构造函数是Object,所以它最后会在Object.prototype上查找。这被称为原型链。
尽管bob、ben和所有其他创建的Person实例共享walk函数,但由于在walk函数中使用了this,函数将根据每个实例而表现出不同的行为;现在假设它是当前实例,因此对于bob.walk(),“this”将是bob。(稍后我们将详细讨论“this”和调用对象)。
如果ben正在等红灯,而bob在绿灯下,则你会在ben和bob上同时调用walk(),显然会发生不同的事情。
当我们执行 ben.walk=22 这样的语句时,就会出现成员遮蔽的情况,尽管 bob 和 ben 共享 walk,但是将 22 赋值给 ben.walk 不会影响 bob.walk。这是因为该语句会在 ben 上直接创建一个名为 walk 的成员,并将其赋值为 22。这样就会有两个不同的 walk 成员:ben.walk 和 Person.prototype.walk。
当请求 bob.walk 时,会得到 Person.prototype.walk 函数,因为在 bob 上找不到 walk。然而,请求 ben.walk 将得到值 22,因为 walk 成员已经在 ben 上被创建了,JavaScript 在 ben 上找到了 walk,因此不会查找 Person.prototype。
因此,成员的赋值会导致 JavaScript 不会在原型链中查找它并分配值。相反,它会将值分配给对象实例的已存在成员或创建成员,然后将值分配给它。 下一部分(更多关于原型的内容)将通过示例代码来解释这一点,并演示如何继承。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接