В программировании мы часто хотим что-то взять и расширить.

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

Прототипы — это базовая функция JavaScript, которая позволяет объектам наследовать и совместно использовать свойства между собой.

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

Что такое прототип

В JavaScript каждый объект имеет скрытое внутреннее свойство [[prototype]], которое либо ссылается на другой объект, либо имеет значение null.

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

  1. Сначала он проверяет свойство в самом объекте.
  2. Если он не может найти его там, то свойство ищется в прототипе объекта.
  3. А если и туда не попасть, то ищется прототип прототипа, и так до тех пор, пока либо свойство не будет найдено, либо не будет достигнут конец цепочки, и в этом случае возвращается undefined.

Поле [[prototype]] является скрытым полем, но мы можем установить его несколькими способами.

  1. obj.__proto__ : свойство __proto__ устарело и больше не должно использоваться.
  2. Object.setPrototypeOf(obj, proto) : устанавливает [[Prototype]] из obj на proto.
  3. Object.create(proto, [descriptors]): создает пустой объект с заданным proto как [[Prototype]] и необязательными дескрипторами свойств.

Метод Object.create немного мощнее, так как у него есть необязательный второй аргумент: дескрипторы свойств. Кроме того, изменение прототипа «на лету» с помощью Object.setPrototypeOf или obj.__proto__ — очень медленная операция, поскольку нарушает внутреннюю оптимизацию операций доступа к свойствам объекта.

По этим причинам мы будем использовать метод Object.create в остальной части статьи.

let animal = {
  eats: true
};

let cat = Object.create(animal, {
  meows: {
    value: true
  }
});

// we can find both properties in cat now:
console.log(cat.eats); // true 
console.log(cat.meows); // true

Как мы видим, когда мы пытаемся прочитать cat.eats, которого нет в cat, JavaScript следует за ссылкой [[Prototype]] и находит ее в animal.

Можно сказать, что animal является прототипом cat или cat прототипически наследуется от animal.

Вот как сильны прототипы. Если в animal много полезных свойств и методов, то они автоматически становятся доступными в cat.

Имейте в виду, что объекты могут иметь только один [[Prototype]]. Они не могут наследовать более чем от одного.

Прототипы не используются в письменной форме

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

В приведенном ниже примере мы назначаем собственное свойство dangerous для cat.

let animal = {
  eats: true,
  dangerous: true
};

let cat = Object.create(animal, {
  meows: {
    value: true
  }
});

console.log(animal.dangerous); // true

// Updating cat object
cat.dangerous = false; 

console.log(animal.dangerous); // true
console.log(cat.dangerous); // false

Отныне вызов cat.dangerous находит свойство сразу в объекте и выполняет его, не используя прототип.

Какова ценность «этого»?

Рассмотрим приведенный ниже пример.

let animal = {
  eats: true,
  doesItEat: function() {
    return this.eats;
  }
  
};

let cat = Object.create(animal, {
  meows: {
    value: true
  }
});

cat.eats = false;

console.log(animal.doesItEat()); // true
console.log(cat.doesItEat()); // false

Может возникнуть интересный вопрос: каково значение this в строке return this.eats. Будь то animal или cat?

Ответ прост: this всегда стоит перед точкой. На прототипы это никак не влияет.

Итак, строка cat.eats = false использует cat как this, а не animal.

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

Роль прототипов в цикле for…in

Цикл for..in также перебирает унаследованные свойства.

let animal = {
  eats: true
};

let cat = Object.create(animal, {
  meows: {
    value: true,
    enumerable: true
  }
});


// for..in loops over both own and inherited keys
for(let prop in cat) console.log(prop); // meows, eats

Если мы хотим исключить унаследованные свойства, мы можем использовать встроенный метод obj.hasOwnProperty(key), который возвращает true, если key является собственным свойством (не унаследованным).

let animal = {
  eats: true
};

let cat = Object.create(animal, {
  meows: {
    value: true,
    enumerable: true
  }
});

for(let prop in cat) {
  let isOwn = cat.hasOwnProperty(prop);

  if (isOwn) {
    console.log(`Our: ${prop}`); // Our: meows
  } else {
    console.log(`Inherited: ${prop}`); // Inherited: eats
  }
}

"Краткое содержание"

  • Все объекты имеют скрытое свойство [[Prototype]], которое либо ссылается на другой объект, либо имеет значение null.
  • Если мы хотим прочитать свойство или вызвать метод объекта, и если он не существует, то JavaScript пытается найти его в своей цепочке прототипов.
  • Операции записи/удаления не задействуют прототипы, они действуют непосредственно на объект.
  • Значение this всегда является объектом, вызывающим функцию.
  • Цикл for..in перебирает как собственные, так и унаследованные свойства.

Ура, ты дошел до конца. Надеюсь, ваши концепции сегодня немного прояснились. Оставайтесь с нами, чтобы увидеть больше таких сообщений.

Если вам понравилось, посмотрите и другие мои работы.

Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube,и Discord. Заинтересованы в Взлом роста? Ознакомьтесь с разделом Схема.