ES6 in Action: Enhanced Object Literals

Craig Buckler
Share

This article looks at what’s possible with object literals in JavaScript, especially in the light of recent ECMAScript updates.

The ability to create JavaScript objects using literal notation is powerful. New features introduced from ES2015 (ES6) make object handling even easier in all modern browsers (not IE) and Node.js.

Creating objects in some languages can be expensive in terms of development time and processing power when a class must be declared before anything can be achieved. In JavaScript, it’s easy to create objects on the fly. For example:

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

myObject.output(); // hello world

Single-use objects are used extensively. Examples include configuration settings, module definitions, method parameters, return values from functions, etc. ES2015 (ES6) added a range of features to enhance object literals.

Object Initialization From Variables

Objects’ properties are often created from variables with the same name. For example:

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

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

There’s no need for nasty repetition in ES6!…

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

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

This could be useful for returned objects when using a revealing module pattern, which (effectively) namespaces code in order to avoid naming conflicts. For example:

// ES6 code
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

You’ve possibly seen it used in ES6 modules:

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

export { sum, mult };

Object Method Definition Shorthand

Object methods in ES5 require the function statement. For example:

// ES5 code
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

This is no longer necessary in ES6; it permits the following shorthand syntax:

// ES6 code
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

It’s not possible to use ES6 fat arrow => function syntax here, because the method requires a name. That said, you can use arrow functions if you name each method directly (like ES5). For example:

// ES6 code
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

Dynamic Property Keys

In ES5, it wasn’t possible to use a variable for a key name, although it could be added after the object had been created. For example:

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

obj[key1] = 1;

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

Object keys can be dynamically assigned in ES6 by placing an expression in [ square brackets ]. For example:

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

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

Any expression can be used to create a key. For example:

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

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

A dynamic key can be used for methods as well as properties. For example:

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

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

Whether you should create dynamic properties and methods is another matter. The code can be difficult to read, and it may be preferable to create object factories or classes.

Destructuring (Variables From Object Properties)

It’s often necessary to extract a property value from an object into another variable. This had to be explicitly declared in ES5. For example:

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

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

ES6 supports destructuring: you can create a variable with the same name as an equivalent object property. For example:

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

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

It’s also possible to assign properties to variables with any name using the notation { propertyName: newVariable }. For example:

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

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

More complex objects with nested arrays and sub-objects can also be referenced in destructuring assignments. For example:

// ES6 code
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'
*/

This initially appears complicated, but remember that in all destructuring assignments:

  • the left-hand side of the assignment is the destructuring source — the array or object which holds the data being extracted
  • the right-hand side of the assignment is the destructuring target — the pattern which defines the variable being assigned.

There are a number of caveats. You can’t start a statement with a curly brace, because it looks like a code block. For example:

{ a, b, c } = myObject; // FAILS

You must either declare the variables — for example:

const { a, b, c } = myObject; // WORKS

or use parentheses if variables have already been declared — for example:

let a, b, c;
({ a, b, c } = myObject); // WORKS

You should therefore be careful not to mix declared and undeclared variables.

There are a number of situations where object destructuring is useful.

Default Function Parameters

It’s often easier to pass a single object to a function than use a long list of arguments. For example:

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

In ES5, it’s necessary to parse the object to ensure appropriate defaults are set. For example:

// ES5 assign defaults
function prettyPrint(param) {

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

  return pubTitle + ', ' + pubName;

}

In ES6, we can assign a default value to any parameter. For example:

// ES6 default value
function prettyPrint(param = {}) { ... }

We can then use destructuring to extract values and assign defaults where necessary:

// ES6 destructured default value
function prettyPrint(
  {
    title: pubTitle = 'No title',
    publisher: { name: pubName = 'No publisher' }
  } = {}
) {

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

}

Whether you find this code easier to read is another matter!

Parsing Returned Objects

Functions can only return one value, but that could be an object with hundreds of properties and/or methods. In ES5, it’s necessary to obtain the returned object, then extract values accordingly. For example:

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

ES6 destructuring makes this process simpler, and there’s no need to retain the object as a variable:

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

You may have seen similar assignments in Node.js code. For example, if you only required the File System (fs) methods readFile and writeFile, you could reference them directly. For example:

// 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) Rest/Spread Properties

In ES2015, rest parameter and spread operator three-dot (...) notation applied to arrays only. ES2018 enables similar rest/spread functionality for objects. A basic example:

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

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

You can use the technique to pass values to a function:

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

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

You can only use a single rest property at the end of the declaration. In addition, it only works on the top level of each object and not sub-objects.

The spread operator can be used within other objects. For example:

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

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

You could use the spread operator to clone objects (obj2 = { ...obj1 };), but be aware you only get shallow copies. If a property holds another object, the clone will refer to the same object.

ES2018 (ES9) rest/spread property support is patchy, but it’s available in Chrome, Firefox and Node.js 8.6+.

Object literals have always been useful. The new features introduced from ES2015 did not fundamentally change how JavaScript works, but they save typing effort and lead to clearer, more concise code.