Когда я впервые перешел с Ruby на JavaScript, мне было сложно понять концепцию прототипа объекта и цепочку прототипов. Ключевое слово class, введенное в ES2015, является просто синтаксическим сахаром и не облегчает понимание.

Тем не менее, независимо от того, на основе прототипа или класса, причина иметь порядок разрешения цепочки или метода (в Python) состоит в том, чтобы установить связи между классами и лучше использовать отношения наследования.

Что касается наследования, в JavaScript есть только одна конструкция: объекты. У каждого объекта есть частное свойство, называемое prototype object, которое действует как объект шаблона, который наследует методы и свойства от другого объекта (например, __mro__ в Python). У этого объекта-прототипа есть собственный прототип, и так до тех пор, пока не будет достигнут объект с null в качестве прототипа. По определению, null не имеет прототипа и действует как последнее звено в этой цепочке прототипов. Перед достижением null предпоследняя остановка должна быть Object, поскольку почти все объекты в JavaScript являются экземплярами Object.

Как это работает? Что ж, давайте посмотрим на пример.

let f = function () {
   this.a = 1;
   this.b = 2;
}
let o = new f(); // {a: 1, b: 2}
f.prototype.b = 3;
f.prototype.c = 4;

Сейчас:

- o.[[Prototype]] имеет свойства b и c, и

- o.[[Prototype]].[[Prototype]]is Object.prototype, и

- o.[[Prototype]].[[Prototype]].[[Prototype]] имеет значение null.

Итак, вся цепочка собственности выглядит так:

{a: 1, b: 2} ---> {b: 3, c: 4} ---> Object.prototype ---> null

Обратите внимание, что {a: 1, b: 2} являются собственными свойствами объекта f, которые не наследуются ни от кого другого.

Когда мы пытаемся получить доступ к o.b, мы получаем значение 2. Почему не 3? Потому что .b существует как f собственное свойство, и если оно определено, то значение считывается, и обход цепочки прототипов здесь останавливается. Даже у прототипа есть свойство b, но оно не посещается. Это называется Затенение свойств.

Давайте рассмотрим более конкретный пример прохождения по цепочке прототипов.

function doSomething(){}
doSomething.prototype.foo = "bar";
var doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value";
console.log(doSomeInstancing.prop);     //some value
console.log(doSomeInstancing.foo);      // bar
console.log(doSomething.prop);          // undefined
console.log(doSomething.foo);           // undefined
console.log(doSomething.prototype.prop);// undefined
console.log(doSomething.prototype.foo); // bar

Как видно выше, __proto__ из doSomeInstancing равно doSomething.prototype.

Когда вы получаете доступ к свойству doSomeInstancing, браузер сначала проверяет, есть ли у doSomeInstancing это свойство.

Если doSomeInstancing не имеет свойства, браузер ищет свойство в __proto__ из doSomeInstancing (он же doSomething.prototype).

Если __proto__ doSomeInstancing не имеет свойства, тогда браузер ищет __proto__ из __proto__ doSomeInstancing, в данном случае window.Object.prototype.

Если свойство не найдено, просматривается __proto__ из __proto__ из __proto__ doSomeInstancing i, и в этом случае null.

Но __proto__ не существует на нуле. Таким образом, возвращается результат undefined.

Короче говоря, prototype - это свойство объекта Function. Это прототип объектов, созданных этой функцией.

__proto__ - внутреннее свойство объекта, указывающее на его прототип.

function example(x, y) {
    this.x = x;
    this.y = y;
}

Когда JavaScript выполняет этот код, он добавляет свойство prototype к example, свойство prototype - это объект с двумя свойствами:

constructor
__proto__

Итак, когда мы делаем example.prototype, он возвращается

constructor: example(x,y)
    __proto__: Object

Теперь, как вы можете видеть, constructor - это не что иное, как сама функция example, а __proto__ указывает на корневой уровень Object JavaScript.

var myExample = new example();

В строке выше мы создаем экземпляр example. Как это работает?

  • Сначала он создает новый пустой объект {}
  • Он создает __proto__ на myExample и указывает на example.prototype, поэтому myExample.__proto__ === example.prototype
  • Он выполняет example.prototype.constructor (что является определением функции example) с вновь созданным пустым объектом в качестве контекста (this), поэтому свойство x,y добавляется к вновь созданному объекту.
  • Возвращает вновь созданный объект
// the following are all true
myExample.__proto__ == example.prototype
myExample.__proto__.__proto__ == Object.prototype
myExample.__proto__.__proto__.__proto__ == null
myExample instanceof example;
myExample instanceof Object;

Существуют разные способы создания объектов и результирующей цепочки прототипов.

  • Объекты, созданные с помощью синтаксических конструкций
var o = {a: 1};
  • С конструктором

«Конструктор» в JavaScript - это «просто» функция, которая вызывается с помощью оператора new.

function Rectangle() {
  this.length = 2;
  this.width = 3;
}
Rectangle.prototype = {
  area: function() {
    this.length * this.width;
  }
};
var re = new Rectangle();
// re is an object with own properties 'length' and 'width'.
// re.[[Prototype]] is the value of Rectangle.prototype when new Rectangle() is executed.
  • С Object.create

В ECMAScript 5 появился новый метод: Object.create(). Вызов этого метода создает новый объект. прототип этого объекта является первым аргументом функции:

var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (inherited)
  • С ключевым словом class

ECMAScript 2015 представил новый набор ключевых слов, реализующих классы. Новые ключевые слова включают class, constructor, static, extends и super.

Будьте осторожны, открывая или изменяя prototype.

Обратите внимание, что __proto__ считается амортизированным. Если возможно, вам следует использовать следующие методы.

  • Object.create(proto, [descriptors]) - создает пустой объект с заданным proto as [[Prototype]] и дополнительными дескрипторами свойств.
  • Object.getPrototypeOf(obj) - возвращает [[Prototype]] из obj.
  • Object.setPrototypeOf(obj, proto) - устанавливает [[Prototype]] для obj на proto.

Но не изменяйте prototype объекта на лету, если это возможно, так как он должен создаваться только при создании объекта. Механизмы JavaScript оптимизированы для этого. Изменение prototype на более позднем этапе нарушит внутреннюю оптимизацию операций доступа к свойствам объекта.

Вот и все сегодня!

Приятного чтения!