Hard parts of Object Oriented programming in JavaScript
A 1 day workshop held by Will Sentance for FrontEndMasters.
Please watch the course first on FrontendMasters then come back for the notes!
Will Sentance is a founder/instructor at Codesmith a developer bootcamp aimed for mid and senior-level engineers.
Main concepts of OOP
OOP is still perhaps to most widely used programming paradigm in the professional/entreprise world.
It's a way to "bundle together" data and their relevant functions that may act on this data (encapsulation). So in most cases, you have some data, e.g. a user with its properties (name
, score
) and some functions that can change this data (increasescore
).
Why OOP?
It's easy to add new features. You just "augment" your object with new members (properties) or new functions.
It's easy to reason about. Both data and functions are bundled together, so it's very easy from looking at the code (or the specs) and see what are the related parts of the application.
Efficient for memory. Newly created objects delegate to the same function that only exist in one place in the memory.
Will mentions that the React codebase is still mostly based on OOP.
Ways to create objects
Object literal
The simplest way is simply declaring your object (object literal):
const user1 = {
name: 'phil',
score: 4,
increment: function (){
user1.score++
}
}
user1.incerment()
Dot notation
We can use the dot notation to assign properties and methods:
user2 = {}
use2.age = 21
Object.create
Object.create
always returns an empty object!
const user3 = Object.create(null);
Important to note, that Object.create
always returns an empty object! The implications of this will be more clear later.
Then, you can populate it:
user3.name = "Phil"
This is of course not only breaking the DRY principle, but not efficient for the memory either (when creating lots of users).
To make it more DRY (but not more memory efficient), you can:
function userCreator(name,score){
const newUser = {}
newUser.name = name
newUser.score = score
newUser.increment = function(){
newUser.score++
}
return newUser
}
Now, you can stamp out many users, but there are two issues with it:
- hard to expand with new properties
- not efficient in memory (
increment
function being repeated)
Better, more efficient organisation via function delegation
The prototype chain
The solution to the above problem is function delegation, which (in JavaScript) involves "linking objects together", via the Prototype chain. This way, we don't need to add (and repeat!) functions for each of our object, but the JavaScript engine will look up on the prototype chain (via the proto property) until it finds the method we are asking for.
There are several ways in JavaScript to create such a prototype chain (new
, class
, Object.create
), but the basic mechanism behind it remain the same.
Using Object.create to link functions
In this pattern, you run Object.create(fooObj)
. This will still return an empty object, but will also create a "special link" to fooObj
in the execution context.
function userCreator(name, score) {
const newUser = Object.create(userFunctionStore)
newUser.name = name
newUser.score = score
return newUser
}
const userFunctionStore = {
increment: function(){this.score++},
login: function(){...}
}
const user1 = userCreator('phil', 4)
const user2 = userCreator('julia', 6)
user1.increment()
Aside on Execution Context
This is how the JavaScript engine runs and interprets functions. It has a local memory, where it first assigns arguments to parameters. For example, running userCreator('phil', 3)
, immediately assigns name: 'phil
and score: 3
.
Inside it, when we run Object.create
, it returns an empty object, but in the function's execution context it will be linked to userFunctionStore
.
The execution context will also become important when the engine has to resolve the this
variable.
The __proto__
property
In the JavaScript specs, it's called [[prototype]]
, and Will argues that it's not a very good name, it will be too overloaded, and cause confusion as we will see later.
As seen in the above example Object.create
returns an empty object, but on it's __proto__
property (observable in any debugger tool, devtools) it will point to the object passed to it.
JavaScript also uses internally this mechanism, so objects and functions by default have a __proto__
property that includes some built-in methods of the language.
This is all the basics of JavaScript's OO implementation!
The basic concept is the prototype chain, that the engine uses to look up methods or properties we want to access inside the execution context. The look up can go "up" several levels and the JavaScript engine uses this method to add it's own "features" to the base types.
In other languages, the inner workings are quite different and the declaration of these relations is much simpler.
You can also just create on Object and use it only once. It's still useful to encapsulate functionalities.
There are other methods in JavaScript to simplify these steps, but the inner workings still remain the same.
Using the new
keyword to simplify the object linking syntax
The basic syntax is just
const user1 = new UserCreator('phil', 4)
Note on the capital first letter: it's just a convention, to let other users know that you meant to use your function with the new
call.
In JavaScript, functions are both objects and functions
To understand the steps that the new
call goes through, first we have to know that function
s are a special types in JavaScript, in which that they are Objects/Functions. This means that they can have properties as well, and one is the .prototype
property. See the comment above that [[prototype]]
spec and .prototype
property can become confusing (and even more on this later).
What the new
keyword does
-
It automatically returns a new object for us. See the introductinary example above,
userCreator
. Note, in the example below, that we don'treturn
anything fromUserCreator
. -
The
this
keyword is bound to this object that is returned to us, inside the function that is called with thenew
keyword. This is how we can populate this object with properties (user:'phil', score:4
) from above. -
The returned object has a link (via
__proto__
) to the.prototype
property of the function used for thenew
call. (Because of the "Object/funtion combo" nature of JavaScript functions)
A code example
function UserCreator(name, score){
this.name = name
this.score = score
}
// note, that Functions are Object/Function combos and
// therefore can have properties
// notably the `.prototype` prop
UserCreator.prototype.increment = function(){
this.score++
}
UserCreator.prototype.login = function() {
console.log('login')
}
const user1 = new UserCreator('Eva', 9)
user1.increment()
Distinction between the __proto__
and .prototype
Here, they are not the same. __proto__
used to look up, or access properties or methods through the [[prototype]]
chain, while the .prototype
property is used to add extra elements to the Object/function comb.
With the new
keyword, the __proto__
of the new Object will point to the .prototype
property of the "original" Object/function combo.
new
auto creates and returns an Object
Note (again) above, that UserCreator
did not explicitly return anything! The this
variable is bound to the newly created object so you can add new properties before it's auto returned. This all happens inside a function execution context that gets created when the function is run (with or without the new
keyword).
Extra notes on the new
keyword
The benefit is really that it's faster to write, and is very widely used in professional practice. The downside is that the majority (99% according to Will) of the developers don't know how it works internally.
The convention is to uppercase your function to hint that it has to be called with the new
keyword. Otherwise the this
will be bound to either the global object (window, global) (or undefined if in strict mode).
The class
keyword will simplify this pattern even further.
More on the this
keyword and execution context
There are four rules on how the this
parameter is resolved (from my previous notes):
- function called via
new
, see above - function called as on the right side of
.
(user1.increment()
), it points to the object to the left of the.
- explicitly binding it
bind
,apply
,call
- the global context
Confusion between the this
inside UserCreator
and .prototype.increment
They do not point to the same parameter. See the rules above how the engine uses the this
inside the execution context of the function.
In the first case (inside UserCreator
), it's the first rule, in the second case it's second rule.
A case that usually causes confusion
According to Will, this is one of the biggest "gotchas" in JavaScript:
UserCreator.prototype.increment = function(){
function add1(){
this.score++
}
add1()
}
This will not work as intended, because as soon as you call add1()
it creates a new execution context, and the JavaScript engine will go through again the rules to determine where the this
should point to and it will be rule 4 that will be applied.
How to solve this (arrow function)
The old way used to be declaring var that = this
(code smell according to Kyle Simpson).
Today, a better solution is via the arrow functions that will "statically" (or "lexically") scope the this
variable:
UserCreator.prototype.increment = function(){
const add1 = () => { this.score++ }
add1()
}
So basically, the arrow function will "pass down" the this
variable to whatever it was in it's own execution context.
There's another popular way, by returning a new function via the .bind
method.
Will recommends that today the best practice is to use arrow functions.
Class keyword in JavaScript
It basically bundles together calling the function with the new
keyword (it will be the constructor
) and bundling with it the .prototype
"extensions".
It's a "facade" (or syntactic sugar) to previous patterns. It does have a few advantages, that we will se later (in subclassing)
Behind the scenes though the mechanis are the same (resolution via the proto chain).
class UserCreator {
constructor (name, score) {
this.name = name
this.score = score
}
increment () {
this.score++
}
login () {
console.log('login')
}
}
const user1 = new UserCreator('Eva', 9)
user1.increment()
Subclassing
Subclassing is about distributing our functions on our objects in a structured way. A subclass is a slightly more specific version of a more generic base object.
Inheritance in JavaScript is not really the precise term. The subclass gets a "right" to go and look up methods or properties (via the proto chain).
Subclassing with factory functions
This is the base pattern. Other methods (new
keyword, class ... extends
) are equivalent to this, but with a simpler syntax.
The idea is to manually link, using Object.createPrototypeOf()
, the proto to another Object/Function combo's .prototype
. We need do this two times, when creating a subclass. First, in the declaration, setting our "subclass" pointer to the base "class", then we need to reset the pointer again when executing the base "factory" function.
function userCreator(name, score) {
const newUser = Object.create(userFunctions)
newUser.name = name
newUser.score = score
return newUser
}
const userFunctions = {
sayName: function () { console.log("I'm", this.name)},
increment: function () { this.score++ }
}
// and now, the subclassing:
function paidUserCreator(paidName, paidScore, accountBalance) {
const newPaidUser = userCreator(paidName, paidScore)
// need to repoint the __proto__
Object.setPrototypeOf(newPaidUser, paidUserFunctions)
}
const paidUserFunctions = {
increaseBalance : function () {
this.accountBalance++
}
}
// first "extend" paidUserCreator
// with the .prototype functions of userCreator
// remember, this happens before paidUserCreator is run
Object.setPrototypeOf(paidUserCreator, userFunctions)
const paidUser1 = paidUserCreator('phil', 4, 12)
Few notes on this snippet
(Just repeating the previous points)
Here we do everything manually. userCreator
does not automatically create a new Object, nor assign to it anything automatically. Object.create
returns an empty object with its __proto__
pointing to its first argument. We add a few properties to it from the arguments/parameters (think function execution context), then return it.
Aside on the Object
function/object combo vs regular objects
Regular, simple objects, create with the object literal {foo:'bar'}
don't have the same properties as function/object combo.
The built in Object
has the properties of a function/object combo, e.g. a .prototype
property (which has for example .setPrototypeOf()
), and also direct methods (not on the .prototype
) eg. .create()
, keys
.
Object.setPrototypeOf
is a bit overloaded term (think .prototype
of function/object combos). It's a confusing name, because it actually changes the __proto__
pointer.
Aside on .call
and .apply
.
This will be necessary, when we want to call a function that expects to be called with the new
keyword, but we want to call it from inside our execution context, that already has a reference to this
.
Functions can be called or executed several ways with JavaScript. One is the putting a ()
after it's name. There are many other ways (think higher order functions), the ones interesting here are .call
and .apply
.
Their first argument is a refence to the this
variable, then either take a list of arguments (.call
) or one Array (.apply
).
This will help us when calling new baseConstructor()
, that it acts on the this
object inside our execution context (the subclass).
Subclasing with Constructor (Pseudoclassical)
This involves using the new
keyword, copying the base "class" .prototype
to the subclass, and .call
ing the base "constructor" to populate our subclass with the base "data".
function userCreator(name, score) {
this.name = name
this.score = score
}
userCreator.prototype.sayName = function() {
console.log("I'm", this.name)
}
userCreator.prototype.increment = function() {
this.score++
}
// now, the subclass creation
function paidUserCreator(paidName, paidScore, accountBalance) {
userCreator.call(this, paidName, paidScore) // see note#3
this.accountBalance = accountBalance
}
paidUserCreator.prototype = Object.create(userCreator.prototype) // see note#1 below
paidUserCreator.prototype.increaseBalance = function() { // see note#2
this.accountBalance++
}
const paidUser1 = new paidUserCreator('Phil', 3, 12)
Notes on the above pattern
Note #1 : We're putting the functions/properties from the userCreator
function/object combo (.prototype
) to our new subclass. To be more precise, we're creating an empty object on paidUserCreator.prototype
and setting its __proto__
pointer to userCreator.prototype
.
Note #2 : then, we're adding our extra functions on it
Note #3 : userCreator
expects to be called via the new
keyword, so it does not explicitly return anything, and expects that the this
in its execution context to be an auto-created, empty Object ({}
).
We get around this, with the .call
invocation. It passes in, the this
from its own execution context. Since it was called with the new
keyword, it's an auto-created, empty Object ({}
).
Subclassing with ES2015 Class syntax
The ES2015 Class syntax gives a simpler, cleaner patter, but behind the scenes it's still relying on the same mechanisms as the other methods.
To repeat:
class userCreator{
constructor (name, score) {
this.name = name
this.score = score
}
sayName () {
console.log("I'm", this.name)
}
increment () {
this.score++
}
}
The constructor
populates the empty this
object inside the execution context, and the class
adds sayName
and increment
to the object/function combo's .prototype
.
The subclassing
The extends
keyword
It sets the __proto__
of the .prototype
of the subclass to the .prototype
of the function/object that comes after (the extends
keyword).
It also sets the __proto__
of the newly created object to the "base class". The super
invocation will use this to find the base constructor to call.
So extends
sets two __proto__
links. First, that of the object created by the class
. This object also has a .prototype
property and extends
sets its __proto__
to the base object's .prototype
.
What's nice about this syntax, is that we don't need to mutate/manipulate externially the __proto__
pointer of subclass as we did in the first two subclassing solutions.
class paidUserCreator extends userCreator {
constructor(paidName, paidScore, accountBalance) {
super(paindName, paidScore) // note#1
this.accountBalance = accountBalance
}
increaseBalance() {
this.accountBalance++
}
}
const paidUser1 = new paidUserCreator('phil', 3, 24)
paidUser1.sayName()
Note #1 : how does the local execution context resolve super
? It's been linked by extends
on the __proto__
of the object created by paidUserCreator
.
Now, the super
will run userCreator
and thus populate the {}
(assigned to this
).
Note #2 : how is sayName()
resolved? Via paidUser1.prototype.__proto__
that points to userCreator.prototype
.
Aside on the this
object when using new
with class
In this scenario, the this
of the execution context is not automatically assigned an empty object ({}
), but remains uninitialised.
We must immediately run the super
to initialise it. This was a design decision from EcmaScript committee.
You can think of it as this = super(paidName, paidScore)
reflect.construct
"Behind the scenes", this what the super
call does.
It's a way the call a function without the new
keyword. It takes three arguments.
It's first argument is the funtion that will create the object, then the arguments Array, and finally the "subclass" (function) we want it to refer to (it's this
object populated). It also sets the __proto__
of the returned object, to the .prototype
of this last, third argument.
From the aside above, the point is that the super
call finally sets __proto__
to the .prototype
of the subclass (paidUserCreator
above).
This concludes the workshop on the hard parts of Object Oriented Programming in JavaScript.
The class
syntax does make working with objects look very clean, but we need to understand the inner workings to be able to effectively debug our programs. Also the prototypal nature of JavaScript is very different, so programmers coming from other languages such as Java or Python may be mislead by the simple syntax of class
, as behind the scenes the relations are very different.