Hard parts of Object Oriented programming in JavaScript

In notebook:
Work Notes
Created at:
2019-08-29
Updated:
2020-02-06
Tags:
JavaScript Fundamentals

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.

Will Sentance

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 functions 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

  1. It automatically returns a new object for us. See the introductinary example above, userCreator. Note, in the example below, that we don't return anything from UserCreator.

  2. The this keyword is bound to this object that is returned to us, inside the function that is called with the new keyword. This is how we can populate this object with properties (user:'phil', score:4) from above.

  3. The returned object has a link (via __proto__) to the .prototype property of the function used for the new 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):

  1. function called via new, see above
  2. function called as on the right side of . (user1.increment()), it points to the object to the left of the .
  3. explicitly binding it bind, apply, call
  4. 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 .calling 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.