Прототипы в JavaScript
Прототипы лежат в основе объектной модели JavaScript и определяют, как объекты получают доступ к свойствам и методам. Понимание этого механизма помогает лучше разбираться в наследовании, переиспользовании кода и работе конструкторов и классов.
Прототипы: основа объектов и наследования в JavaScript
Понятие прототипа занимает центральное место в JavaScript и во многом определяет, как устроены объекты, как они получают доступ к свойствам и методам, а также как строится наследование. Для тех, кто привык к классическим объектно-ориентированным языкам, механизм прототипов поначалу может казаться непривычным. Однако именно он делает JavaScript гибким, лаконичным и выразительным языком для работы с объектами.
Прототипы важны не только для понимания внутреннего устройства языка, но и для практики: они помогают правильно создавать объекты, эффективно переиспользовать поведение, избегать лишнего дублирования кода и лучше разбираться в том, как работают функции-конструкторы, классы, методы и цепочка поиска свойств.
Что такое прототип простыми словами
Прототип — это объект, который используется как источник свойств и методов для другого объекта. Если у объекта нет нужного свойства, JavaScript ищет его в прототипе. Если не находит там, поднимается выше по цепочке прототипов. Такой механизм называется прототипным наследованием.
Иначе говоря, объект может «пользоваться» возможностями другого объекта, не копируя их буквально. Это похоже на заимствование поведения: свойство можно не хранить у каждого экземпляра отдельно, а держать в общем месте.
Почему это удобно
- уменьшается дублирование одинаковых методов;
- проще поддерживать код, когда общее поведение описано один раз;
- можно создавать связанные объекты с общей логикой;
- упрощается расширение объектов без переписывания всей структуры.
Как устроена цепочка прототипов
Каждый объект в JavaScript связан с другим объектом, который выступает его прототипом. Эта связь образует цепочку. При обращении к свойству движок сначала проверяет сам объект, затем его прототип, потом прототип прототипа и так далее, пока не найдёт нужное значение или не дойдёт до конца цепочки.

В конце цепочки находится null. Это означает, что дальше искать уже негде.
Пример логики поиска
const animal = {
eats: true
};
const rabbit = Object.create(animal);
rabbit.jumps = true;
console.log(rabbit.jumps); // true — найдено в самом объекте
console.log(rabbit.eats); // true — найдено в прототипе
console.log(rabbit.sleeps); // undefined — нигде не найдено
В этом примере объект rabbit не содержит свойства eats напрямую, но всё равно может к нему обратиться через прототип animal.
Прототип объекта и внутренние связи
У каждого объекта есть скрытая внутренняя ссылка на прототип. Обычно она не используется напрямую в повседневном коде, но именно благодаря ей работает наследование. Исторически в JavaScript эта связь часто обозначалась как __proto__, однако такой способ не считается лучшей практикой для прикладного кода, несмотря на его доступность в некоторых средах.
Для создания объекта с нужным прототипом обычно используют Object.create(). Это ясный и предсказуемый способ задать начальную структуру объекта.
Создание объекта с прототипом
const user = {
greet() {
return 'Привет';
}
};
const admin = Object.create(user);
admin.name = 'Ирина';
console.log(admin.greet()); // 'Привет'
Здесь admin наследует метод greet() от user, хотя сам метод в объекте admin не определён.
Собственные и унаследованные свойства
Важно различать свойства, принадлежащие самому объекту, и свойства, полученные по цепочке прототипов. Это различие помогает понимать поведение кода и избегать неожиданных результатов.
| Тип свойства | Где хранится | Как определяется |
|---|---|---|
| Собственное | Непосредственно в объекте | Проверяется в самом объекте |
| Унаследованное | В прототипе или выше по цепочке | Находится после поиска в объекте |
Для проверки собственного свойства часто используют Object.hasOwn() или hasOwnProperty(). Это особенно полезно, когда нужно отличить локальное значение от унаследованного.
Практическое значение различия
Если свойство унаследовано, оно может существовать для объекта «как будто бы», но при этом не храниться в нём напрямую. Это важно при переборе свойств, сериализации, сравнении объектов и построении структур данных.
const base = { color: 'blue' };
const item = Object.create(base);
console.log('color' in item); // true
console.log(Object.hasOwn(item, 'color')); // false
Оператор in проверяет всю цепочку прототипов, а Object.hasOwn() — только собственные свойства.
Функции-конструкторы и прототипы
До появления синтаксиса классов прототипное наследование часто строилось через функции-конструкторы. Такой подход по-прежнему встречается и хорошо иллюстрирует механику прототипов.
У каждой функции в JavaScript есть свойство prototype. Когда функцию вызывают с оператором new, создаётся новый объект, и его прототипом становится объект из свойства prototype конструктора.
Как это работает
function User(name) {
this.name = name;
}
User.prototype.sayHi = function() {
return `Привет, ${this.name}`;
};
const user1 = new User('Алексей');
console.log(user1.sayHi()); // 'Привет, Алексей'
Метод sayHi() не создаётся заново для каждого объекта. Он лежит в прототипе и доступен всем экземплярам, созданным через User.
Преимущество такого подхода
Если экземпляров много, хранение общих методов в прототипе экономит память и делает архитектуру чище. Вместо десятков одинаковых функций в каждом объекте создаётся одна общая реализация.
Переопределение свойств и методов
Если объекту задать свойство с тем же именем, что и в прототипе, локальное значение скроет унаследованное. Это называется переопределением.
const vehicle = {
wheels: 4,
move() {
return 'Движение';
}
};
const bike = Object.create(vehicle);
bike.wheels = 2;
console.log(bike.wheels); // 2
console.log(bike.move()); // 'Движение'
Свойство wheels теперь берётся из самого объекта bike, а метод move() по-прежнему доступен из прототипа.
Переопределение особенно полезно, когда нужно изменить отдельные параметры поведения, сохранив общую структуру. Это позволяет строить иерархии объектов, где базовая логика остаётся общей, а частные особенности задаются точечно.
Методы работы с прототипами
JavaScript предоставляет несколько удобных инструментов для работы с прототипами. Каждый из них решает свою задачу и помогает выстраивать объектные модели аккуратно и предсказуемо.
| Метод или средство | Назначение |
|---|---|
Object.create() |
Создаёт объект с заданным прототипом |
Object.getPrototypeOf() |
Возвращает прототип объекта |
Object.setPrototypeOf() |
Изменяет прототип существующего объекта |
Object.hasOwn() |
Проверяет наличие собственного свойства |
Что важно помнить
Изменение прототипа существующего объекта возможно, но обычно не считается оптимальным приёмом. Такие операции могут ухудшать читаемость кода и влиять на производительность. В большинстве случаев лучше задать прототип при создании объекта, а не перестраивать его постфактум.
Прототипы и классы: связь между двумя моделями
Синтаксис class в JavaScript часто воспринимается как отдельная, «классическая» модель объектно-ориентированного программирования. На практике он построен на том же прототипном механизме. Классы не отменяют прототипы, а лишь дают более удобную и привычную форму записи.
class Person {
constructor(name) {
this.name = name;
}
hello() {
return `Меня зовут ${this.name}`;
}
}
const person = new Person('Мария');
console.log(person.hello());
Метод hello() не копируется в каждый экземпляр. Он находится в прототипе, связанном с классом. Поэтому понимание прототипов остаётся необходимым даже при использовании классового синтаксиса.
Зачем знать прототипы, если есть классы
- чтобы понимать реальное устройство объектов в JavaScript;
- чтобы читать чужой код, где используются и классы, и функции-конструкторы;
- чтобы разбираться в наследовании, доступе к методам и цепочке поиска свойств;
- чтобы не путать синтаксический сахар с фундаментальным механизмом языка.
Практические сценарии использования прототипов
Прототипы особенно полезны там, где нужно объединить общие возможности и различия между объектами. Ниже приведены типичные ситуации, в которых этот механизм проявляет себя наиболее естественно.
1. Общие методы для множества объектов
Если множество объектов имеет одинаковое поведение, методы лучше хранить в прототипе. Это снижает количество повторов и упрощает поддержку.
2. Построение иерархий
Базовый объект может содержать общие свойства, а дочерние объекты — частные особенности. Такой подход удобен для моделирования сущностей, которые имеют общую основу, но различаются деталями.
3. Расширение существующих объектов
Иногда требуется создать объект, который использует уже готовую логику и лишь дополняет её. В таких случаях прототип помогает избежать переписывания базового поведения.
4. Работа с «поведенческими шаблонами»
Прототипы позволяют создавать объекты, у которых одни и те же методы работают в контексте разных данных. Это удобно для абстракций, где важна не столько структура хранения, сколько общий набор возможностей.
Типичные ошибки при работе с прототипами
Даже при хорошем понимании основ можно столкнуться с ошибками, связанными с особенностями наследования и поиском свойств. Некоторые из них особенно распространены.
Путаница между собственным и унаследованным свойством
Иногда свойство кажется «потерянным», хотя на самом деле оно находится в прототипе. Для проверки стоит использовать инструменты, которые различают эти случаи.
Неосторожное изменение прототипов
Если менять прототипы уже созданных объектов без необходимости, код становится сложнее для сопровождения. Логика объекта должна быть понятна без скрытых перестроек цепочки наследования.
Избыточное использование общих прототипов
Когда слишком много объектов наследуются от одного базового источника, структура может стать хрупкой. Лучше сохранять разумный баланс между переиспользованием и простотой модели.
Ожидание копирования вместо наследования
Прототип не копирует свойства автоматически. Он лишь предоставляет к ним доступ при поиске. Если нужно именно независимое значение, его следует записать в сам объект.
Наглядное сравнение подходов
| Подход | Описание | Когда полезен |
|---|---|---|
| Собственные свойства | Хранятся в конкретном объекте | Для индивидуальных данных |
| Прототип | Содержит общее поведение и свойства | Для переиспользования логики |
| Классы | Удобный синтаксис поверх прототипов | Для структурированного ООП-кода |
| Функции-конструкторы | Старший способ создавать экземпляры через new |
Для понимания низкоуровневой механики |
Как мысленно представлять прототипы
Чтобы уверенно работать с этим механизмом, полезно представлять его не как абстрактную теорию, а как последовательный поиск по связанным объектам. Такой мысленный образ помогает быстро понимать поведение кода:
- сначала проверяется сам объект;
- если свойства нет, поиск идёт в его прототип;
- затем — выше по цепочке;
- если ничего не найдено, результатом становится
undefined; - для операций проверки свойств учитывается, ищется ли значение только в объекте или во всей цепочке.
Такой подход делает работу с объектами более предсказуемой. Чем лучше понятна цепочка прототипов, тем легче объяснить поведение методов, наследование, перекрытие свойств и обработку обращений к объектам.
Заключение
Прототипы — это фундаментальный механизм JavaScript, на котором строится объектная модель языка. Они позволяют объектам наследовать свойства и методы, сокращают дублирование, упрощают переиспользование логики и делают структуру кода более гибкой. Понимание собственных и унаследованных свойств, цепочки прототипов, функций-конструкторов и связи классов с прототипами помогает писать более надёжный и понятный код. Именно поэтому тема прототипов остаётся одной из самых важных для уверенной работы с JavaScript.