【问题标题】:What are the benefits of adding new methods on a derived class rather than the class itself?在派生类而不是类本身上添加新方法有什么好处?
【发布时间】:2019-05-26 14:49:40
【问题描述】:

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

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

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

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

谢谢!

【问题讨论】:

    标签: javascript node.js winston


    【解决方案1】:

    这个很有意思,谢谢指出!


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

    创建一个新的类派生记录器,其级别可以附加到其原型。这是一种众所周知的 V8 优化,可以提高原型函数的性能。

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

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

    在 Winston 中发现的优化归结为:

    getAttribute() 方法可在 Element.prototype 上找到。这意味着每次我们调用anchor.getAttribute(),JavaScript 引擎都需要……

    • 检查getAttribute 不在锚对象本身上,
    • 检查直接原型是HTMLAnchorElement.prototype,
    • 在那里断言getAttribute 不存在,
    • 检查下一个原型是HTMLElement.prototype,
    • 在那里也断言getAttribute 不存在,
    • 最终检查下一个原型是Element.prototype,
    • 并且getAttribute 存在于那里。

    总共有 7 次检查!由于这种代码在网络上很常见,引擎会使用一些技巧来减少原型属性加载所需的检查次数。

    这大致适用于 Winston,如下所示:

    • 类上的方法在所述类的prototype-object上定义
    • 每次在实例上调用方法时,引擎都需要找到被调用的方法附加到的prototype .
    • 这样做时,它会获取您正在调用该方法的 instance 类的原型,并检查在其prototype 上找到被调用的方法
    • 如果找不到它(例如,因为 方法 是继承),它会沿着prototype 链向上移动到下一个类 看看那里
    • 此过程一直持续到找到方法(并按子顺序执行)或到达prototype-chain 的末尾(并引发错误)。

    通过在构造函数中运行_setupLevels(),级别方法直接附加到特定记录器实现的原型实例。这意味着类层次结构可以任意增长:prototype-chain 查找只需 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 在内部对此进行优化,当前的“优化”可能会明显变慢。
    • 如果没有分析和来源,声称它是一种优化是毫无价值的。
    • 理解起来很复杂(请参阅此问题)
    • 这样做实际上会损害性能。链接文章的结论是:

    不要乱用原型


    至于模块化:对于所有扩展都有一个清晰的基类是有好处的。

    在比 JavaScript 更严格的语言中,这样的类可以提供仅用于扩展的特定方法,这些方法对消费者的公共 API 是隐藏的。但是,在这种特定情况下,Logger 本身就可以了。

    【讨论】:

    • 您的回答让我很高兴提出这个问题,而不是忽略它!谢谢!
    • 很高兴您对此有所确认 my conclusion ;) 我喜欢您对原型链遍历的可视化。
    猜你喜欢
    • 2018-10-11
    • 1970-01-01
    • 1970-01-01
    • 2016-03-27
    • 1970-01-01
    • 2012-05-31
    • 2013-03-28
    • 2021-09-21
    • 2012-11-15
    相关资源
    最近更新 更多