【问题标题】:object in prototype is inherited as reference原型中的对象作为引用被继承
【发布时间】:2013-04-13 11:40:59
【问题描述】:

我想使用原型继承新的对象实例。

测试用例:

var MyObj = function() {}
MyObj.prototype.objName = {} 
// I want this to be a different object for each instance of MyObj

var o1 = new MyObj (),
    o2 = new MyObj ();

o1.objName['a'] = 1;
o2.objName['a'] = 2;

alert(o1.objName['a']) // 2
alert(o1.objName === o2.objName) // true

这意味着原型中的对象不是作为它的副本继承而是作为它的引用。

我知道通常你可以这样做。

var MyObj = function() {
    this.objName = {}
}

var o1 = new MyObj(),
    o2 = new MyObj();

alert(o1.objName === o2.objName) // false

这很好用,但在我的情况下,这不是一个选项。我确实需要在 MyObj 函数之外定义 objName。

我设法“解决”了这个问题

MyObj.prototype.objName = function() {
    if ( this._objName === undefined ) {
        this._objName = {};
    }
    return this._objName;
}

var o1 = new MyObj(),
    o2 = new MyObj();

o1.objName()['a'] = 1;
o2.objName()['a'] = 2;

alert(o1.objName()['a']) // 1

但这不是很漂亮,而且这段代码的性能要差得多。

有什么方法可以更优雅地解决这个问题吗?

【问题讨论】:

  • 为什么要在构造函数之外定义objName
  • 我正在制作一个库,允许用户使用某些功能扩展他的对象,我需要在他的对象中添加一些对象以支持附加功能。我不想通过强迫他自己声明这些属性来打扰用户。我希望我能理解地解释自己:)
  • 您为什么认为性能会很多变差?当然,还有一个额外的函数调用,但如果你不想使用构造函数,你不能阻止它。
  • 根据 www.jsperf.com 的数据,性能降低了 76%。

标签: javascript oop prototype


【解决方案1】:

这意味着原型中的对象不是作为它的副本被继承,而是作为它的引用。

原型上没有任何东西被复制——原型继承的整个概念是属性引用原型对象的共享属性。因此,如果您希望每个实例的属性都是独立的,则必须将其显式分配给对象并 shadow 原型属性;就像您在代码中使用 _objName 属性一样。

但这不是很漂亮,而且这段代码的性能要差得多。

如果你想要它漂亮,把它移到构造函数中(或者让构造函数寻找类似init的方法来调用,如果存在的话,那么你可以在原型上创建那个init方法。

为了让性能好一点,你可以把getter函数改成

MyObj.prototype.getObj = function() {
    var obj = {};
    this.getObj = function(){ return obj; }; // overwrite itself
    return obj;
};

虽然它仍然有函数调用开销。为了更加优雅,您可以使用 getter 属性(旧浏览器不支持),它会在第一次访问时自行删除:

Object.defineProperty(MyObj.prototype, "objName", {
    get: function() {
        var obj = {};
        Object.defineProperty(this, "objName", {
            value: obj,
            writable: true //?
        });
        return obj;
    },
    enumerable: true,
    configurable: true
});

现在你可以省略函数调用括号了。

【讨论】:

    【解决方案2】:

    这意味着原型中的对象不是作为它的副本继承而是作为它的引用。

    只是为了清楚。首先,在 JavaScript 中,所有对象都是通过引用传递的,而不是通过值传递的。只有原语是按值传递的。 其次,你实际上并不是在“复制”或“传递”任何东西。当您设置prototype 时,您正在创建原型链。这意味着在您的情况下:

    var MyObj = function() {};
    MyObj.prototype.objName = {} ;
    
    var o1 = new MyObj ();
    var o2 = new MyObj ();
    

    o1o2 都没有任何名为 objName 的属性,您可以简单地使用以下命令对其进行测试:

    console.log(Object.keys(o1)); // []
    

    当 JS 看到像 o1.objName 这样的代码时,首先检查对象是否具有此属性,如果有,则使用它。如果没有,则开始查找原型链,从o1的原型开始,即MyObj.prototype:找到属性objName,并返回。如果没有找到,那么JS会继续检查MyObj.prototype的原型,以此类推。所以,重点是:MyObj.prototype 这是一个对象:你在o1o2 之间共享了这个对象。这就是objName 的实例相同的原因。这与拥有的逻辑完全相同:

    function objName(obj) {
        return "objName" in obj ? obj.objName : O.objName;
    }
    
    var O = { objName: [] };
    var foo = {};
    var bar = {};
    
    objName(foo).push(0);
    objName(bar).push(1);
    

    因此,您不能放入prototype 任何不打算在使用该原型创建的对象之间共享的对象。我想说像这样的共享状态也是一种应该避免的不好的做法,这就是为什么一般prototype 不应该有这样的属性。 我仍然不清楚为什么你不能修改构造函数,但重点是:当你创建对象的实例时,你必须“设置”它。通常,调用构造函数,但任何函数都可以。当您想要支持继承并调用“超级”构造函数来初始化您的对象时,也会这样做。

    【讨论】:

    • 感谢您了解原型对我的作用。我认为原型对象实际上被复制(有点像类说明实例将具有哪些属性)到所有实例。我这边大失败:)
    • 不客气。 :) 尽管如此,即使在阅读了您对@Koterpillar 的评论之后,我也不知道您的用例是什么。如果您添加一些“真实案例示例”您的代码打算如何使用,也许我们可以更好地解决您的问题。
    • 实际上,甚至对象都不是通过引用传递的——它们像 JS 中的所有其他变量一样通过值传递。只是,对象是其属性映射的引用值。您可能会共享该引用,但您是按值传递它:-)
    • “通过引用”是指不复制该值。 “按值”表示复制值。这就是 JS 中的术语(例如,参见 developer.mozilla.org/en-US/docs/JavaScript/Glossary 中的“参数”)。当然,您可以将“对象引用”视为 C 中的指针,并且在底层不仅可以共享对象,而且出于性能原因,来自不同对象的不同属性也可以共享相同的描述符;但这是引擎的实现细节。 :)
    猜你喜欢
    • 1970-01-01
    • 2022-07-27
    • 1970-01-01
    • 2017-07-06
    • 2012-06-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多