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

Why modifying super.method() in JavaScript fails?

我尝试修改父 class 的方法,将其作为 super 的 属性 进行访问。这里我有两个问题:

  1. 为什么修改 super.getTaskCount 没有更新父 class 中引用的方法?
  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 及其正确使用和限制。

override super 方法不是很好的设计,但如果你真的想改变,你可以这样做

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());

从表面上看,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, the first two cases described correspond to the SuperProperty production and are very similar. You’ll see that the algorithms in both cases begin by retrieving the current this value and end by continuing on to the MakeSuperPropertyReference操作中,我们接下来要看一下。

(我将省略一些步骤的作用,因为如果我们遍历所有内容,我们会整天待在这里;相反,我想提请注意与您的问题相关的特别有趣的部分。 )

在 MakeSuperPropertyReference 中,第三步是使用 env.GetSuperBase() 检索“baseValue”。这里的‘env’指的是最近的环境记录 它有自己的“this”绑定。环境记录是对闭包或范围建模的规范概念——这不是完全相同的东西,但现在可以这么说了。

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

什么是函数的 HomeObject?当按语法创建方法时(在对象字面量或 class 主体中使用 foo() {} 语法),该方法与创建它的对象“所在”相关联——这是它的“主对象” .对于 class 主体中的方法,这意味着普通方法的原型和静态方法的构造函数。与通常完全“可移植”的 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,我们将从最后一步得到下一条线索:

“Return a value of type Reference that is a Super Reference whose base value component is bv [ed. the base value], whose referenced name component is propertyKey, whose thisValue component is actualThis [ed. the current this], and whose strict reference flag is strict.”

这里有两件有趣的事情。一是表明‘超级引用’是一种特殊的引用,二是……‘引用’完全可以是return类型! JavaScript 没有具体化的“引用”,只有值,所以给出了什么?

引用确实作为规范概念存在,但它们只是规范概念。引用从来都不是 JavaScript 中“可触摸”的具体化值,而是评估其他内容的短暂部分。要理解规范中存在这些参考值的原因,请考虑以下语句:

var foo = 2;
delete foo;

在 delete 表达式中,它“未声明”变量“foo”,很明显右侧 (foo) 充当对绑定本身的引用,而不是值 2。比较 console.log(foo),其中,从 JS 代码中一如既往地观察到,foo 'is' 2。同样,当我们执行赋值时,bar.baz = 3 的左侧是一个 reference 到值 bar 的 属性 baz,而在 bar = 3 中,LHS 是对 bar 的绑定(变量名)的引用当前环境记录(范围)。

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

如果在 JS 中建模,我们的超级参考看起来像这样:

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

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

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

如果发生错误,

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

b. Let succeeded be ? base.[[Set]](GetReferencedName(V), W, GetThisValue(V)).

好吧,无论如何。旅程未完成。

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

当我们在括号中看到这样的“方法调用”时,它是对众所周知的内部操作的引用,其实现因对象的性质而异(但通常是相同的)。在我们的例子中,base 是一个普通对象,所以它是 Ordinary Object [[set]]. This in turn calls OrdinarySet which calls OrdinarySetWithOwnDescriptor。在这里,我们将点击 3.d.iv 步,我们的旅程结束......以......成功分配!?

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

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

那里的访问器是用后代实例作为它的接收者调用的,超级属性就是这样。让我们对您的示例做一个小调整,看看:

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

// Child class
class SoftwareProject extends Project {
  getTaskCount() {
    super.getTaskCount = function() {
      return 90;
    };
    return this.getTaskCount() + 6;
  }
}

let p = new SoftwareProject();
console.log(p.getTaskCount());

// 96 — because we actually assigned the new function on `this`

这是一个很好的问题——保持好奇。

tl;dr: super in a SuperProperty ‘is’ this, but with all property lookups starting from the prototype of the prototype of the class on which the method was originally defined (or the prototype of the constructor, if the method is static). But assignment isn’t looking up a value, it’s setting one, and in this particular example, super.getTaskCount = x is interchangeable with this.getTaskCount = x.