Зарегистрируйтесь, чтобы продолжить обучение

Прототипы JS: Введение в ООП

Prototypes

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

Прототипы — это механизм, который оказывает основное влияние на то, как работают объекты в JavaScript. Сами они напрямую в коде используются редко (и обычно только в библиотеках), но их знание важно для понимания поведения кода и отладки. Особенно при работе с классами, которые мы изучим дальше по курсу. В этом уроке мы затронем только самые основы. Глубоко разобраться с прототипами поможет наш курс, указанный в дополнительных материалах.

В JavaScript с каждым объектом связан прототип. Прототип – это обычный объект, хранящийся в специальном служебном поле [[prototype]] (к этому полю невозможно обратиться напрямую). Его можно извлечь так:

const date = new Date()
// Эта функция извлекает прототип объекта из самого объекта
const proto = Object.getPrototypeOf(date) // Date {}

// В прототипе хранится не конструктор
// Что там хранится – узнаем дальше
proto === Date // false

const numbers = [1, 2]
Object.getPrototypeOf(numbers) // [] – отображение отличается, но это массив

// Прототипы есть и у конструкторов, которые мы определяем сами
function Company(name) {
  this.name = name
}

const company = new Company()
Object.getPrototypeOf(company) // Company {}

Для чего нужны прототипы

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

Если свойства в объекте нет, то JavaScript смотрит прототип этого объекта. Если в прототипе есть искомое свойство, то его значение возвращается. В итоге мы можем обратиться к свойству, которого нет в объекте, но есть в прототипе. И тогда мы получим из прототипа какое-то значение.

В реальности процесс еще сложнее. Если свойство не найдено в прототипе, то JavaScript смотрит прототип прототипа и так далее. Так он проходит до конца цепочки прототипов, то есть до последнего прототипа — это всегда null. На базе этого механизма реализуется наследование. Эта тема выходит за рамки текущего урока.

Прототипы есть даже у обычных JavaScript-объектов:

Object.getPrototypeOf({}) // {} — это и есть Object

Именно по этой причине даже пустые объекты содержат свойства и методы:

const obj = {} // То же самое можно сделать так: const obj = new Object();
// Это функция-конструктор, из которой был получен текущий объект, в нашем случае — Object
obj.constructor // [Function: Object]
// У obj нет своего собственного свойства constructor, оно пришло из прототипа
Object.hasOwn(obj, 'constructor') // false
Object.hasOwn(obj, 'name') // false
obj.name = 'hexlet'
// Имя есть в самом объекте, потому что мы его только что добавили
Object.hasOwn(obj, 'name') // true

Доступ к прототипу можно получить не только из объектов, но и из свойства prototype конструктора, который эти объекты создаёт:

function Company(name) {
  this.name = name
}

// Одно и то же, полученное разными способами
// Company.prototype === Object.getPrototypeOf(new Company())

Теперь мы можем ответить на вопрос, откуда берется прототип. Прототип – это объект, находящийся в свойстве prototype функции-конструктора, а не сам конструктор. Проверить работу прототипов достаточно легко, изменив их:

// Добавляем свойство getName (делаем его методом)
Company.prototype.getName = function getName() {
  // this по-прежнему зависит от контекста, в котором вызывается
  return this.name
}

const company = new Company('Hexlet')
// Свойство доступно!
console.log(company.getName()) // => Hexlet

При этом никто не мешает заменить значение свойства getName в конкретном объекте. Это никаким образом не отразится на других объектах, так как они извлекают getName из прототипа:

const company1 = new Company('Hexlet')
const company2 = new Company('Google')
company2.getName = function getName() {
  return 'Alphabet'
}

// Этот вызов возьмет свойство из самого объекта
company2.getName() // Alphabet
// Этот вызов возьмет значение свойства из прототипа
company1.getName() // Hexlet

Создание свойств через прототип – и есть правильный способ создания своих абстракций в JavaScript. Любая новая абстракция, которая нам нужна в коде, должна выглядеть как конструктор и прототип, наполненный нужными свойствами.

Что даёт этот механизм?

Самое простое – расширение ядра языка и библиотек без прямого доступа к исходному коду. Прототипы – невероятно гибкий механизм, который позволяет менять всё что угодно из любого места программы в рантайме, то есть во время работы. Например, мы можем добавить или заменить любые методы в любых объектах самого языка:

const numbers1 = [1, 3]

// Как только выполнится этот код, все массивы,
// включая уже созданные, обзаведутся методом last
Array.prototype.last = function last() {
  // Такое обращение сработает, ведь this — это ссылка на сам объект,
  // который в нашем случае является массивом
  return this[this.length - 1]
}

numbers1.last() // 3

const numbers2 = [10, 0, -2]
numbers2.last() // -2

// Пример замены
// Это запрещенный прием, никогда так не делайте в реальном коде
Array.prototype.map = function map() {
  return 'Ehu!'
}

numbers1.map() // "Ehu!"

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

// Очень злой код, который ломает работу метода push
Array.prototype.push = function push(value) {
  return this.unshift(value)
}

const numbers = [1, 2]
numbers.push(3)
console.log(numbers) // => [3, 1, 2] !!!

Но если использовать этот механизм с умом, то можно получить очень много хорошего. Например, прототипы активно используются в тестировании, они помогают подменить вызовы нежелательных методов, которые выполняют HTTP-запросы. Другой популярный вариант – полифилы. Это код, который добавляет в старые версии JavaScript возможности из новых версий, но не все, конечно — только те, что появляются в виде свойств и методов.

Теперь вы знаете, почему в документации все имена функций описаны так: Number.prototype.toLocaleString().

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff