Summoning Fundamentals: A Three Part Introduction To OOP in JavaScript - Encapsulation
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:
// 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:
// 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:
minion.armor = 'chain mail'
console.log(minion.armor)
// => chain mail
We also studied the use of factory functions that ease object creation:
// 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:
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:
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:
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:
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
:
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:
- First an empty object is created and set as
this
for the function being executed. That is, in the example above, thethis
keyword refers to a new object that has just been created. - If the constructor function has a prototype the new object is given that prototype (more on this in the next section).
- After that, the function body is invoked. In the example above, we add a series of properties to the new object namely
hp
,name
andtoString
- Finally the value of
this
is returned (unless we return something explicitly). As we see from the result of theconsole.log
the object is created successfully.
Let’s see what happens if we return something explicitly from a constructor function.
// 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?
// 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:
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
:
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:
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 theWindow object
orundefined
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:
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:
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:
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:
// 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:
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:
// 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:
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:
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:
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:
// 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.
Have a great week ahead!! :)
More Articles In This Series
- Chapter 0: The Basic Ingredients of JavaScriptMancy
- Chapter 1: The Many a One JavaScript Quirks
- Functions
- Object Oriented Programming
- Basics
- ES6
- OOP
- Introduction to OOP in JavaScript for C# Developers
- Summoning Fundamentals: Introduction to OOP In JavaScript – Encapsulation
- Summoning Fundamentals: Introduction to OOP In JavaScript – Inheritance
- Summoning Fundamentals: Introduction to OOP In JavaScript – Polymorphism
- White Tower Summoning: Mimicking C# Classical Inheritance in Javascript
- White Tower Summoning Enhanced: The Marvels of ES6 Classes
- Black Tower Summoning: Object Composition with Mixins
- Safer JavaScript Composition With Traits and Traits.js
- JavaScript Ultra Flexible Object Oriented Programming with Stamps
- Object Oriented JavaScript for C# Programmers
- Tooling
- Data Structures
- Functional Programming
- And more stuff to be shaped out:
- Functional Programming with JavaScript
- Modules
- Asynchronous Programming
- The JavaScript Ecosystem
Written by Jaime González García , dad, husband, software engineer, ux designer, amateur pixel artist, tinkerer and master of the arcane arts. You can also find him on Twitter jabbering about random stuff.