Оператор new в JavaScript
Узнайте, как конструкторы и оператор new создают объекты в JavaScript: пошаговая работа new, правило возвращаемого значения, new.target и связь с классами.
Введение в конструкторы и оператор new
В JavaScript конструкторы — это функции, предназначенные для инициализации вновь созданных объектов. Они играют ключевую роль в объектно-ориентированном программировании, позволяя разработчикам определять свойства и поведение, которые должны иметь объекты определённого класса. Оператор new используется для создания экземпляра объекта на основе функции-конструктора: он создаёт новое окружение объекта на основе указанного прототипа и запускает конструктор для инициализации нового объекта.
Как работают конструкторы
Функция-конструктор в JavaScript выглядит как обычная функция, однако по соглашению её имя начинается с заглавной буквы, чтобы отличить её от обычных функций. Когда оператор new вызывает функцию-конструктор, за кулисами происходят четыре вещи, примерно эквивалентные следующему псевдокоду:
function User(name) {
// this = {}; (1) an empty object is created and assigned to this
// this.__proto__ = User.prototype; (2) prototype is linked
this.name = name; // (3) the constructor body runs, adding properties to this
// return this; (4) this is returned automatically
}- Создаётся новый пустой object и присваивается
this. - Устанавливается связь с прототипом: внутреннее свойство
[[Prototype]]нового объекта указывает на свойствоprototypeконструктора, поэтому объект наследует свойства и методы, определённые там. - Выполняется тело конструктора: функция выполняется с переданными аргументами, а
thisссылается на только что созданный объект, поэтому присваивания видаthis.name = nameдобавляют к нему свойства. - Объект возвращается:
thisвозвращается автоматически, если только конструктор явно не возвращает другой object (см. Правило возвращаемого значения ниже).
В современном JavaScript для определения конструкторов и методов более интуитивно используется синтаксис класса. Он обеспечивает более понятный, классовый подход, аналогичный другим языкам программирования.
Пример: базовая функция-конструктор
Пояснение: В этом примере User — это функция-конструктор, которая инициализирует name, age и метод greet на вновь созданных объектах. Выражение new User('John', 30) создаёт новый экземпляр User с именем «John» и возрастом 30. Внутри greet this ссылается на объект, для которого был вызван метод.
Правило возвращаемого значения
Конструкторы обычно не используют return — новый объект (this) возвращается автоматически. Однако return внутри конструктора имеет особое, легко упускаемое из виду поведение:
- Если после
returnуказан object, этот object возвращается вместоthis. - Если после
returnуказан примитив (string, число, boolean,undefinedи т. д.), он игнорируется, иthisвозвращается как обычно.
Пояснение: WithObject возвращает обычный object, поэтому он полностью заменяет экземпляр. WithPrimitive возвращает string, которая игнорируется, поэтому возвращается исходный this (с name: 'Alice'). На практике это редко используется намеренно, но объясняет неожиданные результаты, когда конструктор случайно возвращает значение.
Определение вызова через new с помощью new.target
Внутри любой функции new.target равно undefined, когда функция вызвана обычным образом, и равно самой функции, когда она вызвана с new. Это позволяет конструктору определить, как именно он был вызван, — что полезно для принудительного использования (или молчаливого допущения) ключевого слова new.
Пояснение: Поскольку Modal проверяет new.target, вызов Modal('Without new') без new прозрачно перенаправляется к new Modal(...), так что b по-прежнему является настоящим экземпляром Modal. (Заметьте: многие команды предпочитают не делать этого и вместо этого позволяют ошибочному отсутствию new явно завершаться ошибкой.)
Когда стоит использовать конструктор?
Используйте функцию-конструктор (или класс), когда нужно создать множество объектов одной формы — несколько пользователей, автомобилей, виджетов DOM и т. д. Конструктор централизует логику настройки, чтобы каждый экземпляр создавался одинаково и разделял методы через прототип.
Если нужен только один объект, проще воспользоваться литералом объекта { ... }. Для одноразового объекта, которому всё же полезно тело конструктора, можно использовать анонимный конструктор:
Пояснение: Анонимная function запускается один раз как конструктор и нигде не сохраняется, поэтому не может быть использована повторно — это просто способ инкапсулировать сложную одноразовую настройку.
Функции-конструкторы и классы
В современном коде обычно предпочитают синтаксис class, который по сути является синтаксическим сахаром над функциями-конструкторами и прототипами. Два приведённых ниже фрагмента эквивалентны:
Классы имеют реальные преимущества перед функциями-конструкторами: методы по умолчанию не являются перечисляемыми, тело выполняется в строгом режиме, вызов класса без new вызывает ошибку, а extends/super делают наследование значительно чище. Понимание функций-конструкторов по-прежнему важно, поскольку классы построены на том же механизме прототипов, и вы всё равно будете встречать их в старом коде.
Использование конструкторов для сложных объектов
Конструкторы можно использовать для настройки более сложных взаимодействий между объектами, включая методы, работающие с другими свойствами этих объектов.
Пример: конструктор с методами
Пояснение: Конструктор Car настраивает каждый объект-автомобиль с конкретными свойствами и методом, отображающим информацию о нём.
Пример: методы прототипа
Пояснение: Добавляя introduce в прототип Employee, все экземпляры разделяют один и тот же метод, что эффективнее по памяти, чем определение метода непосредственно в конструкторе.
Рекомендуется использовать классы ES6 для определения объектов и конструкторов ради более чистого и читаемого кода.
Лучшие практики работы с конструкторами
При работе с конструкторами в JavaScript соблюдение определённых лучших практик может значительно улучшить читаемость, эффективность и масштабируемость кода. Ниже приведены подробные примеры и пояснения к каждой из них.
1. Соглашение об именовании
Лучшая практика: Всегда начинайте имена конструкторов с заглавной буквы, чтобы отличить их от обычных функций. Это общепринятое соглашение в JavaScript и многих других языках программирования, которое помогает разработчикам быстро идентифицировать функции-конструкторы.
Пример:
Пояснение: Функция-конструктор Laptop начинается с заглавной буквы, что указывает на её предназначение для использования с оператором new при создании новых объектов.
2. Разделение логики
Лучшая практика: Для методов, которым не нужен доступ к данным конкретного экземпляра, определяйте их в прототипе конструктора, а не внутри него самого. Такой подход экономит память, поскольку все экземпляры используют один и тот же метод, вместо того чтобы каждый экземпляр создавал новую функцию в памяти.
Пример:
Пояснение: Метод describe добавлен в прототип Book, то есть все экземпляры Book разделяют один и тот же метод describe. Это эффективнее, чем если бы describe был определён внутри конструктора — в этом случае для каждого экземпляра книги создавалась бы новая функция.
3. Возвращаемые значения
Лучшая практика: Избегайте возврата значений из конструкторов. JavaScript-конструкторы автоматически возвращают новый экземпляр объекта, если только явно не возвращается другой object. Возврат значений, не являющихся object (например, string или числа), не окажет никакого эффекта, и новый экземпляр всё равно будет возвращён.
Пример:
Пояснение: Несмотря на попытку вернуть string из конструктора Player, JavaScript игнорирует это возвращаемое значение, поскольку оно не является object. Новый экземпляр Player возвращается, как и ожидается.
Заключение
Понимание конструкторов и оператора new в JavaScript необходимо для эффективного объектно-ориентированного программирования. Следуя изложенным здесь соглашениям и лучшим практикам, разработчики могут создавать организованный, эффективный и масштабируемый код. Конструкторы предоставляют мощный механизм для инициализации новых объектов и определения их поведения в структурированном и понятном виде.
Чтобы углубиться в тему, изучите, как конструкторы делятся поведением через прототипное наследование, как синтаксис class строится на этих идеях, и как функция сама является объектом со своими свойствами.