如何在ES6中扩展一个类而不必使用super关键字?

137

在ES6中,是否可能在不调用super方法来调用父类的情况下扩展一个类?

编辑:这个问题可能会引起误导。我们必须调用super()吗?还是我遗漏了什么?

例如:

class Character {
   constructor(){
      console.log('invoke character');
   }
}

class Hero extends Character{
  constructor(){
      super(); // exception thrown here when not called
      console.log('invoke hero');
  }
}

var hero = new Hero();

当我在派生类中没有调用super()时,我遇到了作用域问题 -> this is not defined

我正在使用iojs v2.3.0中的--harmony运行此代码


你说的作用域问题是什么意思?你是否遇到了异常(在哪里)? - Amit
当在没有调用super()的情况下调用派生类时,我会在派生类中遇到异常。我编辑了我的问题以使其更清晰 :) - xhallix
6
如果你要扩展另一个类,那么在构造函数中必须首先调用 super()。 - Jonathan de M.
@Amit 即使在这种情况下是正确的,通常也不会与不完全符合规范的实现进行争论。 - zerkms
@zerkms - 首先,Jonathan明确指出了Babel的实现。其次,在规范中哪里说了相反的呢?这是有合法用例的。 - Amit
显示剩余10条评论
10个回答

182

ES2015(ES6)类的规则基本上可以归结为:

  1. 在子类构造函数中,只有在调用super之后才能使用this
  2. 如果ES6类是子类,则构造函数必须调用super,或者它们必须显式返回某个对象来代替未初始化的对象。

这归结为ES2015规范的两个重要部分。

第8.1.1.3.4节定义了在函数中决定this是什么的逻辑。对于类来说重要的部分是它可能处于"uninitialized"状态,当处于此状态时,尝试使用this会抛出异常。

第9.2.2节[[Construct]]定义了通过newsuper调用函数的行为。调用基本类构造函数时,this初始化为[[Construct]]的第8步,但对于所有其他情况,this未初始化。在构造完成后,会调用GetThisBinding,因此如果尚未调用super(从而初始化了this),或者没有显式返回替换对象,则构造函数调用的最后一行将抛出异常。


1
你能否提供一种在不调用 super() 的情况下继承一个类的方法? - Bergi
4
你认为在ES6中是否有可能在构造函数中不调用 super() 的情况下从一个类继承? - Bergi
10
谢谢您的编辑 - 现在就在那里了;您可以使用return Object.create(new.target.prototype, …)来避免调用超级构造函数,这样做会使内容更加易懂。 - Bergi
2
@Maximus 请查看“new.target”是什么? - Bergi
1
你应该添加如果构造函数被省略会发生什么的说明:根据MDN,将使用默认构造函数。 - kleinfreund
显示剩余3条评论

24

新的ES6类语法只是使用原型的“旧”ES5“类”的另一种表示方法。因此,如果不设置它的原型(即基类),则无法实例化特定类。

这就像在三明治上放奶酪而没有做它一样。同样,您也不能在制作三明治之前放置奶酪,所以...

...在使用super()调用超类之前使用this关键字也是不允许的。

// valid: Add cheese after making the sandwich
class CheeseSandwich extend Sandwich {
    constructor() {
        super();
        this.supplement = "Cheese";
    }
}

// invalid: Add cheese before making sandwich
class CheeseSandwich extend Sandwich {
    constructor() {
        this.supplement = "Cheese";
        super();
    }
}

// invalid: Add cheese without making sandwich
class CheeseSandwich extend Sandwich {
    constructor() {
        this.supplement = "Cheese";
    }
}

如果您未为基类指定构造函数,则将使用以下定义:

constructor() {}

对于派生类,将使用以下默认构造函数:

constructor(...args) {
    super(...args);
}

编辑:在developer.mozilla.org上找到了这个:

当在构造函数中使用时,super关键字单独出现,并且必须在使用this关键字之前使用。

来源


如果我理解正确的话,当没有调用super()时,我将无法在我的Hero类中使用Character类的任何方法? 但这似乎并不完全正确,因为我可以调用基类的方法。 所以我想我只需要在调用构造函数时使用super。 - xhallix
  1. 在 OP 中,根本没有使用 this
  2. JS 不是三明治,在 ES5 中,您可以在调用任何其他函数之前使用 this,即使该函数可能或可能不定义补充属性。
- Amit
  1. 现在我也不能使用 this 了吗?!
  2. 我的 JS 类代表一个三明治,在 ES6 中你并不总是能使用 this。我只是想用一个比喻来解释 es6 类,没有人需要这样具有破坏性和不必要的评论。
- marcel
@marcel,非常抱歉我有点讽刺,但是:1. 是为了引入一个在原帖中不存在的新问题。2. 是为了指出你的说法是错误的(这仍然是事实)。 - Amit
@marcel - 看看我的答案 - Amit
显示剩余4条评论

14

有多个答案和评论声称,在constructor中,super MUST是第一行。这是完全错误的。@loganfsmyth的回答提供了必要的要求参考,但归根结底是:

继承(extends)构造函数必须在使用this之前,并在返回之前调用super,即使没有使用this

查看下面的片段(在Chrome中工作……),以了解为什么在调用super之前可能有没有使用this的语句。

'use strict';
var id = 1;
function idgen() {
  return 'ID:' + id++;
}

class Base {
  constructor(id) {
    this.id = id;
  }

  toString() { return JSON.stringify(this); }
}

class Derived1 extends Base {
  constructor() {
    var anID = idgen() + ':Derived1';
    super(anID);
    this.derivedProp = this.baseProp * 2;
  }
}

alert(new Derived1());


2
请查看以下片段(在Chrome中有效...),打开Chrome开发者控制台并单击“运行代码片段”:未捕获的引用错误:this未定义。当然,您可以在调用super()之前在构造函数中使用方法,但是不能在类中使用方法! - marcel
你不能在 super() 之前使用 this(你的代码可以证明这一点),这与规范无关,而是与 JavaScript 的实现有关。因此,你必须在调用 'this' 之前先调用 'super'。 - marcel
@marcel - 我认为我们有很多混淆。我一直在说,在使用super之前只陈述语句是合法的,而你则声称在调用super之前使用this是非法的。我们都是正确的,只是彼此没有理解 :-) (那个异常是故意的,以展示什么是不合法的 - 我甚至将属性命名为“WillFail”) - Amit
我会说这个例子是草率的。抱歉直言不讳。它表明super不一定要是第一个语句。但是我为什么想要那样做呢?在调用基础构造函数之前生成一个id?为什么不对所有类都使用它?derivedPropnull。它至少引出了很多问题。而且你让它听起来像你有一个实际应用的例子。但这个例子仍然是理论的。 - x-yuri
@x-yuri 这是一个五年前的答案 :-) 然而,我仍然认为它的价值在于解释什么是有效或无效的代码,而不是这样的代码有什么用例。更多的知识总是一件好事,即使你在任何时候都看不到用例。 - Amit

12

如果在子类中完全省略构造函数,则可以在子类中省略super()。一个“隐藏”的默认构造函数将自动包含在您的子类中。但是,如果您在子类中包括构造函数,则必须在该构造函数中调用super()。

class A{
   constructor(){
      this.name = 'hello';   
   }
}
class B extends A{
   constructor(){
      // console.log(this.name); // ReferenceError
      super();
      console.log(this.name);
   }
}
class C extends B{}  // see? no super(). no constructor()

var x = new B; // hello
var y = new C; // hello

查看此页面了解更多信息。


6

justyourimage提供的答案是最简单的方法,但他的示例有些冗长。这里是通用版本:

class Base {
    constructor(){
        return this._constructor(...arguments);
    }

    _constructor(){
        // just use this as the constructor, no super() restrictions
    }
}

class Ext extends Base {
    _constructor(){ // _constructor is automatically called, like the real constructor
        this.is = "easy"; // no need to call super();
    }
}

不要扩展真正的constructor(),只需使用虚假的_constructor()来进行实例化逻辑。
请注意,这种解决方案使得调试变得麻烦,因为您需要为每个实例化方法进入一个额外的方法。

2
是的,这绝对是最简单的方法和最清晰的答案 - 我所问的问题是...“为什么,上帝,为什么!?”... super() 不是自动的有一个非常有效的原因...你可能想要将特定参数传递给基类,以便在实例化基类之前进行一些处理/逻辑/思考。 - LFLFM

4
只是注册以发布此解决方案,因为这里的答案都不满足我,实际上有一种简单的方法可以解决这个问题。将您的类创建模式调整为在子方法中覆盖您的逻辑,并仅使用超级构造函数将构造函数参数转发给它。

也就是说,在您的子类中不会创建构造函数,而只会引用在相应子类中被覆盖的方法。

这意味着您可以摆脱强制执行的构造函数功能,并退回到常规方法 - 可以被覆盖,并且不会强制执行super()让您自由选择是否,何时以及如何调用super(完全可选),例如:

super.ObjectConstructor(...)

class Observable {
  constructor() {
    return this.ObjectConstructor(arguments);
  }

  ObjectConstructor(defaultValue, options) {
    this.obj = { type: "Observable" };
    console.log("Observable ObjectConstructor called with arguments: ", arguments);
    console.log("obj is:", this.obj);
    return this.obj;
  }
}

class ArrayObservable extends Observable {
  ObjectConstructor(defaultValue, options, someMoreOptions) {
    this.obj = { type: "ArrayObservable" };
    console.log("ArrayObservable ObjectConstructor called with arguments: ", arguments);
    console.log("obj is:", this.obj);
    return this.obj;
  }
}

class DomainObservable extends ArrayObservable {
  ObjectConstructor(defaultValue, domainName, options, dependent1, dependent2) {
    this.obj = super.ObjectConstructor(defaultValue, options);
    console.log("DomainObservable ObjectConstructor called with arguments: ", arguments);
    console.log("obj is:", this.obj);
    return this.obj;
  }
}

var myBasicObservable = new Observable("Basic Value", "Basic Options");
var myArrayObservable = new ArrayObservable("Array Value", "Array Options", "Some More Array Options");
var myDomainObservable = new DomainObservable("Domain Value", "Domain Name", "Domain Options", "Dependency A", "Depenency B");

干杯!


2
我需要一个“像我五岁一样解释”的解释……我觉得这是一个非常深刻但复杂的答案,因此被忽略了。 - swyx
@swyx:魔法就在构造函数里,'this' 关键字根据你正在创建的对象类型不同而指向不同类型的对象。例如,如果你正在构造一个新的 DomainObservable 对象,'this.ObjectConstructor' 就指向另一种方法,即 DomainObserveable.ObjectConstructor;而如果你正在构造一个新的 ArrayObservable 对象,'this.ObjectConstructor' 就指向 ArrayObservable.ObjectConstructor。 - cobberboy
看我的回答,我发布了一个简单得多的例子。 - Michael Lewis
我完全同意@swyx的观点;这个答案做了太多的事情...我只是浏览了一下,就已经感到疲倦了。我感觉像是“用五岁孩子的语言解释,并且真的很想上厕所...” - spb

2

@Bergi提到了new.target.prototype,但我正在寻找一个具体的示例来证明您可以访问this(或者更好的是,客户端代码正在使用new创建对象的引用,请参见下文)而无需调用super()

空谈误国,展示代码......所以这里有一个例子:

class A { // Parent
    constructor() {
        this.a = 123;
    }

    parentMethod() {
        console.log("parentMethod()");
    }
}

class B extends A { // Child
    constructor() {
        var obj = Object.create(new.target.prototype)
        // You can interact with obj, which is effectively your `this` here, before returning
        // it to the caller.
        return obj;
    }

    childMethod(obj) {
        console.log('childMethod()');
        console.log('this === obj ?', this === obj)
        console.log('obj instanceof A ?', obj instanceof A);
        console.log('obj instanceof B ?',  obj instanceof B);
    }
}

b = new B()
b.parentMethod()
b.childMethod(b)

这将输出:

parentMethod()
childMethod()
this === obj ? true
obj instanceof A ? true
obj instanceof B ? true


因此,您可以看到我们有效地创建了一个类型为B(子类)的对象,该对象也是A(其父类)的对象,并且在子类BchildMethod()中,我们使用this指向在B的constructor中使用Object.create(new.target.prototype)创建的对象obj
而且所有这些都不需要关心super
这利用了JS中的一个事实,即当客户端代码使用new构造新实例时,constructor可以返回完全不同的对象。
希望这能帮助某些人。

1

尝试:

class Character {
   constructor(){
     if(Object.getPrototypeOf(this) === Character.prototype){
       console.log('invoke character');
     }
   }
}


class Hero extends Character{
  constructor(){
      super(); // throws exception when not called
      console.log('invoke hero');
  }
}
var hero = new Hero();

console.log('now let\'s invoke Character');
var char = new Character();

演示


在这个例子中,您还需要使用super(),如果您不使用它,就会抛出异常。因此我认为在这种情况下不能省略super()调用。 - xhallix
我以为你的目标是不执行父类的构造函数,这段代码就是这么做的。当你继承时,无法摆脱 super。 - Jonathan de M.
抱歉之前有些误导你了 :) 我只是想知道是否真的需要使用super(),因为我对语法感到困惑,因为在其他语言中,在调用派生类的构造函数时我们不必调用super方法。 - xhallix
1
根据https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes的说法,“构造函数*可以*使用super关键字调用父类的构造函数”,因此我建议等待ES6发布。 - Jonathan de M.
3
“所以我会建议等待ES6发布”——ES6已经发布了,可以在http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf找到。 - zerkms
显示剩余2条评论

1
我建议使用OODK-JS,如果你打算开发以下面向对象的概念。
OODK(function($, _){

var Character  = $.class(function ($, µ, _){

   $.public(function __initialize(){
      $.log('invoke character');
   });
});

var Hero = $.extends(Character).class(function ($, µ, _){

  $.public(function __initialize(){
      $.super.__initialize();
      $.log('invoke hero');
  });
});

var hero = $.new(Hero);
});

1
简单的解决方案:我认为很清楚,不需要解释。
class ParentClass() {
    constructor(skipConstructor = false) { // default value is false
        if(skipConstructor) return;
        // code here only gets executed when 'super()' is called with false
    }
}
class SubClass extends ParentClass {
    constructor() {
        super(true) // true for skipping ParentClass's constructor.
        // code
    }
}

我不确定为什么需要上述所有样板代码,也不确定这种方法是否会产生副作用。但对我来说,它可以正常工作,没有问题。 - MyUserInStackOverflow
1
一个问题:如果您想再次扩展您的子类,那么您将不得不在每个子类的构造函数中构建skipConstructor功能。 - Michael Lewis

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