В этом уроке дается небольшое введение в тему прототипов. Полностью раскрыть её одним уроком невозможно. Не хватит даже целого курса. Прототипы познаются только через практику, перемешанную с теоретической подготовкой. Поэтому не переживайте, если не всё поймёте с первого раза, это нормально.
Прототипы — это механизм, который оказывает основное влияние на то, как работают объекты в 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()
.
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.