Crockford的原型继承 - 嵌套对象存在的问题

14

我一直在阅读Douglas Crockford的《Javascript: The Good Parts》-虽然有些极端,但我赞同他很多观点。

在第三章中,他讨论了对象,并在某个时候提出了一个模式(也可以在这里找到),用于简化和避免使用内置“new”关键字带来的某些混乱/问题。

if (typeof Object.create !== 'function') {
    Object.create = function (o) {
        function F() {}
        F.prototype = o;
        return new F();
    };
}
newObject = Object.create(oldObject);

我试图在一个项目中使用这个模式,但是当尝试继承嵌套对象时遇到了问题。如果使用这个模式继承的嵌套对象的值被覆盖,它会一直覆盖到原型链的最上层。

Crockford 的示例类似于以下示例中的 flatObj,它运行良好。然而,它与嵌套对象的行为不一致:

var flatObj = {
    firstname: "John",
    lastname: "Doe",
    age: 23
}
var person1 = Object.create(flatObj);

var nestObj = {
    sex: "female",
    info: {
        firstname: "Jane",
        lastname: "Dough",
        age: 32  
    }
}
var person2 = Object.create(nestObj);

var nestObj2 = {
    sex: "male",
    info: {
        firstname: "Arnold",
        lastname: "Schwarzenneger",
        age: 61  
    }
}
var person3 = {
    sex: "male"
}
person3.info = Object.create(nestObj2.info);

// now change the objects:
person1.age = 69;
person2.info.age = 96;
person3.info.age = 0;

// prototypes should not have changed:
flatObj.age // 23
nestObj.info.age // 96 ???
nestObj2.info.age // 61

// now delete properties:
delete person1.age;
delete person2.info.age;
delete person3.info.age;

// prototypes should not have changed:
flatObj.age // 23
nestObj.info.age // undefined ???
nestObj2.info.age // 61

这个模式有限制吗?我是做错了什么吗?(在fiddle上也可以看到)


3个回答

11

不存在不一致性。只需要不考虑嵌套对象:一个对象的直接属性总是在其原型或自有属性上。属性值是基本类型还是对象无关紧要。

因此,当你执行

var parent = {
    x: {a:0}
};
var child = Object.create(parent);

child.x 将引用与parent.x 相同的对象 - 一个 {a:0} 对象。当你改变它的属性时:

var prop_val = child.x; // == parent.x
prop_val.a = 1;

两者都会受到影响。要独立地更改“嵌套”属性,您首先必须创建一个独立的对象:

child.x = {a:0};
child.x.a = 1;
parent.x.a; // still 0

你可以做的是

child.x = Object.create(parent.x);
child.x.a = 1;
delete child.x.a; // (child.x).a == 0, because child.x inherits from parent.x
delete child.x; // (child).x.a == 0, because child inherits from parent

这意味着它们不是完全独立的,但仍然是两个不同的对象。


这样解释清楚了很多 - 我现在明白问题了。谢谢 :) 因为所有对象都是引用...傻瓜。但是这种方式仍然不太方便。哦,算了。 - 1nfiniti
“一个对象的直接属性始终在其原型上或者是自有属性中。” - 对我而言,直接属性指的就是对象的自有属性,而它原型链上的属性可以称为“间接”属性。您能解释一下什么是直接属性,或者什么是“间接”属性吗? - T J
@TJ 我想我在这里使用“直接”的术语是指“嵌套”的相反。就是说,属性链中的单个级别。 - Bergi

1

我已经修改了示例,以便更好地演示这里发生的情况。 演示

首先,我们创建一个具有三个属性的对象;一个数字,一个字符串和一个具有一个字符串值属性的对象。

然后,我们使用Object.create()从第一个对象创建第二个对象;

var obj1 = { 
    num : 1,
    str : 'foo',
    obj : { less: 'more' }
};
var obj2 = Object.create( obj1 );

console.log( '[1] obj1:', obj1 );
console.log( '[1] obj2:', obj2 );

"[1] obj1:"
[object Object] {
  num: 1,
  obj: [object Object] {
    less: "more"
  },
  str: "foo"
}
"[1] obj2:"
[object Object] {
  num: 1,
  obj: [object Object] {
    less: "more"
  },
  str: "foo"
}

看起来不错,对吧?我们有了第一个对象和一个复制的第二个对象。

不要着急,让我们看看当我们改变第一个对象的一些值时会发生什么。

obj1.num = 3;
obj1.str = 'bar';
obj1.obj.less = 'less';

console.log( '[2] obj1:', obj1 );
console.log( '[2] obj2:', obj2 );

"[2] obj1:"
[object Object] {
  num: 3,
  obj: [object Object] {
    less: "less"
  },
  str: "bar"
}
"[2] obj2:"
[object Object] {
  num: 3,
  obj: [object Object] {
    less: "less"
  },
  str: "bar"
}

现在我们有了第一个对象,并进行了更改,还有一个该对象的副本。这里发生了什么?
让我们检查这些对象是否拥有它们自己的属性。
for( var prop in obj1 ) console.log( '[3] obj1.hasOwnProperty( ' + prop + ' ): ' + obj1.hasOwnProperty( prop ) );
for( var prop in obj2 ) console.log( '[3] obj2.hasOwnProperty( ' + prop + ' ): ' + obj2.hasOwnProperty( prop ) );

"[3] obj1.hasOwnProperty( num ): true"
"[3] obj1.hasOwnProperty( str ): true"
"[3] obj1.hasOwnProperty( obj ): true"
"[3] obj2.hasOwnProperty( num ): false"
"[3] obj2.hasOwnProperty( str ): false"
"[3] obj2.hasOwnProperty( obj ): false"

obj1有我们定义的所有属性,而obj2没有。

当我们改变obj2的一些属性时会发生什么?

obj2.num = 1;
obj2.str = 'baz';
obj2.obj.less = 'more';

console.log( '[4] obj1:', obj1 );
console.log( '[4] obj2:', obj2 );
for( var prop in obj1 ) console.log( '[4] obj1.hasOwnProperty( ' + prop + ' ): ' + obj1.hasOwnProperty( prop ) );
for( var prop in obj2 ) console.log( '[4] obj2.hasOwnProperty( ' + prop + ' ): ' + obj2.hasOwnProperty( prop ) );

"[4] obj1:"
[object Object] {
  num: 3,
  obj: [object Object] {
    less: "more"
  },
  str: "bar"
}
"[4] obj2:"
[object Object] {
  num: 1,
  obj: [object Object] {
    less: "more"
  },
  str: "baz"
}
"[4] obj1.hasOwnProperty( num ): true"
"[4] obj1.hasOwnProperty( str ): true"
"[4] obj1.hasOwnProperty( obj ): true"
"[4] obj2.hasOwnProperty( num ): true"
"[4] obj2.hasOwnProperty( str ): true"
"[4] obj2.hasOwnProperty( obj ): false"

所以,numstrobj2 上发生了改变,而不是在 obj1 上发生了改变,正如我们想要的那样,但是 obj1.obj.less 发生了改变,而它不应该改变。
hasOwnProperty() 检查中可以看出,即使我们改变了 obj2.obj.less,我们没有先设置 obj2.obj。这意味着我们仍然在引用 obj1.obj.less
让我们从 obj1.obj 创建一个对象,并将其分配给 obj2.obj,看看是否能得到我们想要的结果。
obj2.obj = Object.create( obj1.obj );

console.log( '[5] obj1:', obj1 );
console.log( '[5] obj2:', obj2 );
for( var prop in obj1 ) console.log( '[5] obj1.hasOwnProperty( ' + prop + ' ): ' + obj1.hasOwnProperty( prop ) );
for( var prop in obj2 ) console.log( '[5] obj2.hasOwnProperty( ' + prop + ' ): ' + obj2.hasOwnProperty( prop ) );

"[5] obj1:"
[object Object] {
  num: 3,
  obj: [object Object] {
    less: "more"
  },
  str: "bar"
}
"[5] obj2:"
[object Object] {
  num: 1,
  obj: [object Object] {
    less: "more"
  },
  str: "baz"
}
"[5] obj1.hasOwnProperty( num ): true"
"[5] obj1.hasOwnProperty( str ): true"
"[5] obj1.hasOwnProperty( obj ): true"
"[5] obj2.hasOwnProperty( num ): true"
"[5] obj2.hasOwnProperty( str ): true"
"[5] obj2.hasOwnProperty( obj ): true"

很好,现在obj2有了自己的obj属性。现在让我们看看当我们改变obj2.obj.less时会发生什么。

obj2.obj.less = 'less';

console.log( '[6] obj1:', obj1 );
console.log( '[6] obj2:', obj2 );

"[6] obj1:"
[object Object] {
  num: 3,
  obj: [object Object] {
    less: "more"
  },
  str: "bar"
}
"[6] obj2:"
[object Object] {
  num: 1,
  obj: [object Object] {
    less: "less"
  },
  str: "baz"
}

所以,这告诉我们的是,如果在创建的对象上尚未更改属性,则对于该属性的任何“get”请求将转发到原始对象。
从先前的代码块中对“obj2.obj.less ='more'”进行的“set”请求首先需要对“obj2.obj”的“get”请求,在那时它不存在于“obj2”中,因此它会转发到“obj1.obj”,进而是“obj1.obj.less”。
最后,当我们再次读取“obj2”时,我们仍然没有设置“obj2.obj”,因此该“get”请求将被转发到“obj1.obj”,并返回我们先前更改的设置,导致第二个对象的子对象的更改属性似乎同时更改了两个对象,但实际上只是实际更改了第一个对象。
你可以使用此函数递归地返回一个与原始对象完全分离的新对象。

演示

var obj1 = { 
    num : 1,
    str : 'foo',
    obj : { less: 'more' }
};
var obj2 = separateObject( obj1 );

function separateObject( obj1 ) {

    var obj2 = Object.create( Object.getPrototypeOf( obj1 ) );
    for(var prop in obj1) {
        if( typeof obj1[prop] === "object" )
            obj2[prop] = separateObject( obj1[prop] );
        else
            obj2[prop] = obj1[prop];
    }

    return obj2;
}

console.log( '[1] obj1:', obj1 );
console.log( '[1] obj2:', obj2 );
for( var prop in obj1 ) console.log( '[1] obj1.hasOwnProperty( ' + prop + ' ): ' + obj1.hasOwnProperty( prop ) );
for( var prop in obj2 ) console.log( '[1] obj2.hasOwnProperty( ' + prop + ' ): ' + obj2.hasOwnProperty( prop ) );

"[1] obj1:"
[object Object] {
  num: 1,
  obj: [object Object] {
    less: "more"
  },
  str: "foo"
}
"[1] obj2:"
[object Object] {
  num: 1,
  obj: [object Object] {
    less: "more"
  },
  str: "foo"
}
"[1] obj1.hasOwnProperty( num ): true"
"[1] obj1.hasOwnProperty( str ): true"
"[1] obj1.hasOwnProperty( obj ): true"
"[1] obj2.hasOwnProperty( num ): true"
"[1] obj2.hasOwnProperty( str ): true"
"[1] obj2.hasOwnProperty( obj ): true"

现在让我们看看当我们改变一些变量时会发生什么。

obj1.num = 3;
obj1.str = 'bar';
obj1.obj.less = 'less';

console.log( '[2] obj1:', obj1 );
console.log( '[2] obj2:', obj2 );

"[2] obj1:"
[object Object] {
  num: 3,
  obj: [object Object] {
    less: "less"
  },
  str: "bar"
}
"[2] obj2:"
[object Object] {
  num: 1,
  obj: [object Object] {
    less: "more"
  },
  str: "foo"
}

所有的功能都能按照您的预期正常工作。


console.log 对象使用哪个控制台来打印,而不显示继承结构? - Bergi
对于 separateObject,我建议使用 Object.create(Object.getPrototypeOf(obj1)) - Bergi
我复制了JSBin控制台的输出。我刚刚尝试了Object.getPrototypeOf(obj1),但它返回了一个空对象。我有什么遗漏吗? - user4639281
当然,这就是重点 - obj1 有一个空原型,所以 obj2 也应该有。不过你需要修复循环并使用 for (var prop in obj1) - Bergi
我现在明白你的意思了。我已经更新了我的回答。 - user4639281

1

我认为正在发生的是,当你创建person2时,它的sexinfo属性指向nestObj中的那些。当你引用person2.info时,由于person2没有重新定义info属性,它会通过原型进行修改。

看起来做到这一点的"正确"方法是构建person3的方式,使对象有自己的info对象进行修改,而不是上溯到原型。

我也在慢慢地阅读这本书,所以我能理解你的感受。 :)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接