【问题标题】:JS: Confusion about inheritanceJS:关于继承的困惑
【发布时间】:2011-12-18 04:41:36
【问题描述】:

我通过 C++、Java 等语言熟悉 OOP 概念。现在我正在尝试学习 JavaScript 作为一种爱好,主要是由于对 WebGL 的兴趣。但是我在基于原型的继承方面遇到了麻烦。

假设我有一个在构造函数中接受参数的基类。我需要扩展它。我这样做的方式如下所示。

function Base(n) {
    this._n = n;
}

Base.prototype.print = function() {
    console.log(this._n);
}

function Derived(n) {
    Base.call(this, n);
}

Derived.prototype = new Base;
Derived.prototype.constructor = Derived;

这就是我所理解的:单个Base 对象充当Derived 的原型。所以Derived 的所有实例都将从这个Base 对象继承属性,例如print 方法。当我调用new Derived(10) 时会创建一个新对象,函数Derived 在这个新创建的对象的上下文中被调用,即this 指向新创建的对象,函数Base 是从函数Derived 调用的,然后_n 被创建并赋值为10。所以如果我创建5 个Derived 对象,它们都将拥有自己的_n 属性。到目前为止还可以。

但我不喜欢这行:

Derived.prototype = new Base;

函数Base 需要一个参数,但我在这里什么也没传递。这里没有必要传递参数,因为该对象将充当Derived 的原型。对于这个原型对象,我不需要任何 _n 值。但是如果函数Base 依赖于参数呢?比如说,Base 加载一个资源并且路径作为参数传递。那该怎么办?

总而言之,我的问题是:

  1. 如何处理原型对象中的数据成员(本例中为_n)?
  2. Derived.prototype = new Base; 正在创建一个 Base 的实例,它会一直保留在内存中(假设 Derived 是在全局空间中定义的)。如果Base 类非常昂贵并且我不想要额外的对象怎么办?

【问题讨论】:

    标签: javascript


    【解决方案1】:

    首先,感谢您如此深入地理解 JavaScript 的原型继承。你显然已经完成了你的功课。大多数具有 Java 或 C++ 背景的人往往会遇到困难,但您已经度过了最糟糕的时期。

    函数Base 需要一个参数,但我在这里什么也没传递。 如何处理原型对象中的数据成员(本例中为_n)?

    如果您需要使用Base作为基础,您需要将其设计为合理地接受零参数,或者您需要在为Derived创建基础对象时使用参数调用它。这些基本上是您仅有的两个选择。

    Derived.prototype = new Base; 正在创建Base 的一个实例,它会一直保留在内存中(假设Derived 是在全局空间中定义的)。如果Base 类非常昂贵,我不想要额外的对象怎么办?

    这与 Java 类中的 static 数据相同:加载类会加载该数据。如果您打算使用Base 作为基础,您需要将其设计为不会加载一堆它不需要的东西(也许通过处理零-参数版本不同于带参数版本)。

    这是您通常在 JavaScript 的“类”系统中看到的最后一种方法(处理零参数构造与带参数构造不同)。通常,您会看到仅用于构造原始对象的实际构造函数,以及用于实际初始化实例的其他一些命名函数(initialize 是 Prototype 使用的名称,我在做 my replacement/revision of Prototype's mechanism 时使用的名称)。所以实际的构造函数没有参数,但是你可以通过调用initialize 函数来初始化一个实例(它又调用它的基类的initialize 函数)。在大多数包装器中,这是在幕后为您处理的。

    使构造函数与初始化器机制在实践中起作用需要一些棘手的管道,因为它需要“超级调用”(对函数的基版本的调用),而超级调用在 JavaScript 中很尴尬。 (这 - 超级调用 - 实际上是链接文章的主要内容,但探索一种有效的方法也涉及创建/更新整个继承系统。我真的需要更新那篇文章,所以它不会'不使用基于类的术语;它仍然是原型,它只是提供了我所说的管道。)

    因为外部资源可能会消失/被移动/等等,而 Stack Overflow 主要是独立的,这是the article linked above 中提出的迭代的最终结果:

    // Take IV: Explicitly handle mixins, provide a mixin for calling super when
    // working with anonymous functions.
    // Inspired by Prototype's Class class (http://prototypejs.org)
    // Copyright (C) 2009-2010 by T.J. Crowder
    // Licensed under the Creative Commons Attribution License 2.0 (UK)
    // http://creativecommons.org/licenses/by/2.0/uk/
    var Helper = (function(){
        var toStringProblematic,    // true if 'toString' may be missing from for..in
            valueOfProblematic;     // true if 'valueOf' may be missing from for..in
    
        // IE doesn't enumerate toString or valueOf; detect that (once) and
        // remember so makeClass can deal with it. We do this with an anonymous
        // function we don't keep a reference to to minimize what we keep
        // around when we're done.
        (function(){
            var name;
    
            toStringProblematic = valueOfProblematic = true;
            for (name in {toString: true, valueOf: true}) {
                if (name == 'toString') {
                    toStringProblematic = false;
                }
                if (name == 'valueOf') {
                    valueOfProblematic = false;
                }
            }
        })();
    
        // This function is used to create the prototype object for our generated
        // constructors if the class has a parent class. See makeConstructor for details.
        function protoCtor() { }
    
        // Build and return a constructor; we do this with a separate function
        // to minimize what the new constructor (a closure) closes over.
        function makeConstructor(base) {
    
            // Here's our basic constructor function (each class gets its own, a
            // new one of these is created every time makeConstructor is called).
            function ctor() {
                // Call the initialize method
                this.initialize.apply(this, arguments);
                }
    
            // If there's a base class, hook it up. We go indirectly through `protoCtor`
            // rather than simply doing "new base()" because calling `base` will call the base
            // class's `initialize` function, which we don't want to execute. We just want the
            // prototype.
            if (base) {
                protoCtor.prototype = base.prototype;
                ctor.prototype = new protoCtor();
                protoCtor.prototype = {};   // Don't leave a dangling reference
            }
    
            // Set the prototype's constructor property so `this.constructor` resolves
            // correctly
            ctor.prototype.constructor = ctor;
    
            // Flag up that this is a constructor (for mixin support)
            ctor._isConstructor = true;
    
            // Return the newly-constructed constructor
            return ctor;
        }
    
        // This function is used when a class doesn't have its own initialize
        // function; since it does nothing and can only appear on base classes,
        // all instances can share it.
        function defaultInitialize() {
        }
    
        // Get the names in a specification object, allowing for toString and
        // valueOf issues
        function getNames(members) {
            var names,      // The names of the properties in 'members'
                name,       // Each name
                nameIndex;  // Index into 'names'
    
            names = [];
            nameIndex = 0;
            for (name in members) {
                names[nameIndex++] = name;
            }
            if (toStringProblematic && typeof members.toString != 'undefined') {
                names[nameIndex++] = 'toString';
            }
            if (valueOfProblematic && typeof members.valueOf != 'undefined') {
                names[nameIndex++] = 'valueOf';
            }
            return names;
        }
    
        // makeClass: Our public "make a class" function.
        // Arguments:
        // - base: An optional constructor for the base class.
        // - ...:  One or more specification objects containing properties to
        //         put on our class as members; or functions that return
        //         specification objects. If a property is defined by more than one
        //         specification object, the last in the list wins.
        // Returns:
        //     A constructor function for instances of the class.
        //
        // Typical use will be just one specification object, but allow for more
        // in case the author is drawing members from multiple locations.
        function makeClass() {
            var base,       // Our base class (constructor function), if any
                argsIndex,  // Index of first unused argument in 'arguments'
                ctor,       // The constructor function we create and return
                members,    // Each members specification object
                names,      // The names of the properties in 'members'
                nameIndex,  // Index into 'names'
                name,       // Each name in 'names'
                value,      // The value for each name
                baseValue;  // The base class's value for the name
    
            // We use this index to keep track of the arguments we've consumed
            argsIndex = 0;
    
            // Do we have a base?
            if (typeof arguments[argsIndex] == 'function' &&
                arguments[argsIndex]._isConstructor) {
                // Yes
                base = arguments[argsIndex++];
            }
    
            // Get our constructor; this will hook up the base class's prototype
            // if there's a base class, and mark the new constructor as a constructor
            ctor = makeConstructor(base);
    
            // Assign the members from the specification object(s) to the prototype
            // Again, typically there's only spec object, but allow for more
            while (argsIndex < arguments.length) {
                // Get this specification object
                members = arguments[argsIndex++];
                if (typeof members == 'function') {
                    members = members();
                }
    
                // Get all of its names
                names = getNames(members);
    
                // Copy the members
                for (nameIndex = names.length - 1; nameIndex >= 0; --nameIndex) {
                    name = names[nameIndex];
                    value = members[name];
                    if (base && typeof value == 'function' && !value._isMixinFunction) {
                        baseValue = base.prototype[name];
                        if (typeof baseValue == 'function') {
                                value.$super = baseValue;
                        }
                    }
                    ctor.prototype[name] = value;
                }
            }
    
            // If there's no initialize function, provide one
            if (!('initialize' in ctor.prototype)) {
                // Note that this can only happen in base classes; in a derived
                // class, the check above will find the base class's version if the
                // subclass didn't define one.
                ctor.prototype.initialize = defaultInitialize;
            }
    
            // Return the constructor
            return ctor;
        }
    
        // makeMixin: Our public "make a mixin" function.
        // Arguments:
        // - ...:  One or more specification objects containing properties to
        //         put on our class as members; or functions that return
        //         specification objects. If a property is defined by more than one
        //         specification object, the last in the list wins.
        // Returns:
        //     A specification object containing all of the members, flagged as
        //     mixin members.
        function makeMixin() {
            var rv,         // Our return value
                argsIndex,  // Index of first unused argument in 'arguments'
                members,    // Each members specification object
                names,      // The names in each 'members'
                value;      // Each value as we copy it
    
            // Set up our return object
            rv = {};
    
            // Loop through the args (usually just one, but...)
            argsIndex = 0;
            while (argsIndex < arguments.length) {
                // Get this members specification object
                members = arguments[argsIndex++];
                if (typeof members == 'function') {
                    members = members();
                }
    
                // Get its names
                names = getNames(members);
    
                // Copy its members, marking them as we go
                for (nameIndex = names.length - 1; nameIndex >= 0; --nameIndex) {
                    name = names[nameIndex];
                    value = members[name];
                    if (typeof value == 'function') {
                        value._isMixinFunction = true;
                    }
                    rv[name] = value;
                }
            }
    
            // Return the consolidated, marked specification object
            return rv;
        }
    
        // Return our public members
        return {
            makeClass: makeClass,
            makeMixin: makeMixin
            };
    })();
    

    用法:

    var Parent = Helper.makeClass(function(){
        function hierarchy() {
            return "P";
        }
        return {hierarchy: hierarchy};
    });
    var Child = Helper.makeClass(Parent, function(){
        function hierarchy() {
            return hierarchy.$super.call(this) + " < C";
        }
        return {hierarchy: hierarchy};
    });
    var GrandChild = Helper.makeClass(Child, function(){
        function hierarchy() {
            return hierarchy.$super.call(this) + " < GC";
        }
        return {hierarchy: hierarchy};
    });
    var gc = new GrandChild();
    alert(gc.hierarchy()); // Alerts "P < C < GC"
    

    如果您不喜欢超级调用的 funcname.$super.call(...) 表示法,这里有一个混合选项,可以让您使用更短/更清晰的版本(但会产生运行时成本):

    // Define our CallSuper mixin
    Helper.CallSuperMixin = makeMixin(function() {
        function callSuper(ref) {
            var f,          // The function to call
                args,       // Arguments to pass it, if we have any
                len,        // Length of args to pass
                srcIndex,   // When copying, the index into 'arguments'
                destIndex,  // When copying args, the index into 'args'
                rv;         // Our return value
    
            // Get the function to call: If they pass in a function, it's the
            // subclass's version so look on $super; otherwise, they've passed
            // in 'arguments' and it's on arguments.callee.$super.
            f = typeof ref == 'function' ? ref.$super : ref.callee.$super;
    
            // Only proceed if we have 'f'
            if (f) {
                // If there are no args to pass on, use Function#call
                if (arguments.length == 1) {
                    rv = f.call(this);
                } else {
                    // We have args to pass on, build them up.
                    // Note that doing this ourselves is more efficient on most
                    // implementations than applying Array.prototype.slice to
                    // 'arguments', even though it's built in; the call to it
                    // is expensive (dramatically, on some platforms).
                    len = arguments.length - 1;
                    args = new Array(len);
                    srcIndex = 1;
                    destIndex = 0;
                    while (destIndex < len) {
                        args[destIndex++] = arguments[srcIndex++];
                    }
    
                    // Use Function#apply
                    rv = f.apply(this, args);
                }
            }
    
            // Done
            return rv;    // Will be undefined if there was no 'f' to call
        }
    
        return {callSuper: callSuper};
    });
    

    再一次,我真的需要更新术语,所以它不是基于类的。 (也许看看 ECMAScript5 是如何让我们做的事情略有不同,因为它添加了一些有用的东西,比如直接控制原型。)

    【讨论】:

    • 恕我直言,您的解决方案过于矫枉过正。我建议你看看some of my OO articlesaxel's prototypes as classes。您对我们如何使用 ES5 做事的支持。
    • @Raynos:上面没有使用ECMAScript5,因为A)它是在标准完成之前编写的,更重要的是B)你还不能在现实世界的网页中使用ECMAScript5。我大约 3/4 的 JavaScript 是针对网页中的实际使用(其余的是服务器端),因此...另外,218 行(大量注释;2,115 个字符缩小)几乎没有 “严重矫枉过正。”
    • 感谢您提供非常好的详细解释。
    • 你是否反对使用 ES5-shim。此外, loc 并没有太大的杀伤力。当您可以使用 ES5 代替时,使用自定义抽象/类工厂是非常过分的。就我个人而言,我会说永远不需要类工厂,也不需要supersugar。
    • @Raynos:“你是否反对使用 ES5-shim。”* 好吧,这是另一个脚本,如果我不需要它是不必要的开销。但更重要的是,它没有提供我的Helper 提供的东西:高效、易于使用的超级调用。 ES5 有一些方法可以在某些地方略微改进Helper(特别是构造函数舞蹈可以替换为Object.create),但它并没有替换它。所以我可能会在某个时候开始在我的项目中使用 es5-shim,但我仍然想要我的 Helper
    【解决方案2】:

    @@@ 2. Derived.prototype = new Base;正在创建 Base 的实例 这将始终保留在内存中(假设 Derived 在 全球空间)。如果基类非常昂贵而我不这样做怎么办 想要一个额外的对象?

    是的。这个例子是继承学习风格。要在您的应用程序中使用,请尝试:

        function F() {}
        F.prototype = Base.prototype; // Linking to Base's prototype
    
        Derived.prototype = new F(); // The least memory-consumption object.
        Derived.prototype.constructor = Base; // Constructor reference correction
    

    @@@ 1.原型对象(本例中为_n)中的数据成员如何处理?

    使用上面的原型链,我们没有创建任何 Base 实例。所以,这个问题是无效的。

    【讨论】:

      【解决方案3】:

      但我不喜欢这行:

      Derived.prototype = new Base;

      然后换成

      Derived.prototype = Object.create(Base.prototype);

      参见Object.create 只是返回一个新对象,其[[Prototype]] 是您为其提供的第一个参数。

      基本上是说Derived 继承自Base 但不要调用那个该死的构造函数!

      如何处理原型对象(本例中为_n)中的数据成员?

      当您的链式原型不调用构造函数时!我为此写了一篇JS OO part 3 文章。

      它基本上说,当您创建对象时,您会实例化和初始化。

      // instantiate
      var o = Object.create(Base.prototype);
      // o now inherits all of Bases methods because o.[[Prototype]] === Base.prototype
      // o also inherits the constructor function (Base.prototype.constructor === Base)
      // initialize
      o.constructor(10);
      

      当然new X 两者兼而有之。以下是新功能的概览

      var new = function (constructor, ...args) {
        var instance = Object.create(constructor.prototype);
        instance.constructor(...args);
        return instance;
      }
      

      如您所见,您不需要 new,因为您不希望调用该构造函数(您不需要初始化 Derived.prototype)。

      Derived.prototype = new Base;正在创建 Base 的实例,这将始终保留在内存中(假设 Derived 在全局空间中定义)。如果 Base 类非常昂贵并且我不想要额外的对象怎么办?

      Object.create 的担忧是无效的。实例化一个对象很便宜。它只是生成一个新事物对象,其内部 [[Prototype]] 属性是指向您传入的原型的指针。

      唯一昂贵的就是构造函数,而且你不调用构造函数。

      次要免责声明:

      Object.create 是 ES5,一些旧版浏览器(主要是 IE8)不支持。然而,有一个可爱的东西叫做ES5-shim,它修复了这些浏览器并让它们表现得像 ES5。

      【讨论】:

      • 您可能应该提到 Object.create 是 ECMAScript5 功能的小细节,而不是在一些当前和大多数较旧的 JavaScript 引擎中。
      • @T.J.Crowder 我有将javascript 翻译成 ES5 并假设每个人都使用 ES5-shim 的习惯。
      • 感谢您指向 Object.create。我没想到这个。
      • “我有将 javascript 翻译成 ES5 并假设每个人都使用 ES5-shim 的习惯” 坏习惯。
      • @T.J.Crowder Meh,人们应该更具体。我为他的问题提供了一个非常明智的 javascript 解决方案。旧平台支持不是交易的一部分(但是,如果你想要它,ES5-shim 会这样做)。
      【解决方案4】:

      JavaScript 与其他“面向对象”语言有很大不同,请先尝试忘记您对其他语言的了解,然后了解 JavaScript 的工作原理。

      Douglas Crockford 的 JavaScript: The World's Most Misunderstood Programming Language 有一个很好的介绍,他还描述了继承在 JS 中的工作原理 here

      编辑:试图真正回答你的问题: 问题1,我完全不明白......对不起。 问题 2:JavaScript 中的经典继承相当丑陋,我也从未发现需要它。 Crockford 所说的“寄生遗传”(第二个链接)我认为可以解决这个问题。这里的“父”对象在构造函数的范围内被调用。

      【讨论】:

      • 这并不能回答他的问题,我会说(事实上,确实说过:-))他已经做得很好,忘记了他对其他语言的了解。跨度>
      • 我不太确定您真正要求的是什么,但您似乎在“用 Java 思考”,这可能会引起混淆。继承(is-a)在 Java 中往往是解决方案,在 JavaScript 中组合(has-a)往往更合适。
      • is-a vs. has-a 是红鲱鱼;两种语言都允许,并且在两种语言中都有自己的位置。我认为他在问什么非常清楚。
      • 继承(或缺少继承)的需要不是特定于语言的。您可以编写没有继承的 Java 代码;您可以编写 JavaScript 代码 继承。两者都做是有原因的。但除此之外,这也不能回答这个问题。 cmets 基本上是在“不要在 JavaScript 中使用继承”这个问题上说的。评论应该是 cmets,而不是答案。
      • @SteinG.Strindhaug “从来没有觉得需要在 javascript 中继承”然后去学习 JAVASCRIPT。严重地。原型真的很棒很强大。不要告诉别人学习 javascript。
      猜你喜欢
      • 1970-01-01
      • 2019-11-07
      • 2019-10-26
      • 1970-01-01
      • 1970-01-01
      • 2012-04-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多