我尝试修改父类的方法,将其作为super
的属性进行访问。在这里,我有两个问题:
- 为什么修改
super.getTaskCount
没有更新父类中引用的方法? - 为什么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.foo
或super[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 Reference
的Reference
。知道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());