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

JavaScript обладает мощной и удобной возможностью создания объектов с использованием объектных литералов. Стандарт ES2015 (ES6) упрощает работу с объектами при создании приложений для современных браузеров (кроме IE) и для платформы Node.js.



Основы


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

// ES5
var myObject = {
  prop1: 'hello',
  prop2: 'world',
  output: function() {
    console.log(this.prop1 + ' ' + this.prop2);
  }
};

myObject.output(); // hello world

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

Инициализация объектов из переменных


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

// ES5
var
  a = 1, b = 2, c = 3;
  obj = {
    a: a,
    b: b,
    c: c
  };

// obj.a = 1, obj.b = 2, obj.c = 3

В ES6 больше не нужно повторять имена переменных:

// ES6
const
  a = 1, b = 2, c = 3;
  obj = {
    a,
    b,
    c
  };

// obj.a = 1, obj.b = 2, obj.c = 3

Этот приём может оказаться полезным для возвращаемых объектов при использовании паттерна Revealing Module, который позволяет создавать пространства имён для различных фрагментов кода для того, чтобы избежать конфликтов имён. Например:

// ES6
const lib = (() => {

  function sum(a, b)  { return a + b; }
  function mult(a, b) { return a * b; }

  return {
    sum,
    mult
  };

}());

console.log( lib.sum(2, 3) );  // 5
console.log( lib.mult(2, 3) ); // 6

Возможно, вы видели как этот приём используется в ES6-модулях:

// lib.js
function sum(a, b)  { return a + b; }
function mult(a, b) { return a * b; }

export { sum, mult };

Сокращённый синтаксис объявления методов объектов


При объявлении методов объектов в ES5 необходимо использовать ключевое слово function:

// ES5
var lib = {
  sum:  function(a, b) { return a + b; },
  mult: function(a, b) { return a * b; }
};

console.log( lib.sum(2, 3) );  // 5
console.log( lib.mult(2, 3) ); // 6

Теперь, в ES6, так больше можно не делать. Здесь допустим следующий сокращённый способ объявления методов:

// ES6
const lib = {
  sum(a, b)  { return a + b; },
  mult(a, b) { return a * b; }
};

console.log( lib.sum(2, 3) );  // 5
console.log( lib.mult(2, 3) ); // 6

Надо отметить, что здесь нельзя использовать стрелочные функции ES6 (=>), так как у методов должны быть имена. Однако стрелочные функции можно использовать если явно назначать имена методам (как в ES5). Например:

// ES6
const lib = {
  sum:  (a, b) => a + b,
  mult: (a, b) => a * b
};

console.log( lib.sum(2, 3) );  // 5
console.log( lib.mult(2, 3) ); // 6

Динамические ключи


В ES5 нельзя было использовать переменные в качестве имён ключей, хотя ключ, имя которого задаётся переменной, можно было добавить после создания объекта. Например:

// ES5
var
  key1 = 'one',
  obj = {
    two: 2,
    three: 3
  };

obj[key1] = 1;

// obj.one = 1, obj.two = 2, obj.three = 3

В ES6 ключи можно назначать динамически, помещая выражение, определяющее имя, в квадратные скобки ([]). Например:

// ES6
const
  key1 = 'one',
  obj = {
    [key1]: 1,
    two: 2,
    three: 3
  };

// obj.one = 1, obj.two = 2, obj.three = 3

Для создания ключа можно использовать любое выражение:

// ES6
const
  i = 1,
  obj = {
    ['i' + i]: i
  };

console.log(obj.i1); // 1

Динамические ключи можно использовать и для методов, и для свойств:

// ES6
const
  i = 2,
  obj = {
    ['mult' + i]: x => x * i
  };

console.log( obj.mult2(5) ); // 10

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

Деструктурирование


Деструктурирование — это извлечение свойств объектов и назначение их переменным. Часто в ходе разработки приложений необходимо извлечь значение свойства объекта и записать его в переменную. В ES5 нужно было описывать это следующим образом, пользуясь командами доступа к свойствам:

// ES5
var myObject = {
  one:   'a',
  two:   'b',
  three: 'c'
};

var
  one   = myObject.one, // 'a'
  two   = myObject.two, // 'b'
  three = myObject.three; // 'c'

ES6 поддерживает деструктурирование. Можно создать переменную с тем же именем, которое носит соответствующее свойство объекта, и сделать следующее:

// ES6
const myObject = {
  one:   'a',
  two:   'b',
  three: 'c'
};

const { one, two, three } = myObject;
// one = 'a', two = 'b', three = 'c'

Переменные, в которые попадают значения свойств объекта, могут, на самом деле, иметь любые имена, но в том случае, если они отличаются от имён свойств, необходимо пользоваться конструкцией { propertyName: newVariable }:

// ES6
const myObject = {
  one:   'a',
  two:   'b',
  three: 'c'
};

const { one: first, two: second, three: third } = myObject;
// first = 'a', second = 'b', third = 'c'

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

// ES6
const meta = {
  title: 'Enhanced Object Literals',
  pageinfo: {
    url: 'https://www.sitepoint.com/',
    description: 'How to use object literals in ES2015 (ES6).',
    keywords: 'javascript, object, literal'
  }
};

const {
  title   : doc,
  pageinfo: { keywords: topic }
} = meta;

/*
  doc   = 'Enhanced Object Literals'
  topic = 'javascript, object, literal'
*/

Поначалу всё это может показаться сложным, однако, с этим не так уж и тяжело разобраться, главное — помнить следующее:

  • Правая часть выражения — это источник данных — массив или объект, который хранит извлекаемые данные.
  • Левая часть выражения — это цель деструктурирующего присваивания — структура, описывающая переменные, которым будут назначены значения, извлечённые из массива или объекта.

При использовании деструктурирования можно столкнуться с некоторыми сложностями. Так, выражение нельзя начать с фигурной скобки, так как тогда оно будет выглядеть как блок кода. Например:

{ a, b, c } = myObject; // неправильно

Эта конструкция нормально воспринимается системой при объявлении переменных:

const { a, b, c } = myObject; // правильно

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

let a, b, c;
({ a, b, c } = myObject); // правильно

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

Деструктурирование — это приём, который может пригодиться во многих ситуациях.

Параметры функций по умолчанию


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

prettyPrint( {
  title: 'Enhanced Object Literals',
  publisher: {
    name: 'SitePoint',
    url: 'https://www.sitepoint.com/'
  }
} );

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

// ES5, назначение значений по умолчанию
function prettyPrint(param) {

  param = param || {};
  var
    pubTitle = param.title || 'No title',
    pubName = (param.publisher && param.publisher.name) || 'No publisher';

  return pubTitle + ', ' + pubName;

}

В ES6 любым параметрам можно назначать значения по умолчанию:

// ES6 - значения параметров по умолчанию
function prettyPrint(param = {}) { ... }

Затем можно воспользоваться деструктурированием для извлечения из объекта значений, и, при необходимости, для назначения значений по умолчанию:

// ES6 деструктурированное значение по умолчанию
function prettyPrint(
  {
    title: pubTitle = 'No title',
    publisher: { name: pubName = 'No publisher' }
  } = {}
) {

  return `${pubTitle}, ${pubName}`;

}

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

Разбор объектов, возвращаемых функциями


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

// ES5
var
  obj = getObject(),
  one = obj.one,
  two = obj.two,
  three = obj.three;

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

// ES6
const { one, two, three } = getObject();

Возможно, вы видели нечто подобное в программах для Node.js. Например, если вам нужны только методы readFile() и writeFile() модуля fs, получить ссылки на них можно так:

// ES6 Node.js
const { readFile, writeFile } = require('fs');

readFile('file.txt', (err, data) => {
  console.log(err || data);
});

writeFile('new.txt', 'new content', err => {
  console.log(err || 'file written');
});

Синтаксис оставшихся параметров и оператор расширения ES2018 (ES9)


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

const myObject = {
  a: 1,
  b: 2,
  c: 3
};

const { a, ...x } = myObject;
// a = 1
// x = { b: 2, c: 3 }

Похожий подход можно использовать и для передачи неких значений в функцию:

function restParam({ a, ...x }) {
  // a = 1
  // x = { b: 2, c: 3 }
}

restParam({
  a: 1,
  b: 2,
  c: 3
});

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

Оператор расширения можно использовать внутри объектов:

const
  obj1 = { a: 1, b: 2, c: 3 },
  obj2 = { ...obj1, z: 26 };

// obj2 is { a: 1, b: 2, c: 3, z: 26 }

Оператор расширения допустимо применять для клонирования объектов (obj2 = { ...obj1 };), однако тут надо учитывать то, что при таком подходе выполняется мелкая копия объекта. Если свойствами объектов являются другие объекты, клон объекта будет ссылаться на те же самые вложенные объекты.

Синтаксис оставшихся параметров и оператор расширения пока пользуются не слишком широкой поддержкой. В настоящий момент ими, без дополнительных усилий, можно пользоваться в браузерах Chrome и Firefox, и при разработке для платформы Node.js версии 8.6 и выше.

Итоги


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

Уважаемые читатели! Какими способами создания JS-объектов вы пользуетесь чаще всего?

Комментарии (3)


  1. k12th
    22.06.2018 11:00
    +1

    Вы бы еще про ES5 что-нибудь перевели.


  1. dfayziev
    22.06.2018 12:27

    Отличная статья


  1. Keyten
    22.06.2018 17:38
    +5

    Интересно, автора не смущает писать в 2018 году про ES2015, повторяя статьи 2012-го года?