barbarian meets coding

WebDev, UX & a Pinch of Fantasy

Summoning Fundamentals: A Three Part Introduction to OOP in JavaScript - Encapsulation

| Comments

The Mastering the Arcane Art of JavaScript-mancy series are my humble attempt at bringing my love for JavaScript to all other C# developers that haven’t yet discovered how awesome this language and its whole ecosystem are. These articles are excerpts of the super duper awesome JavaScript-Mancy book a compendium of all things JavaScript for C# developers.

In An Introduction to Object Oriented Programming in JavaScript for C# Developers I gave you a ten thousand feet introduction to object oriented programming in JavaScript. Now it is time to get into the nitty-gritty! We will start with a three part introduction to the pillars of Object Oriented Programming applied to JavaScript: encapsulation, inheritance and polymorphism.

In this article we’ll take a look at the principle of encapsulation and how to create objects through both object initializers and constructor functions. You’ll also refresh the techniques you have at your disposal to achieve information hiding. We will wrap the article with a comparison between object initializers, factories and constructor functions in a attempt to understand their strengths and weaknesses.

In the coming articles you’ll learn about JavaScript prototypical inheritance model and polymorphism and understand how both differ from what we are accustomed to in C#.

Encapsulation: Creating Objects in JavaScript

The principle of encapsulation consists in putting data and the functions that operate it together into a single component. In some definitions it includes the principle of information hiding (or data hiding), that is the ability to hide implementation details from consumers so as to define a clear boundary or interface that is safe to use from a consumer perspective. This in turn allows the author to change the hidden implementation details without breaking the contract with the consumer established by the public interface of a component. Thus both author and consumer can continue developing without getting in the way of each other, the author can tweak its implementation and the consumer can rest assured that the author won’t break his code. In this article, I will separate encapsulation from data hiding because JavaScript uses different ways to solve each of these problems.

Let’s start with encapsulation. JavaScript provides different strategies for achieving encapsulation: object initializers, constructor functions and ES6 classes. In this article we will take a look at the first two, and we will devote a whole article to ES6 classes later in these series.

Object Initializers

You can experiment with all examples in this article directly within this jsBin

In Understanding The Basics of JavaScript Objects you learned the intricacies of using object initializers (also known as object literals) to create new objects:

1
2
3
4
// creating a simple object
let object = {};
console.log(object);
// => [object Object] { ... }

And you saw how you can define any number of properties and methods in your objects:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// you can create objects with any number of properties and methods
let minion = {
  hp: 10,
  name: 'minion',
  toString(){ return this.name;}
};

console.log(minion);
// => [object Object] {
//  hp: 10,
//  name: "minion",
//  toString: function toString() {
//    return this.name;
//  }
// }

And even augment objects after they have been created:

1
2
3
minion.armor = 'chain mail';
console.log(minion.armor);
// => chain mail

We also studied the use of factory functions that ease object creation:

1
2
3
4
5
6
7
8
// we can use factories to ease object creation
function createMinion(name, hp=10){
  return {
    hp: hp,
    name: name,
    toString(){ return this.name;}
  };
}

You just call the factory function to create a new object and use it as you please:

1
2
3
4
5
6
7
let orc = createMinion(/* name */ 'orc', /* hp */ 100);
console.log(orc);
// => [object Object] {
//  hp: 100,
//  name: "orc",
//  etc...
// }

Well there’s another way to create objects in JavaScript that will feel much more familiar to a C# developer: Using the new operator and constructor functions.

Constructor Functions and the New Operator

In the previous section we saw how to create an object using an object initializer:

1
let object = {};

We can achieve the same thing by applying the new operator to a constructor function. Using this approach we get the statement below which is completely equivalent to the previous example:

1
2
3
let anotherObject = new Object();
console.log(anotherObject);
// => [object Object] { ... }

While the Object function let’s you create empty objects, you can apply the new operator on any function in JavaScript to instantiate new objects of your own devise. Functions that are called with the new operator are known as constructor functions:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Minion(name='minion', hp=10){
  this.hp = hp;
  this.name = minion;
  this.toString = () => this.name;
};

let anotherMinion = new Minion();
console.log(anotherMinion);
// => [object Object] {
//  hp: 10,
//  name: "minion",
//  toString: () => this.name
// }

So first thing to highlight here, in C# we use the new operator on classes to instantiate new objects, in JavaScript however we use the new operator on constructor functions to instantiate these objects. The constructor function is, in a way, acting as a custom type and a class definition since it defines the properties and methods of the resulting object that will be created when we invoke it. To bring this point home even more, if we use the instanceof operator in JavaScript which tells us the type of an object we’ll see that it is indeed Minion:

1
2
3
4
console.log(`anotherMinion is a Minion: ${anotherMinion instanceof Minion}`);
// => anotherMinion is a Minion: true
console.log(`anotherMinion is an Object: ${anotherMinion instanceof Object}`);
// => anotherMinion is an Object: true

If you take a look at the Minion constructor function and compare it with the factory function from the previous section you’ll notice that they are a little bit different. In the factory function we create an object via an object initializer and we return it. In this example however there is no object being created nor returned as far as we can see. What is happening here?

It all comes down to the new operator. When you use the new operator on a function there are several things happening under the hood:

  1. First an empty object is created and set as this for the function being executed. That is, in the example above, the this keyword refers to a new object that has just been created.
  2. If the constructor function has a prototype the new object is given that prototype (more on this in the next section).
  3. After that, the function body is invoked. In the example above, we add a series of properties to the new object namely hp, name and toString
  4. Finally the value of this is returned (unless we return something explicitly). As we see from the result of the console.log the object is created successfully.

Let’s see what happens if we return something explicitly from a constructor function.

1
2
3
4
5
6
7
8
9
10
11
12
13
// if you try to return a primitive it is ignored
function MinionOrBanana(name='minion', hp=10){
  this.hp = hp;
  this.name = name;
  return 'banana';
}

let isItAMinionOrIsItABanana = new MinionOrBanana();
console.log(isItAMinionOrIsItABanana)
// => [object Object] {
//  hp: 10,
//  name: "minion"
//}

In this example above we can see how if we try to return a string explicitly the JavaScript runtime will completely ignore it an return the constructed object. This is also applicable to all primitive types. But What happens if we return an object?

1
2
3
4
5
6
7
8
9
10
11
12
// if you try to return an object it is returned instead of the `this` object
function MinionOrBanana(name='minion', hp=10){
  this.hp = hp;
  this.name = name;
  return {name: 'banana'};
}

let isItAMinionOrIsItABanana = new MinionOrBanana();
console.log(isItAMinionOrIsItABanana)
// => [object Object] {
//  name: "banana"
//}

Well if you try to return an object explicitly {name: 'banana'} this object will be returned and your original object (the one injected as this to the constructor function) will be completely ignored.

JavaScript Arcana: Returning Explicitly from Constructor Functions

Returning expressions explicitly from constructor functions behaves in mysterious ways. If you return a primitive type such as a string or a number it will be ignored. If you return an object it will be returned from the constructor function and original object (the one injected as this in the function) will be lost in space and time.

You may have noticed that I called the constructor function Minion with uppercase instead of the common JavaScript naming convention of using camel case with functions minion. And Why? you may be asking yourself. A popular convention int he JavaScript community is to use uppercase when naming constructor functions to differentiate them from other functions. This convention is a way to tell the consumers of an API that they should use the new operator when calling these functions. But Why do we need to differentiate them? Aren’t all of them functions anyway?

Well consider what happens if we call a constructor function without the new operator:

1
2
3
let yetAnotherMinion = Minion();
console.log(yetAnotherMinion);
// => undefined

Hmm, no object is being returned… But Why? Can you remember what happened with this when a function is called without a context? Yes! That’s right! In the Many JavaScripts Quirks you learned that whenever we call a function without a context the value of this is set to the Window object (unless you are in strict mode in which case it will be undefined). What is happening then? By calling a constructor function without the new operator the function is evaluated in the context of the Window object and instead of creating a new object, we have just extended the Window object with two new properties hp and name:

1
2
3
4
console.log(window.hp);
// => 10
console.log(window.name);
// => 'minion'

And if we had committed the same mistake in strict mode we would’ve immediately received an error that would’ve alerted us much faster:

1
2
let yetAnotherMinion = Minion();
// => TypeError: Cannot set property 'hp' of undefined

JavaScript Arcana: Calling a Constructor Function Without The New Operator

When you call a constructor function without the new operator you run the risk of evaluating it in the context of the Window object or undefined in the case of strict mode.

So this is the cause why we usually use the uppercase notation when writing constructor fuctions. We want to avoid unsuspecting developers from forgetting the new operator and causing weird side-effects or errors in their programs. But conventions are not a very reliable thing are they. Wouldn’t it be better to have a foolproof way to protect our constructor functions so even if the new operator is not used they’ll still work?

We can make our constructor functions more sturdy by following this pattern:

1
2
3
4
5
6
7
function MinionSafe(name='minion', hp=10){
  'use strict';
  if (!this) return new MinionSafe(name, hp);

  this.name = name;
  this.hp = hp;
}

And now it doesn’t matter how we call the constructor function it will work as expected in any case:

1
2
3
4
5
6
7
8
9
10
11
console.log('using new operator: ', new MinionSafe());
// => [object Object] {
//  hp: 10,
//  name: "minion"
//}

console.log('using function call: ', MinionSafe());
// => [object Object] {
//  hp: 10,
//  name: "minion"
//}

Great! But can we improve it? Wouldn’t it be nice if we didn’t have to write the guard clause for every single constructor function we create? Functional programming to the rescue! We can define a safeConstructor function that represents an abstraction of the guard clause and which can be composed with any constructor function of our choosing:

1
2
3
4
5
6
function safeConstructor(constructorFn){
  return function(){
    return new constructorFn(...arguments); // ES6
    // return new (constructorFn.bind.apply(null, arguments); // ES5
  }
}

The safeConstructor function takes a constructorFn constructor function as argument and returns a new function that wraps this constructor function and ensures that the new operator is always called regardless of the circumstances. From now on we can reuse this function to guard any of our constructor functions:

1
2
3
4
// function Minion(name='minion', hp=10){
//    etc...
// }
let SafeMinion = safeConstructor(Minion);

By composing the safeConstructor function with the Minion constructor function we obtain a new function SafeMinion that will work even if we forget to use the new operator:

1
2
3
4
console.log(`using function:`, ${SafeMinion('orc', 110)});
// => using function: [object Object] etc...
console.log(`using new operator:`, ${new SafeMinion('pirate', 50)}`);
// => "using new operator: [object Object] etc..."

Data Hiding in JavaScript

In Understanding The Basics of JavaScript Objects you learned two patterns to achieve data hiding in JavaScript: closures and ES6 symbols, and how only closures provide real data privacy whilst ES6 symbols makes it harder to access “private” data.

Since constructor functions are just functions, you can use both closures and ES6 symbols to implement private properties and methods. Using closures is as easy as declaring variables in your function constructor body and referencing them from the methods that you want to expose:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// just like with factory methods you can implement data privacy 
// using closures with constructor functions
function WalkingMinion(name='minion', hp=10){
  let position = {x: 0, y: 0};

  this.hp = hp;
  this.name = name;
  this.toString = () => this.name;

  this.walksTo = (x,y) => {
    console.log(`${this} walks from (${position.x}, ${position.y}) to (${x}, ${y})`);
    position.x = x;
    position.y = y;
  };

};

In this example we have a WalkingMinion constructor function that we can use to create many walking minions. These minions would effectively have a private property position and would expose a walksTo method that we could use to command each minion to walk without revealing the actual implementation of the positioning system which in this case is just an object.

Indeed if we instantiate a walkingMinion using the above constructor we can see how there’s no way to access the position property:

1
2
3
let walkingMinion = new WalkingMinion();
console.log(walkingMinion.position);
// => undefined

The position property is not really part of the object itself but it’s effectively part of its internal state as the variable has been enclosed or captured by the walksTo function. This means that when we call the walksTo method can read of update the state of the position property as demonstrated below:

1
2
3
4
walkingMinion.walksTo(2, 2)
// => minion walks from (0, 0) to (2, 2)
walkingMinion.walksTo(3,3)
// => minion walks from (2, 2) to (3, 3)

In addition to achieving data hiding with closures you can use ES6 symbols:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function FlyingMinion(name='minion', hp=10){
  let position = Symbol('position');

  this.hp = hp;
  this.name = name;
  this.toString = () => this.name;

  this[position] = {x: 0, y: 0};
  this.fliesTo = (x,y) => {
    console.log(`${this} flies like the wind from (${this[position].x}, ${this[position].y}) to (${x}, ${y})`);
    this[position].x = x;
    this[position].y = y;
  };
};

And attain a similar behavior to that we saw with closures:

1
2
3
4
5
6
7
8
9
// again you cannot access the position property (almost)
let flyingMinion = new FlyingMinion();
console.log(flyingMinion.position);
// => undefined

flyingMinion.fliesTo(1,1);
// => minion flies like the wind from (0, 0) to (1, 1)
flyingMinion.fliesTo(3,3);
// => minion flies like the wind from (1, 1) to (3, 3)

But you must remember how ES6 symbols don’t offer true privacy since you can use Object.getOwnPropertySymbols to retrieve the symbols from an object and thus the “private” property, so prefer using closures over symbols.

Object Initializers vs Constructor Functions

object initializers constructor functions
Easy to write, convenient and readable Little bit more complicated. They look like normal functions but you need to implement them in a different way since the `new` will inject `this` as a new object
One-off creation of objects You can reuse them to create many objects
They only support information hiding via *ES6 symbols* They support information hiding via ES6 symbols and closures
Very simple syntax to define getters (read-only properties) and setters The only way to define getters and setters is using low level Object methods like Object.defineProperty
You don’t create subtypes and can’t use `instanceof`, but it is much better to rely on polymorphism than checking types Allows the creation of custom types and enables the use of `instanceof`
Calling a constructor function without the `new` operator can cause bugs and unwanted side effects if you don’t take measures to allow it.

Object Factories vs Constructor Functions

When you combine object initializers with factories you get all the benefits from both object initializers and constructor functions with none of the weaknesses of constructor functions:

Object initializers + factories Constructor functions
Easy to write, convenient and readable. Factory functions really behave like any other other function, no need to worry about `this` Little bit more complicated. They look like normal functions but you need to implement them in a different way since the `new` will inject `this` as a new object
You can reuse them to create many objects You can reuse them to create many objects
They support information hiding via *ES6 symbols* and closures They support information hiding via *ES6 symbols* and closures
Very simple syntax to define getters (read-only properties) and setters The only way to define getters and setters is using low level Object methods like Object.defineProperty
You don’t create subtypes and can’t use `instanceof`, but it is much better to rely on polymorphism than checking types Allows the creation of custom types and enables the use of `instanceof`
Factory functions works just like any other function. No need to use `new` and thus no need to remember to use it or guard from forgetting it. Very easy to compose with other functions Calling a constructor function without the `new` operator can cause bugs and unwanted side effects if you don’t take measures to allow it

Concluding

In this article you learnt about the first piece of JavaScript Object Oriented Programming, encapsulation, and how you can achieve it using object initializers, factory functions and constructor functions.

Object initializers resemble C# object literals. They are very straightforward to use and very readable, but you can only create one-off objects with them and they only allow for information hiding through ES6 symbols (which is just a step better from convention-based information hiding).

You can enhance your object initializers by wrapping them in factory functions. By doing that you gain the ability to create many objects of the same type and true information hiding via closures.

Finally you can also use constructor functions and the new operator as a method of encapsulation and to instantiate new objects. A constructor function lets you define the properties and methods of an object by augmenting this that is passed to the function as a new object when the function is called with the new operator. Because it is a function it supports information hiding with both ES6 symbols and closures. Although it is a function, it expects to be called with the new operator and have a this object to augment, so if it is called as a regular function it may cause bugs and have unexpected side-effects. We can guard against that problem by implementing a guard for when this happens.

In the next article you’ll discover the next piece of JavaScript Object Oriented Programming: prototypical inheritance. See you then! Have a great day!

Interested In Learning More JavaScript? Buy the Book!

Are you a C# Developer interested in learning JavaScript? Then take a look at the JavaScript-mancy book, a complete compendium of JavaScript for C# developers.

a JavaScriptmancy sample cover

More Articles in These Series

Comments