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

问题描述 投票:3回答:2

我尝试通过访问它作为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的知识,这是正确的使用和限制。

javascript ecmascript-6 es6-class
2个回答
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操作,我们接下来应该看一下。

(如果我们一整天都在这里,我会完全不知道一些步骤,因为我们整天都在这里;相反,我想提请注意那些与你的问题有关的特别感兴趣的部分。)

在MakeSuperPropertyReference中,第三步是使用env.GetSuperBase()检索'baseValue'。这里的'env'指的是最近的环境记录,它有自己的'this'绑定。环境记录是对闭包或范围进行建模的规范概念 - 它不是完全相同的东西,但现在已足够接近。

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

什么是函数的HomeObject?当一个方法在语法上创建时(在对象文字或类体中使用foo() {}语法),该方法与创建它的对象'on'相关联 - 这就是它的'home对象'。对于类体中的方法,这意味着普通方法的原型和静态方法的构造函数。与通常完全“可移植”的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组件是actualThis [ed。目前的this],其严格的参考标志是严格的。“

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

参考确实存在作为规范概念,但它们只是规范概念。引用永远不是JavaScript中可“触摸”的具体值,而是评估其他内容的瞬态部分。要了解规范中存在这些类型的引用值的原因,请考虑以下语句:

var foo = 2;
delete foo;

在这个'undeclares'变量'foo'的表达式中,很明显右侧是作为参考而不是值2。比较console.log(foo),其中,一如既往地从JS代码的POV,foo'是'2.类似地,当我们执行赋值时,bar.baz = 3的左侧是对值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),因此我们继续执行后面的子步骤。 SuperProperty被评估,所以lref现在是Reference类型的Super Reference。知道rval是右侧的评估值,我们可以跳到步骤1.e:PutValue(lref, rval)

如果发生错误,PutValue开始提前退出,并且早退出lref值(此处称为V)不是Reference(例如2 = 7 - ReferenceError)。在步骤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)).

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

我们现在可以为你的例子中的super.getTaskCount = function() {}翻译。基地将是Project.prototype。 GetReferenceName(V)将计算为字符串“getTaskCount”。 W将评估右侧的功能。 GetThisValue(V)将与this相同,SoftwareProjectbase[[Set]]()的当前实例。那只是知道Ordinary Object [[set]]做了什么。

当我们在括号中看到“方法调用”时,它是对众所周知的内部操作的引用,其实现因对象的性质而异(但通常是相同的)。在我们的例子中,base是一个普通的对象,所以它是OrdinarySetWithOwnDescriptor。这又调用了调用this的OrdinarySet。在这里,我们点击了步骤3.d.iv,我们的旅程结束了... ...成功的任务!?

还记得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 被传下来吗?这是作业的目标,而不是超级基础。这不是SuperProperty独有的;例如,访问者也是如此:

// 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

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

tl; dr:在SuperProperty中的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());互换。


2
投票

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

qazxswpoi
© www.soinside.com 2019 - 2024. All rights reserved.