09. Prototype Chain

Inheritance and the prototype chain

Each object has a private property (referred to as [[Prototype]] ) which holds a link to another object called its prototype. That prototype object has a prototype of its own, and so on until an object is reached with null as its prototype. By definition, null has no prototype, and acts as the final link in this prototype chain.

Define object prototype via __proto__

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

const a = { magic: 42 };
const b = { __proto__: a, super: 55 };
b.super; // => 55
b.magic; // => 42
b.constructor; // => Object
b.__proto__; // => {magic: 42}
b.isPrototypeOf(a); // => false
a.isPrototypeOf(b); // => true
a.__proto__ === Object.prototype; // => true

Notice that the usage of object.__proto__ as a getter/setter is not recommended. Alternatives Object.getPrototypeOf() and Object.setPrototypeOf() or Object.create(prototype) should be considered instead.

  • Object.getPrototypeOf(obj) - method returns the direct prototype (i.e. the value of the internal [[Prototype]] property) of the specified object. TypeError if obj is not an object.

  • Object.setPrototypeOf(obj, prototype) - method sets the prototype of a specified object to another object or null.

The good part is that ES2015 allows to use the literal __proto__ as the property name to setup the prototype right in the object literal { __proto__: protoObject }. JavaScript constraints to use only an object or null as a value for __proto__ property.

const myProto = {
  propertyExists(name) {
    return name in this;
  }
};

const myNumbers = {
  __proto__: myProto,
  array: [1, 6, 7]
};

myNumbers.propertyExists("array"); // => true
myNumbers.propertyExists("collection"); // => false

The __proto__ property may not be supported. A simple check for the getter/setter:

// Simple check
const supported = {}.hasOwnProperty.call(Object.prototype, "__proto__");

// A more sophisticated check:
const desc = Object.getOwnPropertyDescriptor(Object.prototype, "__proto__");
const supported =
  typeof desc.get === "function" && typeof desc.set === "function";

Recommendations:

  • Use Object.getPrototypeOf() to get the prototype of an object.

  • Prefer Object.create() to create a new object with a given prototype. Avoid Object.setPrototypeOf(), which hampers performance on many engines.

  • I actually like __proto__ as an operator in an object literal. It is useful for demonstrating prototypal inheritance and for creating dictionary objects.

Access to object prototype via super

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

const animal = {
  walk() {
    console.log("i am walking");
  }
};

const rabbit = {
  __proto__: animal,

  walk() {
    super.walk();
    console.log("rabbit walk");
  }
};

Свойство super работает через прототип только в методах объекта. При обращении через super используется [[HomeObject]] текущего метода, и от него берётся __proto__. В частности, если переписать этот код, оформив rabbit.walk как обычное свойство-функцию, то будет ошибка. Исключением из этого правила являются функции-стрелки. В них используется super внешней функции. Например, здесь функция-стрелка в setTimeout берёт внешний super:

const animal = {
  walk() {
    console.log("I'm walking");
  }
};

const rabbit = {
  __proto__: animal,
  walk() {
    setTimeout(() => super.walk()); // I'm walking
  }
};

rabbit.walk();

Check prototype chain with obj.isPrototypeOf()

The proto.isPrototypeOf(obj) method checks if an proto exists in obj object's prototype chain.

const proto = {};
const obj = Object.create(proto);
const obj2 = Object.create(obj);

Object.getPrototypeOf(obj) === proto; // => true
Object.getPrototypeOf(obj2) === obj; // => true
Object.getPrototypeOf(obj2) === proto; // => false

obj2.isPrototypeOf(proto); // => false
proto.isPrototypeOf(obj2); // => true

Number.prototype.isPrototypeOf(2); // => false
Number.prototype.isPrototypeOf(new Number(2)); // => true
Number.prototype.isPrototypeOf(Object(2)); // => true

Access to properties

Доступ к любым свойствам объекта выполняется в контексте (наследования) цепочки прототипов.

Предположим, что программа обращается к свойству x объекта o. Если объект o не имеет собственного свойства с таким именем, выполняется попытка отыскать свойство x в прототипе объекта o. Если объект-прототип не имеет собственного свойства с этим именем, но имеет свой прототип, выполняется попытка отыскать свойство в прототипе прототипа. Так продолжается до тех пор, пока не будет найдено свойство x или пока не будет достигнут объект, не имеющий прототипа.

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

const proto = {
    describe() {
        return 'name: '+ this.name;
    }
};
const obj={
    __proto__: proto,
    name: 'obj'
};

> obj.describe
[Function]

> obj.describe();
"name: obj"

> obj.describe = function () { return 'overridden' };
> obj.describe()
'overridden'

При присваивании свойства всегда меняется текущий объект и никогда прототип. Присваивание свойства может не выполниться в некоторых случаях.

Операция присваивания значения свойству проверит наличие этого свойства в цепочке прототипов, чтобы убедиться в допустимости присваивания. Например, если объект o наследует свойство x, доступное только для чтения, то присваивание выполняться не будет. Однако если присваивание допустимо, всегда создается или изменяется свойство в оригинальном объекте и никогда в цепочке прототипов.

Тот факт, что механизм наследования действует при чтении свойств, но не действует при записи новых значений, является ключевой особенностью языка JavaScript, потому что она позволяет выборочно переопределять унаследованные свойства.

Существует одно исключение из этого правила, когда операция присваивания значения свойству терпит неудачу. Если объект o наследует свойство x и доступ к этому свойству осуществляется посредством методов доступа, то вместо создания нового свойства x в объекте o производится вызов метода записи нового значения. Однако обратите внимание, что метод записи вызывается относительно объекта o, а не относительно прототипа, в котором определено это свойство, поэтому, если метод записи определяет какие-либо свойства, они будут созданы в объекте o, а цепочка прототипов опять останется неизменной.

Dictionaries

Объекты без прототипа часто называются картами или dictionary pattern:

const dict = Object.create(null);

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

Last updated