Создание объектов JavaScript: Шаблоны и практические рекомендации

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

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

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

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

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

Функции-фабрики

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

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

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

Цепочка прототипов

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

thing.prototype.f = function() {};
thing.prototype.g = function() {};
function thing() {
  var o = Object.create(thing.prototype);
  o.x = 42;
  o.y = 3.14;
  return o;
}
var o = thing();

Этот способ JavaScript создания объектов также имеет один недостаток. Он приводит к возникновению повторений. Первые и последние строки функции 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.

Thing.prototype.f = function() {};
Thing.prototype.g = function() {};
function Thing() {
  this.x = 42;
  this.y = 3.14;
}
var o = new Thing();

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

Но у этого способа JavaScript работы с объектами есть недостаток. Это громоздко и некрасиво, а реализации наследования еще более громоздки и некрасивы.

Классы ES6

Это относительно недавнее дополнение к JavaScript, которое предлагает значительно более чистый синтаксис, чтобы сделать то же самое.

class Thing {
  constructor() {
    this.x = 42;
    this.y = 3.14;
  }
  f() {}
  g() {}
}
var o = new Thing();

Сравнение

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

Представление

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

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

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

Функционал

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

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

Вывод

Учитывая все сказанное выше, мое предпочтение — синтаксис классов. Это стандартный, простой и чистый способ.

Перевод статьи «JavaScript Object Creation: Patterns and Best Practices» был подготовлен дружной командой проекта Сайтостроение от А до Я.