Javascript Crockford风格原型模式gotcha;寻找一个优雅的解决方案

Javascript Crockford风格原型模式gotcha;寻找一个优雅的解决方案,javascript,prototype,Javascript,Prototype,在编写JavaScript程序时,我经常使用Crockford的原型模式。我原以为我了解所有涉及的“陷阱”,但我发现了一个我以前从未想过的陷阱。我想知道是否有人有处理这件事的最佳方法 下面是一个简单的例子: // Here's the parent object var MyObject = { registry: {}, flatAttribute: null, create: function () { var o, F = function () {

在编写JavaScript程序时,我经常使用Crockford的原型模式。我原以为我了解所有涉及的“陷阱”,但我发现了一个我以前从未想过的陷阱。我想知道是否有人有处理这件事的最佳方法

下面是一个简单的例子:

// Here's the parent object
var MyObject = {
    registry: {},
    flatAttribute: null,
    create: function () {
        var o, F = function () {};

        F.prototype = this;
        o = new F();
        return o;
    }
};

// instance is an empty object that inherits
// from MyObject
var instance = MyObject.create();

// Attributes can be set on instance without modifying MyObject
instance.flatAttribute = "This is going to be applied to the instance";

// registry doesn't exist on instance, but it exists on
// instance.prototype. MyObject's registry attribute gets
// dug up the prototype chain and altered. It's not possible
// to tell that's happening just by examining this line.
instance.registry.newAttribute = "This is going to be applied to the prototype";

// Inspecting the parent object
// prints "null"
console.log(MyObject.flatAttribute);
// prints "This is going to be applied to the prototype"
console.log(MyObject.registry.newAttribute);
我想确保对实例所做的任何更改不会向上传播继承更改。当属性是对象且我正在设置嵌套属性时,情况并非如此

解决方案是重新初始化实例上的所有对象属性。然而,使用此模式的一个明显优点是从构造函数中删除重新初始化代码。我正在考虑克隆父对象的所有对象属性,并在create()函数的实例中设置它们:

{ create: function () {
    var o, a, F = function () {};

    F.prototype = this;
    o = new F();
    for (a in this) {
        if (this.hasOwnProperty(a) && typeof this[a] === 'object') {
            // obviously deepclone would need to be implemented
            o[a] = deepclone(this[a]);
        }
    }
    return o;
} };

有更好的方法吗?

这会给你带来预期的结果吗?在这里,我使用的不是对象文字,而是父对象(基)的即时实例化构造函数:

但这一切都有点虚幻。如果您不想使用
new
操作符(我认为这就是它的全部思想),那么下面提供了一种无需创建方法即可实现此目的的方法:

function Base2(){
     if (!(this instanceof Base2)){
         return new Base2;
     }
     this.registry = {}, 
     this.flatAttribute = null;
     if (!Base2.prototype.someMethod){
         var proto = Base2.prototype;
         proto.someMethod = function(){};
         //...etc
     }
}
//now the following does the same as before:
var  instance1 = Base2(),
     instance2 = Base2();

// assign a property to instance1.registry
instance1.registry.something = 'blabla';

// do the instance properties really belong to the instance?
console.log(instance1.registry.something); //=>  'blabla'
console.log(instance2.registry.something === undefined); //=>  true

示例在一个

中,有一个非常简单的解决方案来确保它们仅是实例变量,即在构造函数中使用this关键字

var MyObject = {
    flatAttribute: null,
    create: function () {
        var o, F = function () {
           this.registry  = {}
        };

        F.prototype = this;
        o = new F();
        return o;
   }
};
这确保了“instance.registry.*”的所有属性都是实例的本地属性,因为javascript对象的查找顺序如下所示

object -> prototype -> parent prototype ...
因此,通过向构造函数中名为“registry”的实例添加一个变量,该变量将始终首先找到

另一个我认为更优雅的解决方案是不使用crockford(java风格)构造函数,而是使用更自然地反映javascripts对象系统的布局。这些问题大多来自于实践和语言之间的不协调

// instance stuff
var F = function () {
   this.registry  = {}
};

F.prototype = {
    // static attributes here
    flatAttribute: null,
    methodA: function(){ 
        // code here 'this' is instance object
        this.att = 'blah';
    }
};

var instanceA = new F();
instanceA.registry['A'] = 'hi';
var instanceB = new F();
instanceB.registry['B'] = 'hello';

instanceA.registry.A == 'hi'; // true
instanceB.registry.B == 'hello'; // true
F.prototype.registry == undefined; // true

我认为您正在使用原型继承来模拟经典的、面向对象的继承

您试图做的是停止原型方法查找,这限制了它的表达能力,为什么要使用它呢?使用此功能模式可以达到相同的效果:

var MyObject = function() {
    // Declare here shared vars
    var global = "All instances shares me!";

    return {
        'create': function() {
            var flatAttribute;
            var register = {};

            return {
                // Declare here public getters/setters
                'register': (function() {
                    return register;
                })(),
                'flatAttribute': (function() {
                    return flatAttribute;
                })(),
                'global': (function() {
                    return global;
                })()
            };
        }
    };
}();


var instance1 = MyObject.create();
var instance2 = MyObject.create();

instance1.register.newAttr = "This is local to instance1";
instance2.register.newAttr = "This is local to instance2";
// Print local (instance) var
console.log(instance1.register.newAttr);
console.log(instance2.register.newAttr);
// Print global var
console.log(instance1.global);
console.log(instance2.global);

我总是想记住这个对象。创建是一种选择,而不是在javascript中实现非经典继承的唯一方法

就我自己而言,我总是发现Object.create在我想要从父对象原型链继承元素(即我希望能够应用于继承对象的方法)时效果最好

--

对于简单的“自有属性”继承,Object.create基本上是不必要的。当我想要继承自己的属性时,我更喜欢使用流行的Mixin&Extend模式(它只是将一个对象自己的属性复制到另一个对象,而不必担心原型或“新建”)

在Stoyan Stefanov的《Javascript模式》一书中,他给出了一个深度扩展函数的示例,该函数递归地实现了您想要的功能,包括对数组属性以及标准键/值对象的支持:

function extendDeep(parent, child){
    var i,
        toStr = Object.prototype.toString,
        astr = "[object Array]";

    child = child || {};

    for (i in parent) {
        if (parent.hasOwnProperty(i)) {
            if (typeof parent[i] === "object") {
                child[i] = (toStr.call(parent[i]) === astr) ? [] : {};
                extendDeep(parent[i], child[i]);
            } else { 
                child[i] = parent[i];
            }
        }
    }
    return child;
}

如果您使用的是jQuery,jQuery.extend()有一个可选的“deep”参数,可以让您以几乎相同的方式扩展对象。

我很困惑-您希望发生什么?你想让
MyObject
中的字段做什么?@Claudiu我想安全地设置实例的属性,而不让它们改变继承链下游的对象。如果属性是一个对象,并且我正在设置嵌套属性,则不一定会发生这种情况。代码使它看起来像您期望
注册表
实例
的属性-否则,您为什么希望它作为一个对象存在而不事先设置它?如果希望将其作为实例属性,则必须在构造函数中手动设置。当我阅读您的代码时,我并不感到惊讶,因为我假设您想要设置父对象的字段。简单的回答是,如果您不希望嵌套对象数据是静态的,那么您不应该在原型上存储嵌套对象数据。原型上的数据是共享的,并且是要共享的。如果您想要某个实例的私有内容,那么不要将其存储在原型中。@Claudiu和@Raynos:你们都是对的,我希望它能够神奇地初始化实例变量,但这不会发生。我混淆了范式。所有的答案都是好的,这是第一个,再加上第二个例子给了我一种新的方式来思考模式,接受。这完全是垃圾。在第一个示例中,您是否查看了实例的原型链?不要将
new
用于IEFE构造!第二个例子更好,但是把原型初始化的东西移出构造函数@Bergi:我承认代码可能会更好(编辑答案和小提琴),但我的判断不是完全的垃圾。构造函数中没有原型初始化的任何参数?那么您的替代方案是什么呢?代码会更清晰、更快,并且您不需要
protoInPlace
标志(或其他条件)。在构造函数闭包中使用原型方法在逻辑上也是错误的。您可以使用
Base=(function(){function MyObject(){this.registry={},this.flattattribute=null;}返回{create:function(){return new MyObject;}};}()),但我赞成FireCrow的答案。嗯,这些无用的IEF是干什么的?你为什么称他们为“接受者/接受者”?
function extendDeep(parent, child){
    var i,
        toStr = Object.prototype.toString,
        astr = "[object Array]";

    child = child || {};

    for (i in parent) {
        if (parent.hasOwnProperty(i)) {
            if (typeof parent[i] === "object") {
                child[i] = (toStr.call(parent[i]) === astr) ? [] : {};
                extendDeep(parent[i], child[i]);
            } else { 
                child[i] = parent[i];
            }
        }
    }
    return child;
}