Creating JavaScript objects in 2018

7 minute read Published:

If you want to learn how some popular design patterns look in JavaScript I recommend you to read The Comprehensive Guide to JavaScript Design Patterns by Marko Mišura. It argues why learning design patterns is important and shows some examples. However, the post was written in 2015. Since then JavaScript has evolved and obsoleted some common patterns that we were used to in the past.

Making a “class” in JavaScript was a Wild West before ES6. JavaScript did not include concept of a class at the time. But everyone was used to the concept of classes from other languages. Because JavaScript supports constructors and has the new keyword, developers came up with several patterns to make something that behaved like classes in other languages.

This is an example presented by Marko in his guide:

// we define a constructor for Person objects
function Person(name, age, isDeveloper) {
    this.name = name;
    this.age = age;
    this.isDeveloper = isDeveloper || false;
}

// we extend the function's prototype
Person.prototype.writesCode = function() {
    console.log(this.isDeveloper? "This person does write code" : "This person does not write code");
}

// creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode
var person1 = new Person("Bob", 38, true);
// creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode
var person2 = new Person("Alice", 32);

// prints out: This person does write code
person1.writesCode();
// prints out: this person does not write code
person2.writesCode();

But this is not the only pattern that existed. There are others and they mostly differ on handling inheritance and private class members (properties and methods). ES6 simplifies and standardizes working with classes. The above code looks like this in ES6:

class Person {
  constructor(name, age, isDeveloper) {
    this.name = name;
    this.age = age;
    this.isDeveloper = isDeveloper || false;
  }
  
  writesCode() {
        console.log(this.isDeveloper? "This person does write code" : "This person does not write code");
  }
}

// creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode
var person1 = new Person("Bob", 38, true);
// creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode
var person2 = new Person("Alice", 32);

// prints out: This person does write code
person1.writesCode();
// prints out: this person does not write code
person2.writesCode();

Which browsers support ES6 classes?

The new class syntax is supported in Firefox, Chrome, Safari and MS Edge. Unfortunately Internet Explorer lacks support for most of the new ES6 features. This can be partially mitigated by using a transpiler such as Babel. Details about compatiblity are available at https://kangax.github.io/compat-table/es .

Are the above examples functionally equal?

Let’s see what the above code is equivalent to by using Babel.

This is our output:

"use strict";

var _createClass = (function() {
  function defineProperties(target, props) {
    for (var i = 0; i < props.length; i++) {
      var descriptor = props[i];
      descriptor.enumerable = descriptor.enumerable || false;
      descriptor.configurable = true;
      if ("value" in descriptor) descriptor.writable = true;
      Object.defineProperty(target, descriptor.key, descriptor);
    }
  }
  return function(Constructor, protoProps, staticProps) {
    if (protoProps) defineProperties(Constructor.prototype, protoProps);
    if (staticProps) defineProperties(Constructor, staticProps);
    return Constructor;
  };
})();

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var Person = (function() {
  function Person(name, age, isDeveloper) {
    _classCallCheck(this, Person);

    this.name = name;
    this.age = age;
    this.isDeveloper = isDeveloper || false;
  }

  _createClass(Person, [
    {
      key: "writesCode",
      value: function writesCode() {
        console.log(
          this.isDeveloper
            ? "This person does write code"
            : "This person does not write code"
        );
      }
    }
  ]);

  return Person;
})();

// creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode

var person1 = new Person("Bob", 38, true);
// creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode
var person2 = new Person("Alice", 32);

// prints out: This person does write code
person1.writesCode();
// prints out: this person does not write code
person2.writesCode();

Step 1: inline all the functions until we cannot do so any more

First iteration

var _createClass = function(Constructor, protoProps, staticProps) {
    if (protoProps) {
      for (var i = 0; i < protoProps.length; i++) {
        var descriptor = protoProps[i];
        descriptor.enumerable = descriptor.enumerable || false;
        descriptor.configurable = true;
        if ("value" in descriptor) descriptor.writable = true;
        Object.defineProperty(Constructor.prototype, descriptor.key, descriptor);
      } 
    }
    if (staticProps) {
      for (var i = 0; i < staticProps.length; i++) {
        var descriptor = staticProps[i];
        descriptor.enumerable = descriptor.enumerable || false;
        descriptor.configurable = true;
        if ("value" in descriptor) descriptor.writable = true;
        Object.defineProperty(Constructor.prototype, descriptor.key, descriptor);
      }  
    }
    return Constructor;
  };


var Person = (function() {
  function Person(name, age, isDeveloper) {
     if (!(this instanceof Person)) {
    throw new TypeError("Cannot call a class as a function");
  }

    this.name = name;
    this.age = age;
    this.isDeveloper = isDeveloper || false;
  }

  _createClass(Person, [
    {
      key: "writesCode",
      value: function writesCode() {
        console.log(
          this.isDeveloper
            ? "This person does write code"
            : "This person does not write code"
        );
      }
    }
  ]);

  return Person;
})();

// creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode

var person1 = new Person("Bob", 38, true);
// creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode
var person2 = new Person("Alice", 32);

// prints out: This person does write code
person1.writesCode();
// prints out: this person does not write code
person2.writesCode();

Second iteration:

var Person = (function() {
  function Person(name, age, isDeveloper) {
    if (!(this instanceof Person)) {
      throw new TypeError("Cannot call a class as a function");
    }

    this.name = name;
    this.age = age;
    this.isDeveloper = isDeveloper || false;
  }

  var protoProps = [{
    key: "writesCode",
    value: function writesCode() {
      console.log(
        this.isDeveloper
          ? "This person does write code"
          : "This person does not write code"
      );
    }
  }];
  for (var i = 0; i < protoProps.length; i++) {
    var descriptor = protoProps[i];
    descriptor.enumerable = descriptor.enumerable || false;
    descriptor.configurable = true;
    if ("value" in descriptor) descriptor.writable = true;
    Object.defineProperty(Person.prototype, descriptor.key, descriptor);
  } 

  return Person;
})();

// creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode

var person1 = new Person("Bob", 38, true);
// creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode
var person2 = new Person("Alice", 32);

// prints out: This person does write code
person1.writesCode();
// prints out: this person does not write code
person2.writesCode();

Step 2: unroll the for loop

var Person = (function() {
  function Person(name, age, isDeveloper) {
    if (!(this instanceof Person)) {
      throw new TypeError("Cannot call a class as a function");
    }

    this.name = name;
    this.age = age;
    this.isDeveloper = isDeveloper || false;
  }

  var descriptor = {
    key: "writesCode",
    value: function writesCode() {
      console.log(
        this.isDeveloper
          ? "This person does write code"
          : "This person does not write code"
      );
    }
  };
  descriptor.enumerable = descriptor.enumerable || false;
  descriptor.configurable = true;
  if ("value" in descriptor) descriptor.writable = true;
  Object.defineProperty(Person.prototype, descriptor.key, descriptor);

  return Person;
})();

// creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode

var person1 = new Person("Bob", 38, true);
// creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode
var person2 = new Person("Alice", 32);

// prints out: This person does write code
person1.writesCode();
// prints out: this person does not write code
person2.writesCode();

We can see that the code is now very similar to what we used to write by hand in old JavaScript. Esentially we define a function named Person which is the constructor. After that Object.defineProperty is used to add a method to Person.prototype.

There however is one function difference: constructors of ES6 classes require construction by using the new keyword:

//The following is valid with the old pattern. If Person is an ES6 class it throws TypeError("Cannot call a class as a function"). 
var person1 = Person("Bob", 38, true); 

Explore

I hope this post has been educational to you. Writing it surely was for me. You can investigate output of Babel yourself online. Here you can play with TypeScript. If you want to learn some C++ and assembly I recommend you to take a look at Compiler Explorer. You are also invited to try WebAssembly Explorer to see C++ and WebAssembly side by side.