Данная статья рассматривает различные шаблоны для создания объектов в JavaScript и плюсы и минусы каждого из них.

Самым популярным способом создания объекта является использование синтаксиса литерала объекта:

let person = {
  firstName: 'John',
  lastName: 'Doe'
};

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

Фабрика (Factory pattern)

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

Например, следующая функция createAnimal() инкапсулирует логику создания объекта животного.

function createAnimal(name) {
  let o = new Object();
  o.name = name;
  o.identify = function() {
    console.log('Привет! Я - ' + o.name);
  }
  return o;
}

Функция createAnimal() принимает аргумент, который будет использоваться для инициализации свойства name объекта животного.

Чтобы создать новый объект, вам просто нужно вызвать эту функцию и передать аргумент name следующим образом:

let cat = createAnimal('Кот');
let dog = createAnimal('Собака');

cat.identify();  // => Привет, Я - Кот
dog.identify();  // => Привет, Я - Собака

Хотя шаблон фабрики может создавать несколько похожих объектов, он не позволяет вам определить тип создаваемого объекта.

Конструктор (Constructor pattern)

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

По соглашению, имя функции-конструктора в JavaScript начинается с заглавной буквы.

Следующий пример - как создать наш объект животного с помощью конструктора:

function Animal(name) {
  this.name = name;
  this.identify = function() {
    console.log('Привет! Я - ' + this.name);
  };
}

В отличие от фабрики, свойства и методы объекта animal назначаются непосредственно элементу this внутри функции-конструктора.

На этом этапе движок JavaScript создает функцию Animal() и анонимный объект.

Функция Animal() по-умолчанию содержит свойство prototype, ссылающееся на анонимный объект, а анонимный объект содержит свойство constructor, ссылающееся на функцию Animal(). И движок JavaScript связывает анонимный объект и наш Object.

console.log(Animal.prototype.constructor == Animal)  // => true

Схематически это выглядит так:

Чтобы создать новый экземпляр Animal, нужно использовать оператор new. Например:

let dog = new Animal('Собака');

Вот что происходит под капотом JavaScript:

  • Создаётся новый объект;
  • Устанавливается значение this конструктора новому объекту;
  • Выполняется код внутри конструктора, т.е. добавляются свойства к новому объекту;
  • Возвращается новый объект.

Для нашего созданноо объекта dog:

console.log(dog.constructor === Animal);  // => true

Поскольку объект dog не имеет свойства constructor, движок JavaScript следует по цепочке прототипов, чтобы найти его в объекте Animal.prototype.

Он нашел свойство constructor в объекте Animal.prototype, и в этом случае constructor указывает на функцию Animal(), поэтому приведенное выше утверждение возвращает true.

Объект dog является экземпляром как для Animal, так и для Object:

console.log(dog instanceof Animal);  // => true
console.log(dog instanceof Object);  // => true

Недостатком шаблона конструктора является то, что один и тот же метод identify() дублируется в каждом экземпляре.

Создадим ещё один объект Animal с именем cat:

let cat = new Animal('Кот');

Как видите, метод identify() дублируется как в объектах dog, так и в cat. Чтобы решить эту проблему, используйте шаблон прототипа.

Прототип (Prototype pattern)

Шаблон прототипа добавляет свойства объекта к прототипу объекта. Затем эти свойства доступны и используются всеми экземплярами.

В следующем примере шаблон прототипа используется, чтоб перезаписать объект выше:

function Animal() {
  // свойства будем добавлять для prototype
}

Animal.prototype.name = 'Животное';
Animal.prototype.identify = function() {
  console.log('Привет! Я - ' + this.name);
}

Теперь создадим экземпляр для Animal:

let dog = new Animal();
dog.name = 'Собака'; // переписываем свойство name

В строке dog.name = 'Собака'; движок JavaScript добавляет свойство name к объекту dog. В результате оба объекта dog и Animal.prototype имеют одинаковое свойство name.

Внутри метода identify() this ссылается на объект dog, поэтому this.name ссылается на свойство name объекта dog. В результате:

dog.identify();  // => Привет! Я - Собака

Попробуем удалить свойство name в объекте dog и вызовем identify():

delete dog.name;
dog.identify();  // => Привет! Я - Животное

JavaScript не может найти свойство name в объекте dog, поэтому он следует по цепочке прототипов и находит его в объекте Animal.prototype. Следовательно, this.name вернёт "Животное".

Конструктор-прототип (Constructor / prototype pattern)

Комбинация шаблонов конструктора и прототипа - наиболее распространенный способ определения пользовательских типов.

Шаблон конструктора определяет свойства объекта, а шаблон прототипа определяет методы и общие свойства.

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

function Animal(name) {
  this.name = name;
}

Animal.prototype.identify = function() {
  console.log('Привет! Я - ' + this.name);
}

let dog = new Animal('Собака');
dog.identify();  // => Привет! Я - Собака

let cat = new Animal('Кот')
cat.identify();  // => Привет! Я - Кот

 

Паразитарный конструктор (Parasitic constructor pattern)

В шаблоне паразитарного конструктора вы создаете функцию-конструктор, которая создает объект и возвращает этот объект.

function Animal(name) {
  let o = new Object();
  o.name = name;
  o.identify = function() {
    console.log('Привет! Я - ' + o.name);
  }
  return o;
}

В примере функция конструктора Animal такая же, как и в шаблоне фабрики. Однако вы вызываете её как конструктор с помощью оператора new.

var dog = new Animal('Собака');

По умолчанию оператор new возвращает объект, возвращаемый конструктором функции. Если constructor не возвращает объект, оператор new создает этот объект.

Устойчивый конструктор (Durable constructor pattern)

Устойчивый объект - это объект, не имеющий общедоступного свойства, а его методы не ссылаются на его this.

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

Пример:

function secureAnimal(name) {
  let o = new Object();
  o.identify = function() {
    console.log(name); // this не используем
  }
  return o;
}

let fox = secureAnimal('Лиса');
fox.identify();  // => Лиса

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