为什么在JavaScript中修改super.method()失败?

时间:2019-04-30 16:51:10

标签: javascript ecmascript-6 es6-class

我尝试通过将其作为super的属性来访问来修改父类的方法。这里有两个问题:

  1. 为什么修改super.getTaskCount并没有更新父类中引用的方法?
  2. 为什么JavaScript在修改super.getTaskCount时没有给出任何错误?代码执行过程中真正发生了什么?

让我们看一下示例:

// Parent Class
class Project {
  getTaskCount() {
    return 50;
  }
}

// Child class
class SoftwareProject extends Project {
  getTaskCount() {
    // Let's try to modify "getTaskCount" method of parent class
    super.getTaskCount = function() {
      return 90;
    };
    return super.getTaskCount() + 6;
  }
}

let p = new SoftwareProject();
console.log(p.getTaskCount()); // prints 56. Why not 96?
// Why did super.getTaskCount method remain unchanged?

PS:我知道我们可以在这种情况下使用getter和setter方法,但是我     尝试了解有关super的更多信息,以及适当的使用和限制。

2 个答案:

答案 0 :(得分:4)

表面上,super看起来很像this。但这有很大的不同,细节也不是很直观。关于其真实性质的第一个提示是,单独浮动的关键字super在语法上无效。

console.log(this);  // works; `this` refers to a value
console.log(super); // throws a SyntaxError

相反,SuperCall — super() —是某些构造函数中可用的特殊语法,而SuperProperty — super.foosuper[foo] —是方法中可用的特殊语法。在任何情况下,表达式都不能进一步简化为super部分,而该部分与其右手边无关。

在我们可以了解SuperProperty是任务的左侧时发生的情况之前,我们需要研究一下SuperProperty本身的评估工作。

ECMA-262, § 12.3.5中,所描述的前两种情况与SuperProperty产品相对应,并且非常相似。您会看到,两种情况下的算法都首先获取当前的this值,然后继续进行MakeSuperPropertyReference操作,我们将在下一步进行讨论。

(我将阐明一些步骤,因为如果我们一整天都待在这里,我们将整日待在这里;相反,我想提请注意与您的问题相关的特别有趣的部分。)< / p>

在MakeSuperPropertyReference中,第三步是使用env.GetSuperBase()检索“ baseValue”。这里的“ env”是指最近的环境记录 具有自己的“ this”绑定环境记录是一种规范概念,用于对闭包或范围进行建模-并非完全相同,但就目前而言已经足够接近了。

在环境GetSuperBase中,引用了环境记录的[[HomeObject]]。此处的双括号表示与规格模型关联存储的数据。环境记录的HomeObject与调用的相应函数的[[HomeObject]]相同(如果存在)(不在全局范围内)。

什么是函数的HomeObject?通过语法创建方法(在对象常量或类主体中使用foo() {}语法)时,该方法与在其上创建的对象“上”相关联-即它的“主对象”。对于类主体中的方法,这意味着普通方法的原型和静态方法的构造函数。与通常完全“可移植”的this不同,方法的HomeObject永久固定为特定值。

HomeObject本身不是“超级对象”。相反,它是对对象的固定引用,将从中获取“超级对象”(基本)。实际的“超级对象”或基础就是HomeObject当前的[[Prototype]]。因此,即使[[HomeObject]]是静态的,由super引用的对象也可能不是:

class Foo { qux() { return 0; } }
class Baz { qux() { return 1; } }
class Bar extends Foo { qux() { return super.qux(); } }

console.log(new Bar().qux());
// 0

console.log(Bar.prototype.qux.call({}));
// also 0! the [[HomeObject]] is still Bar.prototype

// However ...

Object.setPrototypeOf(Bar.prototype, Baz.prototype);

console.log(new Bar().qux());
// 1 — Bar.prototype[[Prototype]] changed, so GetSuperBase resolved a different base

因此,现在我们对“ super.getTaskCount”中的“ super”的含义有了一些额外的了解,但是仍然不清楚为什么分配失败。如果我们现在回头看看MakeSuperPropertyReference,那么我们将从下一步中获得下一个线索:

  

“返回类型为Reference的值,该值为超级参考,其基本值   组件是bv [ed。基本值],其引用名称部分为   propertyKey,其thisValue组件是实际的This [ed。当前的this],   并且其严格引用标志是严格的。”

这里有两个有趣的事情。一个是表示“超级参考”是一种特殊的参考,另一个是……“参考”完全可以是返回类型! JavaScript没有统一化的“引用”,只有值,那有什么用?

参考确实作为规范概念存在,但它们只是规范概念。引用绝不是来自JavaScript的可触摸的固定值,而是评估其他内容的过渡部分。要了解为什么规范中存在这些参考值,请考虑以下语句:

var foo = 2;
delete foo;

在此表达式中,“未声明”变量“ foo”,很明显右侧是参考,而不是值2。比较console.log(foo),在这里,和从JS代码的POV开始一样,foo'是'2。类似地,当我们执行赋值时,bar.baz = 3的左侧是 reference baz的属性bar,在bar = 3中,LHS是对当前环境记录(范围)的绑定(变量名)bar的引用)。

我说过我将尽量避免在这里的任何单个兔子洞中过深,但我失败了! ...我的观点主要是SuperReference不是最终的返回值­-ES代码永远无法直接观察到它。

如果使用JS建模,我们的超级参考将类似于以下内容:

const superRef = {
  base: Object.getPrototypeOf(SoftwareProject.prototype),
  referencedName: 'getTaskCount',
  thisValue: p
};

那么,我们可以分配它吗?让我们看看what happens when evaluating a normal assignment来找出答案。

在此操作中,我们满足第一个条件(SuperProperty不是ObjectLiteral或ArrayLiteral),因此我们继续进行以下子步骤。计算超级属性,因此lref现在是类型Reference的{​​{1}}。知道Super Reference是右侧的评估值,我们可以跳到步骤1.e .: rval

PutValue首先是在发生错误时退出,并且还早退出PutValue(lref, rval)值(此处称为lref)不是V(请考虑{ {1}} — ReferenceError)。在步骤4中,将Reference设置为2 = 7,由于这是超级引用,因此它再次是与该类相对应的原型的[[Prototype]]在其中创建方法的主体。我们可以跳过步骤5;该引用是可解析的(例如,它不是未声明的变量名)。 SuperProperty确实满足base,因此我们继续执行步骤6的子步骤。GetBase(V)是一个对象,而不是原始对象,因此跳过6.a。然后它发生了! 6.b-作业。

HasPropertyReference

好吧,无论如何。旅程还没有完成。

在您的示例中,我们现在可以将其翻译为base。基数为b. Let succeeded be ? base.[[Set]](GetReferencedName(V), W, GetThisValue(V)). 。 GetReferenceName(V)将计算为字符串“ getTaskCount”。 W将求出右侧的函数。 GetThisValue(V)将与super.getTaskCount = function() {}的当前实例Project.prototype相同。只剩下知道this的作用。

当我们在类似的方括号中看到“方法调用”时,它是对众所周知的内部操作的引用,该操作的实现方式取决于对象的性质(但通常是相同的)。在我们的例子中,base是一个普通对象,因此它是Ordinary Object [[set]]。依次调用OrdinarySet,后者调用OrdinarySetWithOwnDescriptor。在这里,我们将执行步骤3.d.iv,我们的旅程将以成功完成的任务结束!!

还记得SoftwareProject被传下来吗? 那是任务的目标,而不是超级基础。不过,这并不是SuperProperty所独有的。例如访问者也是如此:

base[[Set]]()

那里的访问者以后代实例作为接收者被调用,并且超级属性就是这样。让我们对您的示例进行一些小的调整,看看:

this

这是一个奇妙的问题-保持好奇心。

  

tl; dr:SuperProperty'是'const foo = { set bar(value) { console.log(this, value); } }; const descendent = Object.create(foo); descendent.baz = 7; descendent.bar = 8; // console logs { bar: 7 }, 8 中的// Parent Class class Project { getTaskCount() { return 50; } } // Child class class SoftwareProject extends Project { getTaskCount() { // Let's try to modify "getTaskCount" method of parent class super.getTaskCount = function() { return 90; }; return this.getTaskCount() + 6; } } let p = new SoftwareProject(); console.log(p.getTaskCount());,但所有属性查找都从最初定义方法的类的原型(或原型)开始(如果方法是静态的)。在此特定示例中,super可与this互换。

答案 1 :(得分:2)

替代超级方法不是很好的设计,但是如果您真的想更改,可以用这种方式完成

class Project {
      getTaskCount() {
        return 50;
      }
    }
    
    // Child class
    class SoftwareProject extends Project {
      getTaskCount() {
        // Let's try to modify "getTaskCount" method of parent class
        let getTaskCount = Project.prototype;
        Project.prototype.getTaskCount = function() {
          return 90;
        };
        let count = super.getTaskCount() + 6;
        Project.prototype.getTaskCount = getTaskCount;
        return count;
      }
    }
    
    let p = new SoftwareProject();
    console.log(p.getTaskCount());