Возможная ошибка в объяснении MDN свойства __proto__?

Итак, работая над дальнейшим укреплением своего понимания объектно-ориентированного JavaScript, я жадно читал, а затем тестировал то, чего не понимал. Я читал статью Mozilla Developer Network (MDN) под названием «Object.prototype.proto» по адресу: https://developer.mozilla.org/en-US/docs/Web./JavaScript/Reference/Global_Objects/Object/proto

и наткнулся на следующее объяснение:

Для объектов, созданных с помощью new fun, где fun — это функция, определенная в скрипте, это значение [__proto__] является значением fun.prototype во время оценки нового fun. (То есть, если для fun.prototype назначено новое значение, ранее созданные экземпляры fun будут по-прежнему иметь предыдущее значение в качестве своего [[Prototype]], а последующие новые вызовы fun будут использовать вновь присвоенное значение в качестве своего [[Прототип]].)

Примечание. MDN использует [[Prototype]] для обозначения «внутреннего» прототипа объекта, который в коде JavaScript обозначается как __proto__.

Итак, я открыл свою консоль Chrome и написал простой JavaScript:

function Person(name, age)
{
    this.name = name?name:"Parent Function";
    this.age = age?age:"Old as Time";
}

var parent = new Person("Ebeneezer", 42);    

//new Person evaluated before strength is added to Person.prototype
var child = new Person("Aluiscious", 12);

console.log(child.strength);

Person.prototype.strength = "your value here";
console.log(child.strength);

var second_child = new Person('Sprout', 5);
console.log(second_child.strength);

После этого, если я введу в консоль child.__proto__ и second_child.__proto__, я получу то же значение, то есть Person {strength: "ваше значение здесь"}

Согласно MDN, не должен ли child.__proto__ «продолжать иметь предыдущее значение» Person.prototype в качестве своего внутреннего прототипа?


person papiro    schedule 30.08.2014    source источник
comment
вы можете прочитать предыдущее значение как предыдущую ссылку   -  person Bryan Chen    schedule 30.08.2014
comment
Вы знаете, ваш комментарий действительно помог мне сделать первый шаг к пониманию ответа на мой вопрос, который я в конечном итоге нашел ниже. Я признателен за это!   -  person papiro    schedule 30.08.2014


Ответы (2)


Документы MDN говорят о полной замене прототипа, а не о добавлении к нему новых свойств или методов (которые будут добавлены ко всем объектам, использующим этот прототип, поскольку внутреннее свойство [[Prototype]] является общим). Рассмотрим этот пример:

function Person(name, age)
{
    this.name = name?name:"Parent Function";
    this.age = age?age:"Old as Time";
}

Person.prototype.strength = "some strength";
var parent = new Person("Ebeneezer", 42);

console.log(parent.strength); //"some strength"

//Replace `Person.prototype` with a completely new prototype object
Person.prototype = {
    //setting the 'constructor' property correctly when replacing a prototype object
    //is a best practice, but it will work without this too
    constructor: Person
};

console.log(parent.strength); //still "some strength"

var child = new Person("Aluiscious", 12);

//This will be undefined, because the object was created after the prototype was changed
console.log(child.strength);

В приведенном выше примере свойства [[Prototype]] экземпляров относятся к двум разным объектам-прототипам, поскольку я заменил прототип с помощью .prototype = перед созданием второго объекта.

Важно понимать, что свойство внутреннего прототипа является общим для всех экземпляров, созданных с использованием одного и того же прототипа. Вот почему в вашем примере свойство strength добавляется к обоим объектам — внутреннее свойство [[Prototype]] обоих объектов по-прежнему является ссылкой на один и тот же общий объект-прототип. Также важно понимать, что свойства объекта и массива прототипа также являются общими. Итак, например, предположим, что вы добавили массив children в свой прототип Person:

//Don't do this!
Person.prototype.children = [];
var parent1 = new Person("Ebeneezer", 42);
parent1.children.push(new Person("Child A"));

var parent2 = new Person("Noah", 35);
parent2.children.push(new Person("Child B"));

Вы могли бы ожидать, что это приведет к тому, что у Эбенизера будет массив, содержащий только дочерний элемент A, а у Ноя будет массив, содержащий только дочерний элемент B, но на самом деле оба родителя теперь будут иметь массив, содержащий ОБОИХ дочерних элементов A и дочерний элемент B, потому что children на самом деле ссылается в тот же массив, принадлежащий внутреннему объекту [[Prototype]].

Вот почему я считаю лучшей практикой всегда объявлять свойства данных в конструкторе и только методы в прототипе. Например:

function Person(name, age)
{
    this.name = name?name:"Parent Function";
    this.age = age?age:"Old as Time";
    this.children = [];
}

//it's fine to declare methods on the prototype - in fact it's good, because it saves
//memory, whereas if you defined them in the constructor there would be a separate copy
//of the method for each instance
Person.prototype.addChild = function(child) {
    if (!child instanceof Person) {
        throw new Error("child must be a Person object");
    }
    //Note: in a real system you would probably also want to check that the passed child
    //object isn't already in the array
    this.children.push(child);
}

Примечание. Концепция модификации или замены применяется не только к самим прототипам, но и к свойствам прототипа. Если вы зададите свойство непосредственно для объекта, оно будет использоваться вместо свойства в прототипе. Итак, если бы я изменил приведенный выше пример на это:

Person.prototype.children = [];
var parent1 = new Person("Ebeneezer", 42);
parent1.children.push(new Person("Child A"));

var parent2 = new Person("Noah", 35);
parent2.children = [];
//now `parent2` has its own `children` array, and Javascript will use that
//instead of the `children` property on the prototype.
parent2.children.push(new Person("Child B"));

... тогда у двух родителей будут отдельные массивы children, но, конечно, я упоминаю об этом только в иллюстративных целях, и вы должны объявить свойства массива или объекта в конструкторе, как я показал выше. В этом примере массив children для parent1 по-прежнему ссылается на свойство children в прототипе, поэтому, если вы создадите новый объект Person, он все равно разделит children с Ebeneezer:

var parent3 = new Person("Eve");
console.log(parent3.children); //array containing Child A

Эта статья также может помочь понять это: http://www.bennadel.com/blog/1566-using-super-constructors-is-critical-in-prototypal-inheritance-in-javascript.htm

person Matt Browne    schedule 30.08.2014
comment
Что ж, спасибо, Мэтт. Я бы проголосовал за вас, но у меня еще нет репутации 15! Это было очень ясное объяснение и определенный шаг для меня в моем путешествии ‹i›понимания‹/i› прототипного наследования :D - person papiro; 30.08.2014
comment
Кстати, я только что добавил ссылку в конец своего ответа, которая может быть полезна. - person Matt Browne; 30.08.2014
comment
Хорошая ссылка, но не рекомендуется создавать экземпляр Parent для установки прототипа Child. Родительский объект может иметь конкретные члены экземпляра, которых не должно быть в Child.prototype, или могут потребоваться переданные параметры (которые проверены), которые недоступны при объявлении дочернего типа. Лучше использовать Child.prototpe=Object.create(Parent.prototype) и polyfil Object.create, если это необходимо для очень старых браузеров. - person HMR; 30.08.2014
comment
@HMR Я думаю, что это недопонимание ... мой пример родителя / дочернего элемента предназначен не для наследования, а для дочерних объектов, которые принадлежат родительскому объекту (состав, а не наследование ). Возможно, мне следовало просто использовать массив целых чисел, чтобы избежать путаницы — я просто указывал, что если вы поместите массив в прототип, он будет общим для всех экземпляров этого прототипа. - person Matt Browne; 30.08.2014
comment
Для наследования, согласен с вами, лучше было бы использовать Object.create(Parent.prototype) - person Matt Browne; 30.08.2014
comment
В статье, на которую вы ссылаетесь, создается экземпляр Parent для установки прототипа Child. Ваш ответ хорош, но статья ошибочна в этом смысле. - person HMR; 31.08.2014
comment
Ах, хорошо, не понял, что вы говорили только о статье, а не о моем ответе; Спасибо за разъяснения. - person Matt Browne; 31.08.2014

Просто добавляю ответ, потому что это поведение применимо не только к прототипу, и должно быть ясно, в чем разница между удалением ссылки и изменением.

Я думаю, что правильными терминами являются de reference и mutate. Вы мутируете:

var org = {};
var copy1 = org;//copy1 is a reference to org
var copy2 = org;//copy2 is a reference to org
org.mutate=1;
console.log(copy1===org);//true
console.log(copy1===copy2);//true
console.log(copy2===org);//true
//basically copy1, copy2 and org all point to the same object
//so they all have a member called mutate with a value of 1
//because there is only one object with 3 variables referencing it.

Вот о чем говорит MDN (по ссылке):

var org = {orgVal:22};
var copy1 = org;//copy1 is a reference to org
var copy2 = org;//copy2 is a reference to org
//de reference copy1 and copy2
org={mutate:1};
console.log(copy1===org);//false
console.log(copy1===copy2);//true
console.log(copy2===org);//false
console.log(copy1.orgVal);//=22
//since copy1 and copy2 both still reference the same object
//  mutating copy1 will affect copy2
copy1.orgVal='changed';
console.log(copy2.orgVal);//='changed'

Удаление ссылки на прототип конструктора после создания множества экземпляров негативно влияет на производительность (см. здесь). Вот почему вы обычно не ссылаетесь на конструктор.прототип после создания экземпляров.

Изменение элементов прототипа или прототипа может привести к неожиданным результатам, как показано здесь (в разделе "Подробнее о прототипе"). Это может быть полезно до тех пор, пока вы знаете, почему вы это делаете и что на самом деле происходит.

Мэтт упомянул об этом в своем ответе, но проводит различие между данными и поведением там, где оно должно быть между общим и конкретным экземпляром. Могут быть общие данные, которые преднамеренно изменены в экземплярах, хотя использование статического члена обычно было бы лучше (Person.static=...). Даже в тех случаях, когда вы используете фабричный шаблон и не можете жестко запрограммировать имя конструктора, вы можете использовать someInstance.constructor.static (при условии, что вы не испортили прототип.конструктор при установке прототипа).

person HMR    schedule 30.08.2014
comment
+1 За различие между общим и экземпляром, а не между данными и поведением. Обычно данные для каждого экземпляра (за исключением статических свойств, и, как вы упомянули, обычно лучше сделать эти свойства функцией конструктора), а методы являются общими, но я должен был быть более ясным в этом. - person Matt Browne; 30.08.2014
comment
Было бы неплохо, если бы я мог объединить два предоставленных ответа, поскольку они оба представляют собой части полного ответа для меня. Я не буду менять принятый ответ, потому что я вышел из леса с ответом Мэтта. Спасибо, HMR, за разъяснения по этому вопросу! - person papiro; 01.09.2014