JS6
Прототипы

Прототипы в 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 Для понимания низкоуровневой механики

Как мысленно представлять прототипы

Чтобы уверенно работать с этим механизмом, полезно представлять его не как абстрактную теорию, а как последовательный поиск по связанным объектам. Такой мысленный образ помогает быстро понимать поведение кода:

  1. сначала проверяется сам объект;
  2. если свойства нет, поиск идёт в его прототип;
  3. затем — выше по цепочке;
  4. если ничего не найдено, результатом становится undefined;
  5. для операций проверки свойств учитывается, ищется ли значение только в объекте или во всей цепочке.

Такой подход делает работу с объектами более предсказуемой. Чем лучше понятна цепочка прототипов, тем легче объяснить поведение методов, наследование, перекрытие свойств и обработку обращений к объектам.

Заключение

Прототипы — это фундаментальный механизм JavaScript, на котором строится объектная модель языка. Они позволяют объектам наследовать свойства и методы, сокращают дублирование, упрощают переиспользование логики и делают структуру кода более гибкой. Понимание собственных и унаследованных свойств, цепочки прототипов, функций-конструкторов и связи классов с прототипами помогает писать более надёжный и понятный код. Именно поэтому тема прототипов остаётся одной из самых важных для уверенной работы с JavaScript.

FAQ

Чем прототип отличается от обычного объекта, у которого просто есть общие методы?
Прототип — это не отдельная «магия», а обычный объект, который выступает источником свойств и методов для другого объекта. Разница в том, что доступ к этим свойствам происходит по цепочке поиска: если нужного значения нет в самом объекте, JavaScript обращается к его прототипу и далее выше по цепочке. Поэтому поведение может быть общим, но физически храниться в одном месте.

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

На практике это объясняет поведение вроде обращения к свойству через объект-наследник. Например, если у объекта нет своего свойства, но оно есть у базового объекта, доступ всё равно будет успешным. Это полезно, когда нужно разделять собственные данные и общее поведение: локальные значения можно хранить у конкретного объекта, а общие признаки и методы — в прототипе.
Можно ли отличить собственное свойство от унаследованного без перебора всей цепочки?
Да, для этого используют Object.hasOwn() или hasOwnProperty(). Эти способы проверяют только свойства, которые принадлежат самому объекту, не заглядывая в прототипы. Это важно, когда нужно понять, действительно ли значение хранится локально или просто доступно по наследованию.

Такое различие особенно полезно при переборе свойств, сериализации и сравнении объектов. Оператор in, наоборот, проверяет не только сам объект, но и всю цепочку прототипов. Поэтому он может вернуть true даже для унаследованного свойства, а это иногда создаёт ложное ощущение, что значение задано непосредственно в объекте.
Зачем использовать Object.create(), если объект можно просто записать литералом?
Object.create() нужен, когда важно сразу задать прототип нового объекта. Обычный литерал создаёт объект с типовым прототипом, а Object.create() позволяет связать новый объект с конкретным базовым объектом и тем самым сразу задать структуру наследования.

Это особенно удобно, когда нужно, чтобы новый объект пользовался общими свойствами и методами, не копируя их в себя. Такой способ делает поведение более предсказуемым: вы явно видите, от чего объект наследуется, и можете строить небольшие иерархии без лишнего кода. В тексте именно этот подход показан как ясный и практичный способ создать объект с нужным прототипом.
Что будет, если у объекта и прототипа есть свойство с одним и тем же именем?
В таком случае сработает переопределение: локальное свойство объекта скроет одноимённое значение из прототипа. JavaScript всегда начинает поиск с самого объекта, поэтому его собственное свойство имеет приоритет над унаследованным. Это касается не только обычных данных, но и методов.

Такой механизм удобен, когда базовая структура должна оставаться общей, а отдельные объекты — немного отличаться. Например, можно задать общий метод в прототипе, но изменить конкретный параметр у одного экземпляра. В результате вы сохраняете общую архитектуру и при этом гибко настраиваете поведение отдельных объектов.
Как работают функции-конструкторы и почему методы лучше хранить в prototype?
Когда функцию вызывают с new, JavaScript создаёт новый объект и связывает его прототип с объектом из свойства prototype конструктора. Поэтому методы, добавленные в User.prototype, становятся доступны всем экземплярам, созданным через new User(). Это и есть один из классических способов использовать прототипное наследование.

Плюс такого подхода в том, что метод не создаётся заново для каждого объекта. Если экземпляров много, это экономит память и делает код чище: общая логика хранится один раз, а данные — у каждого экземпляра отдельно. Поэтому prototype особенно полезен для методов, которые одинаковы для всех объектов одного типа.
Можно ли менять прототип уже созданного объекта, и стоит ли так делать?
Технически да: для этого существует Object.setPrototypeOf(). Но в тексте отдельно подчёркивается, что изменять прототип существующего объекта обычно не лучшая идея. Такие операции могут ухудшать читаемость кода и даже влиять на производительность.

Практичнее задавать прототип сразу при создании объекта, например через Object.create(). Тогда структура наследования понятна с самого начала, а поведение объекта не приходится перестраивать задним числом. Это особенно важно в коде, где ценятся предсказуемость и простота сопровождения.
Чем синтаксис class отличается от прототипов в JavaScript?
Синтаксис class выглядит более привычно для тех, кто знаком с классическим ООП, но он не заменяет прототипы. В основе class всё равно лежит тот же прототипный механизм: методы и наследование работают через прототипы, просто запись становится более компактной и удобной.

Это значит, что классы — скорее более высокий уровень представления, чем отдельная модель. Они помогают писать код понятнее, но не отменяют того, как JavaScript устроен внутри. Поэтому при разборе поведения объектов полезно помнить, что за внешним видом классов всё равно стоит цепочка прототипов и поиск свойств по ней.

Похожие страницы