Создание Объектов в JavaScript: Паттерны и Best Practices
Тип Поста

Создание Объектов в JavaScript: Паттерны и Best Practices

Создание Объектов в JavaScript: Паттерны и Best Practices

Создание объектов в JavaScript является весьма запутанным вопросом. В этом языке существует множество вариантов создания объектов, и поэтому, в независимости от того, являетесь ли вы новичком или ветераном – может возникнуть ситуация, когда нет четкой уверенности, какой вариант выбрать. Но несмотря на все это разнообразие и отличный друг от друга синтаксис, варианты реализации довольно-таки схожи между собой. В этой статье мы с вами разберем несколько различных стилей создания объектов и каким образом, каждый из этих вариантов создается на основе других.

Литералы объектов

Первое, что мы с вами рассмотрим – это самый простой способ создания объектов с помощью их литералов. JavaScript предоставляет возможность создавать объекты как-бы “из ничего”, т.е. без классов, шаблонов, прототипов – простым объявлением объектов с методами и данными.

var o = {
  x: 42,
  y: 3.14,
  f: function() {},
  g: function() {}
};

Но в таком способе есть один недостаток. Если нам понадобится создать точно такой же объект где-нибудь в другом месте, то для этого потребуется полностью копипастить методы объекта, данные и его инициализацию. Нам нужен другой способ создания не просто одиночного объекта, а семейства объектов.

Фабричные функции

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

function thing() {
  return {
    x: 42,
    y: 3.14,
    f: function() {},
    g: function() {}
  };
}

var o = thing();

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

Прототипная последовательность

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

var thingPrototype = {
  f: function() {},
  g: function() {}
};

function thing() {
  var o = Object.create( thingPrototype );

  o.x = 42;
  o.y = 3.14;

  return o;
}

var o = thing();

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

javascript-objects-01

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

ES5 классы

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

function create( fn ) {
  var o = Object.create( fn.prototype );

  fn.call( o );

  return o;
}

// ...

Thing.prototype.f = function() {};
Thing.prototype.g = function() {};

function Thing() {
  this.x = 42;
  this.y = 3.14;
}

var o = create( Thing );

По существу, данный способ также является достаточно часто используемым паттерном, поэтому его поддержка встроена в JavaScript. Функция create, которую мы объявили в примере выше, являет собой рудиментарную версию ключевого слова new, и мы можем избавиться от этой функции, заменив её словом new.

Thing.prototype.f = function() {};
Thing.prototype.g = function() {};

function Thing() {
  this.x = 42;
  this.y = 3.14;
}

var o = new Thing();

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

Но подобная реализация выглядит не слишком привлекательно – она многословна и безобразна, и эти неприятные качества еще более усугубляется при осуществлении наследования. И здесь мы подошли вплотную к восхитительным возможностям нового синтаксиса ES6.

ES6 классы

Относительно недавно добавленные в JavaScript ES6 классы предоставляют значительно более чистый синтаксис для производства таких же действий, что мы совершили в предыдущем примере.

class Thing {
  constructor() {
    this.x = 42;
    this.y = 3.14;
  }

  f() {}
  g() {}
}

var o = new Thing();

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

Производительность

Движок JavaScript настолько хорошо оптимизируется, что на сегодняшний день крайне трудно будет аргументировано сказать после изучения кода, какой вариант будет быстрее работать. Поэтому измерения являются ключевым моментом в этом деле. Но даже проводимые измерения могут ввести нас в заблуждение. Релизы движка JavaScript обычно выходят каждые шесть недель, и иногда со значительными изменениями в производительности. И по этой причине, сделанные нами до этого измерения и любые решения, принятые на основе этих результатов – можно смело выкинуть в окно. Моим правилом было отдавать предпочтение наиболее официально поддерживаемому и широко распространенному синтаксису на сегодняшний день, на том основании, что такой вариант будет получать самое пристальное внимание от разработчиков и в результате окажется самым производительным. Прямо сейчас таким синтаксисом является ES6 классы, и производительность такого варианта, примерно, в три раза быстрее, чем фабричные функции с возвращаемыми литералами.

Особенности

Некоторые различия между классами и фабричными функциями, которые были между ними, исчезли с приходом ES6. Сегодня и фабричные функции, и классы, могут обеспечить по-настоящему приватные данные: фабричные функции с помощью замыканий, а классы благодаря WeakMap. Оба эти способа позволяют достичь множественного наследования: фабричные функции через подмешивание других свойств в собственный объект; классы также с помощью смешивания других свойств с прототипом или с фабрикой класса, или с прокси. И функции, и классы могут вернуть любой произвольный объект, если в этом будет необходимость. А также оба способа имеют простой синтаксис.

Заключение

Рассмотрев способы работы с объектами, наиболее предпочтительным вариантом окажется синтаксис ES6 классов. На данный момент он является стандартом, он прост в использовании и имеет чистый вид, и хорошую производительность. А также предоставляет все возможности, которые доступны в старом-добром способе с фабричной функцией.

При создании статьи были использованы следующие источники:

  1. JavaScript Object Creation: Patterns and Best Practices
  2. MDN JavaScript Docs
Поделиться

Оставить комментарий

Вы можете использовать следующие HTML-теги:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>
Обязательно к заполнению