【问题标题】:How to implement classical class inheritance through prototypes如何通过原型实现经典的类继承
【发布时间】:2015-12-18 15:57:28
【问题描述】:

我想在 JS 中实现以下行为。 请注意语法是象征性的。

这是我的父类

class = TList {
   FList: array;

   function AddElement(Ele) {
        Flist.Add(Ele)
   };

   function RemoveEle(Ele) {
        FList.Remove(Ele)
   };   
}

现在我要继承这个类。我的子类应该自动具有父类的所有属性,并且应该能够在不重写代码的情况下扩展它们。

class = TAlertList(inherit from TList) {

   function AddElement(Ele) {
      Alert('element will be added');
      call.parent.AddElement(Ele)
   };

   function RemoveElement(Ele) {
     call.parent.RemoveElement(Ele);
     Alert('element removed');
   }

}

请注意我如何在我希望的地方继承父方法。

现在我应该能够从我的子类创建一个对象并执行以下操作。

MyAlertList = new TAlertList;
MyAlertList.Add('hello');
console.log(MyAlertList.FList);

我应该能够从 TAlertList 继承更多的子类并能够改变现有的行为。我需要在纯 ES5 中执行此操作而不使用任何库。标准的 OOP 实践是预期的。

【问题讨论】:

  • 这看起来不像 javascript,你用的是什么转译器吗?
  • 请注意语法是符号的。任何做过 oop 的人都应该能够通过这个符号代码理解我需要在 JS 中实现什么。
  • 啊,第二句话好像略过。

标签: javascript class inheritance prototype


【解决方案1】:

请注意TList 构造函数应该应用于TAlertList 实例;

ES5,先设置基础构造函数

function TList() {
    this.Flist = [];
    // ...
}

TList.prototype = {
    constructor: TList,
    AddElement: function AddElement(Ele) {
        this.Flist.push(Ele);
    },
    RemoveEle: function RemoveEle(Ele) {
        var i = this.Flist.lastIndexOf(Ele);
        if (i !== -1)
            this.Flist.splice(i, 1);
    }
};

接下来设置扩展它的构造函数,看看这意味着如何在由扩展构造函数创建的实例上调用基础构造函数并创建一个继承基础构造函数原型的原型对象

function TAlertList() {
    // construct from base
    TList.call(this);
    // further construct
    // ...
}
TAlertList.prototype = Object.create(TList.prototype);
TAlertList.prototype.constructor = TAlertList;

// depending on how you want to reference stuff
TAlertList.prototype.AddElement = function AddElement(Ele) {
    alert('element will be added');
    TList.prototype.AddElement.call(this, Ele);
};
TAlertList.prototype.RemoveElement = function RemoveElement(Ele) {
    TList.prototype.RemoveEle.call(this, Ele);
    alert('element removed');
};

ES6 语法使用super 关键字

class TList {
    constructor() {
        this.FList = [];
    }
    AddElement(Ele) {
        this.Flist.push(Ele);
    }
    RemoveEle(Ele) {
        var i = this.Flist.lastIndexOf(Ele);
        if (i !== -1)
            this.Flist.splice(i, 1);
    }
}

class TAlertList extends TList {
    constructor() {
        super();
    }
    AddElement(Ele) {
        alert('element will be added');
        super.AddElement(Ele);
    }
    RemoveElement(Ele) {
        super.RemoveEle(Ele);
        alert('element removed');
    }
}

回到 ES5,概括为一个工厂,这样你就可以看到一种如何做到这一点的算法

function extend(baseConstructor, extendedConstructor, prototypeLayer) {
    function Constructor() {
        var i = 0, j = 0, args = Array.prototype.slice.call(arguments);

        i = j, j += baseConstructor.length;
        baseConstructor.apply(this, args.slice(i, j));

        i = j, j = args.length;
        extendedConstructor.apply(this, args.slice(i, j));
    }
    Object.defineProperty(Constructor, 'length', { // fix .length
        value: baseConstructor.length + extendedConstructor.length,
        configurable: true
    });
    Constructor.prototype = Object.create(baseConstructor.prototype);
    Constructor.prototype.constructor = Constructor;
    Object.assign(Constructor.prototype, prototypeLayer);
    return Constructor;
}

那么

function Foo(x) {this.foo = x;}
Foo.prototype.fizz = 1;
var Bar = extend(Foo, function (x) {this.bar = x;}, {buzz: 1});
// ...
var b = new Bar('foo', 'bar');
b.foo; // "foo"
b.bar; // "bar"
b instanceof Foo; // true
b instanceof Bar; // true
b.fizz; // 1
b.buzz; // 1

请注意,这是您在编写每个扩展构造函数时应该遵循的算法示例,而不是生产代码

【讨论】:

  • 感谢您的详细解释。我最初将其发布为问答,虽然我有点晚了。请看我的回答并告诉我你的想法。 stackoverflow.com/a/34420032/4185234
  • 虽然我接受了这个作为答案,但我发现这种方法存在很大的可扩展性问题。您将不得不更改所有下线对象以在层次结构中间引入一个新对象。这意味着这不是非常面向生产的解决方案。但是感谢您的宝贵时间。
  • @CharlieH 确实,使用这种方法更改中间的构造函数可能很乏味,但这是非常不寻常的行为;如果您要这样做,请经常考虑是否正确排序了继承 - 链中最旧的应该是最通用的,而链中最新的应该是最具体的。
【解决方案2】:

您的代码如下

function TList(){
  this.FList = [];
}

TList.prototype.AddElement = function(Ele){
 this.FList.push(Ele);
}
TList.prototype.RemoveElement = function(Ele){
 this.FList.splice(Ele,1); //Ele is the index to remove;
}

这是了解继承在 JavaScript 中如何工作的近似值。

function TAlertList (){
     TList.call(this);
}

TAlertList.prototype = Object.create(TList.prototype);
TAlertList.prototype.constructor = TAlertList;

TAlertList.prototype.AddElement = function(ele){
   alert('Element will be added');
   TList.prototype.AddElement.call(this,ele);
};
TAlertList.prototype.RemoveElement = function(ele){
   alert('Element will be remove');
   TList.prototype.RemoveElement.call(this,ele);
};

所以,经典的超级调用是

ParentClass.prototype.myMethod.call(this,args);

【讨论】:

  • 您的答案不包含任何继承代码。请更新。
  • 请注意您没有正确构建TAlertList 实例; 'FList' in (new TAlertList); // false
  • 请看我的回答,告诉我你的想法。
【解决方案3】:

这是问答

编辑 - 如果您打算阅读全文,请不要忘记阅读 @paul 的评论。

几乎所有的答案都基于 MDN 文档中关于 JS OOP 的流行“Person”示例。

这个方法背后的理论是把对象的字段放在一个构造函数中,同时在一个原型对象中实现方法。子对象可以通过使用人为的this 值调用构造函数来继承所有字段。它也可以继承所有的方法,方法是让父对象的原型对象与它的原型对象相同。唯一的规则是在实现继承时,您需要使用callapply 调用方法以将方法 指向正确的this 对象。

我不喜欢这种方法有两个原因。

  1. 对象的字段方法必须在两个对象之间分开(字段 - 构造函数,方法 - 原型)。这没有独特实体的独特行为的味道——它应该是一个单一的类。

  2. 继承时必须指定父对象的名称。这不是自动的。假设对象 C 继承自对象 A。所以,在对象C的方法中,你需要在继承对象A时提及它。 (TList.prototype.AddElement.call(this, Ele);) 如果对象 B 稍后出现在两者之间怎么办?您将不得不更改 C 的所有继承方法以从 B 调用。这绝不是好的继承。


我想在 MDN 方法中克服这些问题。我想出了以下模型,没有this 没有call 也没有apply。代码简单易懂。从父对象继承时,您不必提及父对象的名称。所以,一个新的类可以随时在父母和孩子之间加入,而不需要太多的改变。请对此进行讨论并指出该模型的弱点。

这是返回父对象的函数。

function tList() {

   var ret = Object.create(null);

   ret.list = [];

   ret.addElement = fucntion(ele) {
      ret.list.push(ele)
   };

   return ret;

}


//You can create tList object like this: 
var myList = tList();
myList.addElement('foo');

现在是子对象:

function tAlertList() {

   var ret = Object.create(tList());

   //Lets inherit with the fashion of overriding a virtual method
   ret.addElement = function(ele) {

      //Here is new code
      alert('Adding element ' + ele);

      //Automatic inheritance now
      Object.getPrototypeOf(ret).addElement(ele);
   }

   return ret;
}

//Just create the child object and use it
var myAlertList = tAlertList();
myAlertList.addElement('buzz') ;

您可以拥有大子对象并从父对象继承而无需提及他们的名字。假设您必须将tCustomList 放在tListtAlertList 之间。您所要做的就是在一个地方告诉tAlertListtCustomList 继承。 (var ret = Object.create(tCustomList());) 其他一切都保持不变。

这里是fiddle

【讨论】:

  • 这是一种有效的方法,但是您丢失了用于类型检查的instanceof 运算符,并且在内存中重新初始化和复制方法会产生一些额外的开销。不过,我没有看到像您所做的那样竭尽全力避免.call/.apply 的好处。考虑一下如果tAlertList 调用了ret.list = ['new list']; 然后你调用了.addElement 会发生什么,你现在遇到了一个奇怪的情况,.addElement 没有看到预期的列表。这可能会导致难以发现的错误。
  • 通过这样做,您的所有实例都将创建一个额外的对象,因为每个实例都有不同的原型。 list 属性不属于实例,而是属于它的原型。更不用说你的工厂方法会产生独特的函数实例而不是链接它们,tAlertList().addElement !== tAlertList().addElement
  • 哦,好的。我懂了。方法重复在经典 OOP 中被认为是“正常的”,因为编译器可以在后台管理实例。但这是一个不同的故事。是的。
  • 其他模型的可扩展性问题如何?如果您正在处理一个项目,该项目具有一系列在层次结构中相互继承的类,并且该项目容易在两者之间引入更多类(或删除),那么您对创建更改父类名称的要求有何建议在所有方法中?
【解决方案4】:

使用纯 ES5,你可以这样做:

function TList() {
   this.FList = []; //not really ideal.
}

TList.prototype.addElement = function(ele) {
   this.FList.push(ele);
};

TList.prototype.removeElement = function(ele) {
   this.FList.splice(this.FList.indexOf(ele), 1);
}

function TAlertList(){
    this.FList = [];
}

TAlertList.prototype = new TList(); //inherit from TList
TAlertList.prototype.constructor = TAlertList; //reset constructor

TAlertList.prototype.addElement = function(ele) {
  alert('element will be added');
  TList.prototype.addElement.call(this, ele);
};

TAlertList.prototype.removeElement = function(ele) {
  alert('element removed');
  TList.prototype.removeElement.call(this, ele);
};

几个笔记:

其父级的FList 属性实际上将在从父级继承的所有对象之间共享,除非被覆盖。这意味着如果你不覆盖FList,你会得到这个:

var a = new TAlertList();
var b = new TAlertList();

a.push(1); //b.Flist === [1]

在我看来,最好将您的子函数命名为与父函数不同的其他名称。这样你就不需要这样做了:

TList.prototype.function.call(this, parameter1, parameter2, ..);

你可以这样称呼他们:

this.function(paremeter1, parameter2);

当然,这不是调用父函数的静态方式,因为您可以用自己的函数覆盖该函数。然后再次TList.prototype.function 不是拥有该功能的对象的父级的功能。为此,您需要使用非标准 ES5 __proto__ 属性。

this.__proto__.function.call(this, parameter1, parameter2, ..);

除非您打算在不同的对象上使用该功能,否则您不需要这样做。

【讨论】:

  • function TAlertList(){ this.FList = []; } 并用不同的名称命名子函数违背了继承的概念。在第一种情况下,您可以执行 'TList.call(this)' 来防止 FList 被共享。
  • 不,他们不反对继承。也许是基于类的继承。 Javascript 并没有真正考虑到这一点,它遵循一种不同的继承模型。是的,您可以重用构造函数,但构造函数可能比您想要的更多。我手动覆盖该属性的唯一原因是为了说明我的观点。父属性不会被复制,它们是链接的。
  • 如果父类中有 500 个属性怎么办?你要在子类中声明它们吗?这显然是针对任何继承模式的。
  • 只有在父构造函数初始化 500 个属性时才会出现这个问题。如果我要设计一个这样的对象,我会在单独的函数中将该逻辑与构造函数分开(如果它的用途不止于此),以便可以单独调用它。
  • 如果您的每个原型函数都管理自己的属性,那么您将永远不会遇到这个问题。
猜你喜欢
  • 2016-04-10
  • 2013-11-07
  • 1970-01-01
  • 2011-02-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多