在派生 class 而不是 class 本身上添加新方法有什么好处?

What are the benefits of adding new methods on a derived class rather than the class itself?

出于个人学习目的,我试图了解 Winston 的包设计结构及其每个模块背后的目的,但我无法理解这一点。

在Winstons包中,logger.js模块中有核心Loggerclass,实现了logger的主要功能,并提供了一些public方法,例如logger.log 方法。它还实现了供内部使用的转换流方法。

然后在 create-logger.js 模块中有一个派生的 class 名为 DerivedLogger 扩展了 Logger class 并且它的唯一目的似乎是添加优化级别方法到记录器原型。 DerivedLogger class 然后在模块底部的工厂函数中实例化和导出。

我的问题是,为什么需要 DerivedLogger class?如果将这些级别方法添加到 Logger class 原型本身,然后让工厂函数直接实例化 Logger class,性能会有什么不同吗?我能想到的唯一原因是,也许添加 DerivedLogger class 只是为了模块化目的?有人可以帮我理解原因吗?

谢谢!

这个很有趣,谢谢指出!


简而言之:与代码结构无关,是性能优化。评论说:

Create a new class derived logger for which the levels can be attached to the prototype of. This is a V8 optimization that is well know to increase performance of prototype functions.

就我个人而言,我认为这需要引用(我不会在代码审查中接受它)。幸运的是,我认为我找到了作者所说的"optimization":

Mathias 的

This article(一位从事 V8 工作的 Google 工程师)谈到通过正确使用 prototype 来加速 JavaScript 执行。这篇文章有 很多 的详细信息,如果您正在学习,真的值得一读。

Winston 中发现的优化归结为:

The getAttribute() method is found on the Element.prototype. That means each time we call anchor.getAttribute(), the JavaScript engine needs to…

  • check that getAttribute is not on the anchor object itself,
  • check that the direct prototype is HTMLAnchorElement.prototype,
  • assert absence of getAttribute there,
  • check that the next prototype is HTMLElement.prototype,
  • assert absence of getAttribute there as well,
  • eventually check that the next prototype is Element.prototype,
  • and that getAttribute is present there.

That’s a total of 7 checks! Since this kind of code is pretty common on the web, engines apply tricks to reduce the number of checks necessary for property loads on prototypes.

大致适用于Winston如下:

  • 上的方法 是在上述 class 的 prototype 对象上定义的
  • 每次在 实例 上调用 方法 时,引擎需要找到被调用的 prototype 方法附于.
  • 在这样做时,它会获取您正在调用该方法的 实例 -class 的原型,并检查以找到被调用的 方法prototype
  • 如果找不到它(例如因为 方法 继承的 ),它会向上走 prototype -链接到下一个 class 并查看那里
  • 这一直持续到找到方法(并按顺序执行)或到达 prototype 链的末尾(并抛出错误)。

通过 运行 _setupLevels() 在构造函数中,级别方法直接附加到特定记录器实现的原型 实例 。这意味着 class-层次结构可以增长到任意大:prototype-链查找只需要 1 步就可以找到方法

这是另一个(简化的)示例:

class A {
    constructor() {
        this.setup();
    }
    testInherit() {
        console.log("Inherited method called");
    }
    setup() {
        this["testDirect"] = () => console.log("Directly attached method called");
    }

}

class B extends A {
    constructor() {
        super();
    }
}

const test = new B();
test.testInherit();
test.testDirect();

如果我们在 test 实例化后设置断点,我们会看到以下内容:

如您所见,testDirect-方法直接附加到 test,而 testInherit 向下多级。


个人认为这是不好的做法:

  • 现在可能是这样,但将来可能就不是这样了。如果 V8 在内部对此进行优化,当前的 "optimization" 可能会显着变慢。
  • 如果没有分析和来源,声称这是一种优化就没有任何价值。
  • 理解起来比较复杂(看这个问题)
  • 这样做实际上会影响性能。链接文章的结论是:

don’t mess with prototypes


至于模块化:对于所有扩展都有一个明确的基础-class 有话要说。

在比 JavaScript 更严格的语言中,这样的 class 可以提供仅用于扩展的特定方法,这些方法对 public API 隐藏消费者。然而,在这种特定情况下,Logger 本身就没问题。