Crockford Prototypal inheritance - Проблемы с вложенными объектами

Я читал "Javascript: Good Parts" Дугласа Крокфорда - и, хотя это немного экстремально, я на борту с большим количеством того, что он должен сказать.

В главе 3 он обсуждает объекты и в какой-то момент излагает шаблон (также найденный здесь) для упрощения и исключения некоторых из путаница/проблемы, связанные с использованием встроенного ключевого слова "новое".

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

Итак, я попытался использовать это в проекте, над которым я работаю, и я заметил проблему при попытке наследовать от объектов, которые являются вложенными. Если я перезаписываю значение вложенного объекта, унаследованного с помощью этого шаблона, он перезаписывает вложенный элемент на всем протяжении цепи прототипа.

Пример Крокфорда похож на 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)

Я делаю что-то неправильно или это ограничение этого шаблона?

Ответы

Ответ 1

Нет несогласованности. Просто не думайте о вложенных объектах: прямое свойство объекта всегда либо на его прототипе, либо на собственном свойстве. Это не имеет значения, если значение свойства является примитивным или объектом.

Итак, когда вы делаете

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

что означает, что они не являются абсолютно независимыми, но все еще два разных объекта.

Ответ 2

Я думаю, что происходит, когда вы создаете person2, его свойства sex и info относятся к тем в nestObj. Когда вы ссылаетесь на person2.info, поскольку person2 не переопределяет свойство info, оно переходит к прототипу и изменяет объект там.

Похоже, что "правильный" способ сделать это так, как вы создаете person3, чтобы объект имел свой собственный объект info для изменения и не доходил до прототипа.

Я тоже читаю книгу (медленно), поэтому я сочувствую вам.:)

Ответ 3

Я изменил примеры, чтобы лучше продемонстрировать, что здесь происходит. Demo

Сначала мы создаем объект с тремя свойствами; Число, строка и объект с одним свойством со строковым значением.

Затем мы создаем второй объект из первого, используя 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"

Итак, num и str изменились на obj2, а не на 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 к созданному объекту для этого свойства будут перенаправлены на исходный объект.

Запрос set для obj2.obj.less = 'more' из предыдущего кодового блока сначала требует запроса get для obj2.obj, который не существует в obj2 в этой точке, поэтому он переходит в obj1.obj и в свою очередь, obj1.obj.less.

Затем, наконец, когда мы снова прочитаем obj2, мы все еще не установили obj2.obj, чтобы запрос get был перенаправлен на obj1.obj и возвратил ранее измененный параметр, что вызвало изменение эффекта свойство второго объекта объект-ребенок, похоже, изменяет оба, но на самом деле это только изменение первого.


Вы можете использовать эту функцию, чтобы вернуть новый объект, полностью отделенный от оригинала рекурсивно.

Demo

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"
}

Все работает точно так, как вы ожидали.