Frontend blog.

Прототипы, наследование

В программировании часто возникает необходимость что-то расширить.

Например у нас есть объект user со своими свойствами и методами, и мы хотим создать объекты admin и guest как его слегка измененные варианты. Мы хотели бы повторно использовать то, что есть у объекта user, не копировать его методы, а создать новый объект на его основе.

Прототипное наследование — это возможность языка, которая помогает в этом.

[[Prototype]]

В JavaScript объекты имеют специальное скрытое свойство [[Prototype]], которое либо равно null, либо ссылается на другой объект. Этот объект называется «прототип».

Когда мы хотим прочитать свойство из object, а оно отсутствует, JavaScript автоматически берет его из прототипа.

Свойство [[Prototype]] является внутренним и скрытым, но есть много способов задать его.

Один из способов — использование __proto__:

let animal = {
  eats: true
}
let rabbit = {
  jumps: true
}

rabbit.__proto__ = animal; // (*)

// теперь мы можем найти оба свойства в rabbit:
console.log(rabbit.eats);  // -> true (**)
console.log(rabbit.jumps); // -> true

Здесь строка (*) устанавливает animal как прототип для rabbit.

Затем, когда console.log пытается прочитать свойство rabbit.eats (**), его нет в rabbit, поэтому JavaScript следует по ссылке [[Prototype]] и находит его в animal.

Так что если у animal много полезных свойств и методов, то они автоматически становятся доступными у rabbit. Такие свойства называются «унаследованными».

Если у нас есть метод в animal, он может быть вызван на rabbit:

let animal = {
  eats: true,
  walk() {
    console.log('Animal walk');
  }
}
let rabbit = {
  jumps: true,
  __proto__: animal
}

// walk взят из прототипа
rabbit.walk(); // -> Animal walk

Метод автоматически берется из прототипа. Цепочка прототипов может быть длиннее:

let animal = {
  eats: true,
  walk() {
    console.log('Animal walk');
  }
}
let rabbit = {
  jumps: true,
  __proto__: animal
}

let longEar = {
  earLength: 10,
  __proto__: rabbit
}

// walk взят из цепочки прототипов
longEar.walk(); // -> Animal walk
console.log(longEar.jumps); // -> true (из rabbit)

Есть некоторые ограничения:

  1. Ссылки не могут идти по кругу.
  2. Значение __proto__ может быть объектом или null. Другие типы игнорируются.
  3. Может быть только один [[Prototype]]. Объект не может наследоваться от двух других объектов.

Операция записи не использует прототип

Прототип используется только для чтения свойств.

Операции записи/удаления работают напрямую с объектом.

В приведенном ниже примере мы присваиваем rabbit собственный метод walk:

let animal = {
  eats: true,
  walk() {
    /* этот метод не будет использоваться в rabbit */
  }
};

let rabbit = {
  __proto__: animal
};

rabbit.walk = function () {
  console.log("Rabbit! Bounce-bounce!");
};

rabbit.walk(); // -> Rabbit! Bounce-bounce!

Теперь вызов rabbit.walk() находит метод непосредственно в объекте и выполняет его, не используя прототип.

Значение this

Прототипы никак не влияют на this.

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

Например, здесь animal представляет собой «хранилище методов», и rabbit использует его.

Вызов rabbit.sleep() устанавливает this.isSleeping для объекта rabbit:

// методы animal
let animal = {
  walk() {
    if (!this.isSleeping) {
      console.log('I walk');
    }
  },
  sleep() {
    this.isSleeping = true;
  }
};

let rabbit = {
  name: 'White Rabbit',
  __proto__: animal,
}

// модифицирует rabbit.isSleeping
rabbit.sleep();

console.log(rabbit.isSleeping); // -> true
console.log(animal.isSleeping); // -> undefined (нет такого свойства в прототипе)

Итого

  • В JavaScript все объекты имеют скрытое свойство [[Prototype]], которое является либо другим объектом, либо null.
  • Мы можем использовать obj.__proto__ для доступа к [[Prototype]].
  • Объект, на который ссылается [[Prototype]], называется «прототипом».
  • Если мы хотим прочитать свойство obj или вызвать метод, которого не существует у obj, тогда JavaScript попытается найти его в прототипе.
  • Операции записи/удаления работают непосредственно с объектом, они не используют прототип.
  • Если мы вызываем obj.method(), а метод при этом взят из прототипа, то this все равно ссылается на obj.

Конспект статьи из учебника по JavaScriptПрототипное наследование

вернуться к списку