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

Aad*_*rma 3 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?
Run Code Online (Sandbox Code Playgroud)

PS:我知道我们可以在这种情况下使用getter和setter方法,但是我正在尝试了解更多信息super,它是正确的用法和局限性。

Sem*_*lon 5

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

console.log(this);  // works; `this` refers to a value
console.log(super); // throws a SyntaxError
Run Code Online (Sandbox Code Playgroud)

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

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

ECMA-262,第12.3.5节中,描述的前两种情况与SuperProperty产品相对应,并且非常相似。您会看到两种情况下的算法都从检索当前this值开始,然后继续执行MakeSuperPropertyReference操作,我们将在接下来的内容中进行讨论。

(我将阐明一些步骤,因为如果我们经历了所有事情,我们将整日待在这里;相反,我想提请注意与您的问题相关的特别有趣的部分。)

在MakeSuperPropertyReference中,第三步是使用检索“ baseValue” env.GetSuperBase()。这里的“ 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
Run Code Online (Sandbox Code Playgroud)

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

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

这里有两个有趣的事情。一个是表明“超级引用”是一种特殊的引用,另一个是……“引用”根本可以是返回类型!JavaScript没有固定化的“引用”,只有值,那么有什么用呢?

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

var foo = 2;
delete foo;
Run Code Online (Sandbox Code Playgroud)

在“取消声明”变量“ foo”的delete表达式中,很明显,右侧(foo)充当对绑定本身的引用,而不是value 2。Compare console.log(foo),其中,就像从JS代码中始终观察到的那样,foo'is'2。类似地,当我们执行赋值时,的左侧bar.baz = 3是对值属性的引用,而在中,LHS是baz对值属性的引用。当前环境记录(作用域)的绑定(变量名)。barbar = 3bar

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

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

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

那么,我们可以分配它吗?让我们看看评估正常作业以找出答案时会发生什么

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

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

b. Let succeeded be ? base.[[Set]](GetReferencedName(V), W, GetThisValue(V)).
Run Code Online (Sandbox Code Playgroud)

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

我们现在可以super.getTaskCount = function() {}在您的示例中进行翻译。该基地将是Project.prototype。GetReferenceName(V)将计算为字符串“ getTaskCount”。W将求出右侧的函数。GetThisValue(V)将与this的当前实例相同SoftwareProject。只剩下知道做什么base[[Set]]()

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

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

b. Let succeeded be ? base.[[Set]](GetReferencedName(V), W, GetThisValue(V)).
Run Code Online (Sandbox Code Playgroud)

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

const foo = {
  set bar(value) {
    console.log(this, value);
  }
};

const descendent = Object.create(foo);

descendent.baz = 7;
descendent.bar = 8;

// console logs { baz: 7 }, 8
Run Code Online (Sandbox Code Playgroud)

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

tl; dr:super在SuperProperty'is'中this,但所有属性查找均始于最初定义方法的类的原型的原型(如果方法是静态的,则为构造函数的原型)。但是赋值不是查找值,而是设置一个值,在此特定示例中,该super.getTaskCount = x值可与互换this.getTaskCount = x