22 февраля 2023 г.

Конструктор, оператор "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. Если же она была вызвана при помощи new, new.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() { ... }
}
*/

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

Итого

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

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

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

Мы ещё вернёмся к объектам!

В этой главе мы рассмотрели только основы объектов и конструкторов. Данная информация необходима нам для дальнейшего изучения типов данных и функций в последующих главах.

Как только мы с ними разберёмся, мы вернёмся к объектам для более детального изучения в главах Прототипы, наследование и Классы.

Задачи

важность: 2

Возможно ли создать функции A и B, чтобы new A() == new B()?

function A() { ... }
function B() { ... }

let a = new A();
let b = new B();

alert( a == b ); // true

Если да – приведите пример вашего кода.

Да, возможно.

Если функция возвращает объект, то new вернёт его вместо this.

Таким образом, они могут, к примеру, возвращать один и тот же внешне определённый объект obj:

let obj = {};

function A() { return obj; }
function B() { return obj; }

alert( new A() == new B() ); // true
важность: 5

Создайте функцию-конструктор Calculator, которая создаёт объекты с тремя методами:

  • read() запрашивает два значения при помощи prompt и сохраняет их значение в свойствах объекта.
  • sum() возвращает сумму этих свойств.
  • mul() возвращает произведение этих свойств.

Например:

let calculator = new Calculator();
calculator.read();

alert( "Sum=" + calculator.sum() );
alert( "Mul=" + calculator.mul() );

Запустить демо

Открыть песочницу с тестами для задачи.

function Calculator() {

  this.read = function() {
    this.a = +prompt('a?', 0);
    this.b = +prompt('b?', 0);
  };

  this.sum = function() {
    return this.a + this.b;
  };

  this.mul = function() {
    return this.a * this.b;
  };
}

let calculator = new Calculator();
calculator.read();

alert( "Sum=" + calculator.sum() );
alert( "Mul=" + calculator.mul() );

Открыть решение с тестами в песочнице.

важность: 5

Создайте функцию-конструктор Accumulator(startingValue).

Объект, который она создаёт, должен уметь следующее:

  • Хранить «текущее значение» в свойстве value. Начальное значение устанавливается в аргументе конструктора startingValue.
  • Метод read() должен использовать prompt для считывания нового числа и прибавления его к value.

Другими словами, свойство value представляет собой сумму всех введённых пользователем значений, с учётом начального значения startingValue.

Ниже вы можете посмотреть работу кода:

let accumulator = new Accumulator(1); // начальное значение 1

accumulator.read(); // прибавляет введённое пользователем значение к текущему значению
accumulator.read(); // прибавляет введённое пользователем значение к текущему значению

alert(accumulator.value); // выведет сумму этих значений

Запустить демо

Открыть песочницу с тестами для задачи.

function Accumulator(startingValue) {
  this.value = startingValue;

  this.read = function() {
    this.value += +prompt('Сколько нужно добавить?', 0);
  };

}

let accumulator = new Accumulator(1);
accumulator.read();
accumulator.read();
alert(accumulator.value);

Открыть решение с тестами в песочнице.

Карта учебника