【问题标题】:Why modifying super.method() in JavaScript fails?为什么在 JavaScript 中修改 super.method() 会失败?
【发布时间】:2019-09-19 09:16:30
【问题描述】:

我尝试通过将父类的方法作为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 的信息以及它的正确使用和限制。

【问题讨论】:

  • 你想做什么?你能告诉我一个现实世界的用例,你正在尝试做的事情可能需要吗?
  • @VikramDeshmukh 了解有关 super、它的用途和限制的更多信息。
  • “超级”只是访问原型的另一种方式吗?当我们从原型继承的角度来看它时,这是有道理的。
  • 这个例子没有多大意义。在这种情况下,子类应该只是return 96;。子类实际上改变父类中方法的实现是没有意义的。

标签: javascript ecmascript-6 es6-class


【解决方案1】:

从表面上看,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]] 的引用。此处的双括号表示与规范模型关联存储的数据。环境记录的 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

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

“返回一个引用类型的值,它是一个超级引用,其基值 组件是 bv [ed。基值],其引用的名称组件是 propertyKey,其 thisValue 组件是 actualThis [ed.当前this], 并且其严格引用标志是严格的。”

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

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

var foo = 2;
delete foo;

在“取消声明”变量“foo”的删除表达式中,很明显右侧 (foo) 充当对绑定本身的引用,而不是值 2。比较console.log(foo),与从JS代码中观察到的一样,foo 'is' 2. 同样,当我们执行赋值时,bar.baz = 3 的左侧是属性@的引用 987654350@ 的值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 现在是Super Reference 类型的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)).

好吧,无论如何。旅程尚未结束。

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

当我们在这样的括号中看到“方法调用”时,它是对众所周知的内部操作的引用,其实现取决于对象的性质(但通常是相同的)。在我们的例子中,base 是一个普通对象,所以它是Ordinary Object [[set]]。这反过来又调用了调用 OrdinarySetWithOwnDescriptor 的 OrdinarySet。在这里,我们将完成第 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 在 SuperProperty 'is' this 中,但所有属性查找都从最初定义方法的类的原型的原型(或构造函数的原型,如果方法是静态的)。但是赋值不是查找一个值,而是设置一个值,在这个特定的示例中,super.getTaskCount = x 可以与this.getTaskCount = x 互换。

【讨论】:

    【解决方案2】:

    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 是有道理的。
    猜你喜欢
    • 2023-04-05
    • 2014-04-24
    • 1970-01-01
    • 2016-04-11
    • 2021-10-10
    • 2013-01-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多