Конструкторы, оператор "new"


Введение

Обычный синтаксис {...} позволяет создать только один объект. Но зачастую нам нужно создать множество похожих, однотипных объектов, таких как пользователи, элементы меню и так далее.

Это можно сделать при помощи функции-конструктора и оператора "new".

Функция-конструктор

Функции-конструкторы технически являются обычными функциями. Но есть два соглашения:

  1. Имя функции-конструктора должно начинаться с большой буквы.
  2. Функция-конструктор должна выполняться только с помощью оператора "new".

Например:

function User(name) {   
	this.name = name;   
	this.isAdmin = false; }  
	
_let user = new User("Jack");_  
alert(user.name); // Jack 
alert(user.isAdmin); // false`

Когда функция вызывается как new User(...), происходит следующее:

  1. Создаётся новый пустой объект, и он присваивается this.
  2. Выполняется тело функции. Обычно оно модифицирует this, добавляя туда новые свойства.
  3. Возвращается значение this.

Другими словами, new User(...) делает что-то вроде:

function User(name) {   _// 
	this = {};  (неявно)_    // добавляет свойства к this   
	this.name = name;   
	this.isAdmin = false;    _// 
	
	return this;  (неявно)_ 
}`

Таким образом, let user = new User("Jack") возвращает тот же результат, что и:

let user = { name: "Jack", isAdmin: false };

Теперь, если нам будет необходимо создать других пользователей, мы можем просто вызвать new User("Ann")new User("Alice") и так далее. Данная конструкция гораздо удобнее и читабельнее, чем многократное создание литерала объекта.

Это и является основной целью конструкторов – реализовать код для многократного создания однотипных объектов.

Давайте ещё раз отметим – технически любая функция (кроме стрелочных функций, поскольку у них нет this) может использоваться в качестве конструктора. Его можно запустить с помощью new, и он выполнит выше указанный алгоритм. Подобные функции должны начинаться с заглавной буквы – это общепринятое соглашение, чтобы было ясно, что функция должна вызываться с помощью «new».

new function() { … }

Если в нашем коде присутствует большое количество строк, создающих один сложный объект, то мы можем обернуть их в функцию-конструктор, которая будет немедленно вызвана, вот так:

// создаём функцию и сразу же вызываем её с помощью new 
 
let user = new function() {   
	this.name = "John";   
	this.isAdmin = false;    // ...другой код для создания пользователя   
	// возможна любая сложная логика и инструкции   
	// локальные переменные и так далее };`

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

Проверка на вызов в режиме конструктора: new.target

Продвинутая возможность

Синтаксис из этого раздела используется крайне редко. Вы можете пропустите его, если не хотите углубляться в детали языка.

Используя специальное свойство new.target внутри функции, мы можем проверить, вызвана ли функция при помощи оператора new или без него.

В случае обычного вызова функции new.target будет undefined. Если же она была вызвана при помощи newnew.target будет равен самой функции.

function User() {   
	alert(new.target); 
}  
	// без "new": _User(); // undefined_  // с "new": _new User(); // 
 
function User { ... }_`

Это можно использовать внутри функции, чтобы узнать, была ли она вызвана при помощи new, «в режиме конструктора», или без него, «в обычном режиме».

Также мы можем сделать, чтобы вызовы с new и без него делали одно и то же:

function User(name) {   
	if (!new.target) { // в случае, если вы вызвали меня без оператора new     
	return new User(name); // ...я добавлю new за вас   }    
	
	this.name = name; }  
	
	let john = User("John"); // переадресовывает вызов на new User 
	alert(john.name); // John`

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

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

Возврат значения из конструктора, return

Обычно конструкторы не имеют оператора return. Их задача – записать все необходимое в this, и это автоматически становится результатом.

Но если return всё же есть, то применяется простое правило:

  • При вызове return с объектом, вместо this вернётся объект.
  • При вызове return с примитивным значением, оно проигнорируется.

Другими словами, return с объектом возвращает этот объект, во всех остальных случаях возвращается this.

К примеру, здесь return замещает this, возвращая объект:

function BigUser() {    
	this.name = "John";    
	return { name: "Godzilla" };  // <-- возвращает этот объект }  
	
	alert( new BigUser().name );  // Godzilla, получили этот объект`

А вот пример с пустым return (или мы могли бы поставить примитив после return, неважно):

function SmallUser() {    
	this.name = "John";    
	return; // <-- возвращает this }  
	alert( new SmallUser().name );  // John`

Обычно у конструкторов отсутствует return. Здесь мы упомянули особое поведение с возвращаемыми объектами в основном для полноты картины.

Пропуск скобок

Кстати, мы можем не ставить круглые скобки после new:

let user = new User; // <-- без скобок // то же, что и let user = new User();

Пропуск скобок считается плохой практикой, но просто чтобы вы знали, такой синтаксис разрешён спецификацией.

Создание методов в конструкторе

Использование конструкторов для создания объектов даёт большую гибкость. Функции-конструкторы могут иметь параметры, определяющие, как создавать объект и что в него записывать.

Конечно, мы можем добавить к this не только свойства, но и методы.

Например, new User(name) ниже создаёт объект с заданным name и методом sayHi:

function User(name) {   
	this.name = name;    
	this.sayHi = function() {     
		alert( "Меня зовут: " + this.name );   }; }  _
		
let john = new User("John");  
john.sayHi(); // Меня зовут: John_  /* 
john = {    
	name: "John",    
	sayHi: function() { ... } } */`

Для создания сложных объектов есть и более продвинутый синтаксис – 0059 Класс. базовый синтаксис, который мы рассмотрим позже.

Итого

  • Функции-конструкторы или просто конструкторы, являются обычными функциями, но существует общепринятое соглашение именовать их с заглавной буквы.
  • Функции-конструкторы следует вызывать только с помощью new. Такой вызов подразумевает создание пустого this в начале и возврат заполненного в конце.

Мы можем использовать конструкторы для создания множества похожих объектов.

JavaScript предоставляет функции-конструкторы для множества встроенных объектов языка: таких как DateSet, и других, которые нам ещё предстоит изучить.