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



我尝试修改父类的方法,将其作为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:我知道我们可以在这种情况下使用吸气剂和二传手,但我是 试图了解更多关于super及其正确使用和限制的信息。

从表面上看,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部分。

在我们进入当超级财产是作业的左侧时会发生什么之前,我们需要看看评估超级财产本身到底做了什么。

在ECMA-262,§ 12.3.5中,描述的前两种情况对应于SuperProperty的生产,并且非常相似。您将看到,这两种情况下的算法都从检索当前this值开始,最后继续执行 MakeSuperPropertyReference 操作,我们接下来应该看看。

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

在 MakeSuperPropertyReference 中,第三步是使用env.GetSuperBase()检索"baseValue"。这里的"env"是指最近的环境记录 它有自己的"这个"绑定。环境记录是建模闭包或范围的规范概念 - 它不完全是一回事,但足以说现在。

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

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

HomeObject 本身并不是"超级对象"。相反,它是对从中派生"超级对象"(base)的对象的固定引用。实际的"超级对象"或基础是HomeObject的当前[[原型]]。因此,即使 [[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 [编辑基值],其引用的名称组件是 属性键,其 thisValue 组件是 实际这 [编辑当前this], 谁的严格参考标志是严格的。

这里有两件有趣的事情。一个是它表明"超级参考"是一种特殊的参考,另一个是......"引用"完全可以是返回类型!JavaScript没有具体的"引用",只有值,那么什么给了呢?

引用确实作为规范概念存在,但它们只是一个规范概念。引用从来都不是一个从 JavaScript "可触摸"的化值,而是评估其他东西的瞬态部分。要了解规范中存在这些参考值的原因,请考虑以下语句:

var foo = 2;
delete foo;

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

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

如果用 JS 建模,我们的超级引用将如下所示:

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

那么,我们可以分配给它吗?让我们看看在评估正常作业时会发生什么。

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

如果发生错误,PutValue 首先提前退出,如果lref值(这里称为V)不是Reference,则提前退出(例如2 = 7— 引用错误)。在步骤 4 中,base设置为GetBase(V),因为这是一个超级引用,所以它再次是原型的 [[Prototype]],对应于在其中创建方法的类主体。我们可以跳过第 5 步;引用是可解析的(例如,它不是未声明的变量名)。超级属性确实满足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 是一个普通对象,所以它是普通对象 [[set]]。这反过来调用 OrdinarySet,后者调用 OrdinarySetWithOwnDescriptor。在这里,我们点击了步骤3.d.iv,我们的旅程结束了......用...任务成功!?

还记得this传承吗?这是任务的目标,而不是超级基地。不过,这并不是超级财产所独有的;例如,对于访问器也是如此:

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在SuperProperty中"is"this,但所有属性查找都从最初定义方法的类的原型原型开始(如果方法是静态的,则从构造函数的原型开始)。但是赋值不是查找值,而是设置值,在此特定示例中,super.getTaskCount = x可与this.getTaskCount = x互换。

覆盖超级方法不是好的设计,但如果你真的想改变,你可以这样做

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

相关内容

最新更新