HTML / CSSJavaScriptNode jsПаттерны проектированияПрактические

Функции конструкторы

Представьте, у вас есть небольшой магазин. В магазине есть два менеджера, занимающихся продажей ваших товаров. И вы решаете организовать их работу. Для каждого менеджера создаётся отдельный объект:

1const john = {
2 name: 'John',
3 sales: 10,
4 sell: function(thing) {
5 this.sales += 1;
6 return 'Manager ' + this.name + ' sold ' + thing;
7 }
8};
9
10// Итак, один менеджер готов. Джон умеет продавать вещи, знает своё имя и количество продаж.
11console.log(john.sales); // 10
12john.sell('Apple'); // Manager John sold Apple
13john.sell('Pomegrade'); // Manager John sold Pomegrade
14console.log(john.sales); // 12

Создадим еще одного менеджера

1const mary = {
2 name: 'Mary',
3 sales: 120,
4 sell: function(thing) {
5 this.sales += 1;
6 return 'Manager ' + this.name + ' sold ' + thing;
7 }
8};

Это было просто и всё работает хорошо. Или нет? У каждого менеджера есть свой метод sell. В том, что он у всех будет разным можно убедиться с помощью сравнения:

1john.sell === mary.sell; // false

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

А если и методов будет много? Очевидно, что создавать объекты подобным способом — не самая лучшая затея. Чтобы решить подобную проблему существуют функции конструкторы.

Как и следует из названия функции конструкторы являются не более чем обычными функциями.

❗️Важно

Конструкторы, вызванные с помощью оператора new всегда возвращают объект.

Для начала разберёмся, что мы хотим сделать: написать функцию, которая будет возвращать объект со всеми указанными нами свойствами и методами. Написать подобную функцию очень просто:

1const manager = function(name, sales) {
2 return {
3 name: name,
4 sales: sales,
5 sell: function(thing) {
6 this.sales += 1;
7 return 'Manager ' + this.name + ' sold ' + thing;
8 }
9 };
10};

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

1const john = manager('John', 10);
2const mary = manager('Mary', 120);
3
4console.log(john.sales, mary.sales); // 10 120
5john.sell('Apple'); // Manager John sold Apple
6mary.sell('Pomegrade'); // Manager Mary sold Pomegrade
7console.log(john.sales, mary.sales); // 11 121

Таким же образом будет работать и функция конструктор:

1const Manager = function(name, sales) {
2 this.name = name;
3 this.sales = sales;
4 this.sell = function(thing) {
5 this.sales += 1;
6 return 'Manager ' + this.name + ' sold ' + thing;
7 };
8};
9
10const Manager = new Manager('John', 10);
11const mary = new Manager('Mary', 120);

Приведённые выше функции manager и Manager делают одно и то же, но разными способами. Но функция Manager является конструктором, а manager — нет. Что это значит? То, что внутри функции Manager мы можем пользоваться ключевым словом this, которое содержит ссылку на новый объект.

Другими словами, каждый раз, когда вы вызываете любую функцию с оператором new, вы подразумеваете, что будет создан новый объект, к которому можно обратиться с помощью ключевого слова this внутри функции.

Что же происходит, когда мы вызывали функцию с new?

Когда функция вызываетьсся с ключевым словом new движок выполняет несколько шагов.

  1. Создает пустой объект, {}, и устанавливает его в this.
  2. Присваивает ему все значения, которые обьявлены таким образом внутри функции. this.name = 'John'
  3. Возвращает this и сохраняет в переменной, которая принимает значение, возвращаемое функцией new Manager().

Выглядит это следующим образом:

1function Manager() {
2 this = {};
3 this.name = 'Tree';
4 return this;
5}

Как вы могли заметить, при создании функции-конструктора Manager мы не использовали return, для того, чтобы вернуть созданный функцией объект. Но в переменных john и mary всё равно есть объекты. Это значит, что при использовании оператора new возвращать что-либо из функции необязательно. Хотя вы можете вернуть любой объект, если в этом есть смысл:

1const Manager = function(name, sales) {
2 this.name = name;
3 this.sales = sales;
4 this.sell = function(thing) {
5 this.sales += 1;
6 return 'Manager ' + this.name + ' sold ' + thing;
7 };
8 return {prop: 'Prop of new object'};
9};
10
11const john = new Manager('John', 10);
12console.log(john); // {"prop":"Prop of new object"}

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

1const john = new Manager('John', 10);
2
3console.log(john.constructor); // function Manager(name, sales) { ... };
4console.log(john.constructor.name); // Manager
5console.log(john instanceof Manager); // true

Таким образом, с помощью свойства constructor можно получить, как саму функцию-конструктор, так и её имя.

Итак, мы выяснили, что с помощью функций-конструкторов и оператора new можно создать объект. Но нашу проблему всё равно не решили. Для каждого нового объекта будет создаваться новая функция, которая будет записываться в метод sell. И здесь нам поможет прототип функции Manager. Перейдём сразу к делу:

1const Manager = function(name, sales) {
2 this.name = name;
3 this.sales = sales;
4};
5
6Manager.prototype.sell = function(thing) {
7 this.sales += 1;
8 return 'Manager ' + this.name + ' sold ' + thing;
9};
10
11const john = new Manager('John', 10);
12const mary = new Manager('Mary', 120);
13
14console.log(john.sales, mary.sales); // 10 120
15john.sell('Apple'); // Manager John sold Apple
16mary.sell('Pomegrade'); // Manager Mary sold Pomegrade
17console.log(john.sales, mary.sales); // 11 121

Что вообще происходит? Каждый объект в JavaScript обладает прототипом. Чтобы в этом убедиться, откройте консоль и введите console.dir([]);. Открыв свойство __proto__ вы сможете увидеть все методы для работы с массивами, которые предусмотрены в вашем браузере. Очевидно, что каждый массив по-отдельности не снабжается всеми данными методами. Но, тем не менее, мы без проблем можем их использовать, например, [1, 2, 3].map((n) => n * 2). Когда вы используете какой-либо метод массивов, например, map или forEach, то вы подразумеваете, что этот метод будет взят из прототипа функции-конструктора Array. Любой массив может использовать все методы, записанные в прототип конструктора Array, хотя у самого массива нет ни одного метода. Таким образом, любой объект получает возможность использовать все методы, записанные в прототипе его функции-конструктора.

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

1const Manager = function(name, sales) {
2 this.name = name;
3 this.sales = sales;
4};
5
6Manager.prototype = {
7 sell: function(thing) {
8 this.sales += 1;
9 return 'Manager ' + this.name + ' sold ' + thing;
10 },
11 speak: function(word) {
12 return this.name + ' says ' + word;
13 }
14};
15
16const john = new Manager('John', 10);
17const mary = new Manager('Mary', 120);
18
19john.sell('Apple'); // Manager John sold Apple
20mary.speak('Hello!'); // Mary says Hello!

Как и при работе внутри функции-конструктора this содержит ссылку на текущий экземпляр объекта, поэтому им можно пользоваться во всех объявляемых в прототипе методах.

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

1console.log(john.sell === mary.sell); // true
2console.log(john.speak === mary.speak); // true

Подобным образом мы можем создать любое количество объектов с помощью функции конструктора Manager и оператора new и всем им будут доступны методы из прототипа Manager.

Hello