Read more

The JavaScript Object Model: A deep dive into prototypes and properties

Henning Koch
June 25, 2020Software engineer at makandra GmbH

Speaker today is Henning Koch, Head of Development at makandra Show archive.org snapshot .

Illustration online protection

Rails Long Term Support

Rails LTS provides security patches for old versions of Ruby on Rails (2.3, 3.2, 4.2 and 5.2)

  • Prevents you from data breaches and liability risks
  • Upgrade at your own pace
  • Works with modern Rubies
Read more Show archive.org snapshot

This talk will be in German with English slides.

Introduction

As web developers we work with JavaScript every day, even when our backend code uses another language. While we've become quite adept with JavaScript at a basic level, I think many of us lack a deep understanding of the JavaScript object model and its capabilities.

Some of the questions we will answer in this talk:

  • How does the new keyword construct an object?
  • What is the difference between { prototype }, { __proto__ } and Object.getPrototypeOf()?
  • With ES6 classes, how do I navigate from an instance to its class? How do I navigate from a class to its superclass?
  • How do ES6 classes translate to prototypes and vice versa?

We will spend the first 10 minutes to cover some JavaScript basics and then dive deep into details.

Level 1: Objects

In JavaScript we can create an object without a class:

let shape = {
  width: 30,
  height: 20,
  computeArea: function() { return this.width * this.height }
}


shape.width          // => 30
shape.height         // => 20
shape.height = 100

Note how "methods" are just properties with a function value:

shape.computeArea   // => function() { return this.width * this.height } 
shape.computeArea() // => 3000

Objects are like hashes

In many ways a JavaScript object is more like a Ruby Hash or a Java Map than an actual Object:

  • Objects can be created without a class
  • Accessing an undefined property will return undefined instead of crashing with NoMethodError (or similar)
  • You may add additional properties after construction
  • You may remove properties after construction

You may also access keys with a "hash index":

shape.width         // => 30
shape['width']      // => 30
shape['width'] = 5 
shape.width         // => 5

Property keys are strings

Property keys are always strings. Non-string keys are silently cast to strings:

let hash = {}
hash[1000] = 'foo' 
hash['1000'] = 'bar' 
hash[document.body] = 'baz'

hash // => { '1000': 'bar', '[object HTMLBodyElement]': 'baz' }

If you need a hash of non-string keys, use Map instead:

let map = new Map()

map.set(1000, 'foo')
map.set('1000', 'bar')
map.set(document.body, 'baz')

map.get(1000)           // => 'foo'
map.get('1000')         // => 'bar'
map.get(document.body)  // => 'baz'

Level 2: Functions

This is a basic function in JavaScript:

function foo() {
  return "this is foo"
}

foo() // => "this is foo"

Functions are also objects that can have properties:

function foo() {
  return "this is foo"
}

foo.key = "value"

foo()   // => "this is foo"
foo.key // => "value"

This might be confusing because typeof distinguishes between functions and objects:

let fn = function() {}
let obj = {}

typeof fn  // => "function"
typeof obj // => "object"

Due to practical considerations, utility libraries like Lodash Show archive.org snapshot or Unpoly Show archive.org snapshot 's up.util module Show archive.org snapshot consider functions to be objects:

up.util.isFunction(fn)  // => true
up.util.isObject(fn)    // => true

up.util.isFunction(obj) // => false
up.util.isObject(obj)   // => true

The value of this

Every function may use this, even if the function does not belong to the object. The default value of this is the global object (window in browsers):

function sayHello() {
  console.log("Hello, I am " + this)
}

sayHello() // logs "Hello, I am [object Window]"

When we assign that same function to an object and call it as a method, that object becomes the this value:

let object = new String("Alice")
object.sayHello = sayHello

object.sayHello() // logs "Hello, I am Alice"

Generally speaking: In a method invocation (receiver.method()), the value of this is the receiver ("left side of the dot").

When we pluck the function property from the object and call it without a receiver, we see that this has reverted to its default value:

let fn = object.sayHello
fn()              // logs "Hello, I am [object Window]"
object.sayHello() // logs "Hello, I am Alice"

As we can see the this value is decided anew for every invocation of that function. There is no hard connection between a function and object.

Function#call() und Function#apply()

Instead of calling function directly with fn() we can also use fn.call(context). With call() we can run the function with any value as the function's this context, even if both never belonged to the same object.

In the example below we will use the array's push() method on an object that isn't an array:

let object = {} 
object.push('foo') // => Uncaught TypeError: object.push() is not a function

Array.prototype.push.call(object, 'bar')
object // => {0: "bar", length: 1}
object.push('foo') // => Uncaught TypeError: object.push() is not a function

Instead of call() you may also use apply(). apply() is like call(), but it takes its arguments as an Array instead of varargs:

let object = {} 
Array.prototype.push.apply(object, ['bar'])
object // => {0: "bar", length: 1}

Level 3: Prototypes

Inheriting behaviors from other objects

When we have many similar objects we want to inherit the shared behavior from a shared place. We don't need classes to use inheritance in JavaScript. Instead we can use Object.create(parent) to inherit from an arbitrary object.

let parent = { parentKey: 1 }
let child = Object.create(parent)
child.childKey = 2

child.childKey   // => 2
child.parentKey  // => 1

This feature is very unique to JavaScript.

Note that when you inspect child in the browser console, you only see { childKey } property. The inherited properties are listed under __proto__ (Chrome) or <prototype> (Firefox).

Constructor functions

Every JavaScript function can be an object constructor for the new keyword:

function User(firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
}

let user = new User('Max', 'Muster') // => { firstName: 'Max', lastName: 'Muster' }

Just in case a function is used as a constructor, every function has a property { prototype }:

function User(firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
}

User.prototype                      // => { constructor: User }
User.prototype.constructor === User // => true

Note how the prototype object contains a { constructor } property pointing back at the function.

When we add properties to the prototype object, these properties are inherited down to constructed instances:

function User(firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
}
    
User.prototype.fullName = function() {
  return this.firstName + ' ' + this.lastName
}

User.prototype // => { constructor: User, fullName: function() { ... } }

let user = new User('Max', 'Muster') // => { firstName: 'Max', lastName: 'Muster', __proto__: User.prototype }
user.firstName                       // => "Max"
user.lastName                        // => "Muster"
user.fullName()                      // => "Max Muster"

Note how { firstName } and { lastName } are user's own properties, since they were defined directly on user. Only { fullName } is inherited.

Here is what the new keyword actually does when you type new Constructor():

  1. It creates a new object that inherits from Constructor.prototype
  2. It calls the Constructor() function, using the created object as the this context

That means we can create instances of a "class" without using the new keyword:

let user = Object.create(User.prototype) // => { __proto__: User.prototype }
User.call(user, 'Max', 'Muster')         // run the constructor with the new object as `this`
user                                     // => { __proto__: User.prototype, firstName: 'Max', lastName: 'Muster' }
user.firstName                           // => "Max"
user.lastName                            // => "Muster"
user.lastName()                          // => "Max Muster"

ES6 classes are a nicer way to define constructors and prototypes

When you have an ES6 transpiler or don't support IE11, we can use class keyword.

ES6 classes bring some syntactic sugar to define constructors and prototypes:

class User {
  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }
      
  fullName() {
    return this.firstName + ' ' + this.lastName
  }
}

Even when you don't use a transpiler, the browser's JavaScript VM converts the ES6 version to the prototype version when it parses your script.

JavaScript has no concept of "classes" at runtime. You cannot ask an object for its class, but you can ask it for its prototype.

A note on performance

Creating many objects that inherit from a prototype is very fast. The objects themselves are very lightweight. The common behavior is stored in a single prototype object that is shared by all instances.

Here is a performance comparison Show archive.org snapshot with the revealing module pattern, where all methods are re-created for every single object.

Native ES6 classes are not faster than their transpiled prototype form. Since the browser's JavaScript VM automatically convert the ES6 version to the prototype version, they're literally the same.

Object literals vs. Object.create()

All of us have used an object literal to create an object:

let x = {}

This is equivalent to the following:

let x = Object.create(Object.prototype)

From Object.prototype you will inherit some methods common to most objects, such as x.toString() or x.hasOwnProperty().

If you want to create an entirely empty object without any properties, you can use:

let x = Object.create(null)

Compare this to Object Show archive.org snapshot vs. BasicObject Show archive.org snapshot in Ruby:

BasicObject can be used for creating object hierarchies independent of Ruby’s object hierarchy, proxy objects like the Delegator class, or other uses where namespace pollution from Ruby’s methods and classes must be avoided.

prototype property vs. Object.getPrototypeOf()

The following is maybe the most confusing thing about JavaScript OOP: JavaScript has two very different concepts that are both called "prototype":

Only functions have a child prototype

Only functions have an automatic { prototype } property, just in case they are used as a constructor function. Properties in this prototype will be passed down to objects constructed with that function, which is why we will call it the child prototype.

The default child prototype returns an object with a constructor property pointing back to the function:

function foo() {}
foo.prototype // => { constructor: foo }

Non-function objects have no { prototype } property!

All objects have a parent prototype

Object.getPrototypeOf(object) returns the prototype from which an object inherits, which is why we will call it the parent prototype.

let object = new foo()
object.prototype              // => undefined, only functions have a child prototype!
Object.getPrototypeOf(object) // => foo.prototype, which is { constructor: foo }
object.constructor            // => function foo() {}

Only functions have both a parent prototype and child prototype

Functions are the only values that have both a parent prototype and a child prototype:

function fn() {}

fn.prototype              // returns the child prototype: { constructor: fn }
Object.getPrototypeOf(fn) // returns the parent prototype: Function.prototype

Function.prototype contains the instance methods of all functions, such as fn.call(), fn.name, fn.bind() or fn.length (arity).

Other ways to access the parent prototype

You might encounter other ways to access an object's parent prototype:

  1. Every object has a non-standard { __proto__ } property. This is the same object that is returned by Object.getPrototypeOf(o). Prefer Object.getPrototypeOf(o), since that's a JavaScript standard.
  2. There is Object.setPrototypeOf(o, newProto) to change an object's inheritance parent. Changing prototypes after object construction is slow in every JavaScript implementation. Avoid doing that at once your app has booted.
  3. Finally, there is Reflect.getPrototypeOf(o). For all practical cases this behaves like Object.getPrototypeOf(o).

Inheriting from another class

In ES6 we may use the extends keyword to inherit from another class:

class Student extends User {
  constructor(firstName, lastName, studentNumber) {
    super(firstName, lastName)
    this.studentNumber = studentNumber
  }
  
  isEnrolled() {
    return typeof this.studentNumber === 'number'
  }
}

The browser will translate this into code similiar to this:

function Student(firstName, lastName, studentNumber) {
  // Call superclass constructor
  User.call(this, firstName, lastName)
  this.studentNumber = studentNumber
}

// Have our own child prototype that inherits all properties form User's
// child prototype and may then be extended with Student-specific methods.
Student.prototype = Object.create(User.prototype)

Student.prototype.isEnrolled = function() {
  return typeof this.studentNumber === 'number'
}

Let's play with our new Student class:

let max = new Student('Max', 'Muster', 1234)
max.prototype                                     // => undefined, only functions have a child prototype!
max.constructor                                   // => Student

Object.getPrototypeOf(max)                        // => Student.prototype
Object.getPrototypeOf(Object.getPrototypeOf(max)) // => User.prototype

Student.prototype                                 // => { constructor: Student, isEnrolled: function() { ... } }
Student.prototype.prototype                       // => undefined, only functions have a child prototype!
Object.getPrototypeOf(Student.prototype)          // => User.prototype

Overriding methods and calling the super class

When we override an inherited method, ES6 let's us use super to access the implementation of our parent prototype:

class Parent {

  foo() {
    console.log("this is parent")
  }

}

class Child extends Foo {

  foo() {
    super.foo()
    console.log("this is child")
  }

}

let child = new Child()
child.foo() // logs "this is parent", then log "this is child"

How would we access a superclass method as a child class in a hand-rolled protoype implementation?

Remember that in JavaScript we can call any function with any object as this. Hence we can call Parent.prototype.foo on our Child instance:

function Parent() {}
Parent.prototype.foo = function() {
  console.log("this is parent")
}

function Child() {}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.foo = function() {
  Parent.prototype.foo.call(this)
  console.log("this is child")
}

Class methods ("static methods")

Remember that functions are objects with properties?

Class methods are properties of the constructor function:

function Constructor() { ... }
Constructor.prototype.instanceMethod = function() { ... }
Constructor.classMethod = function() { ... }

new Constructor().instanceMethod()
Constructor.classMethod()

Since instances inherit a { constructor } property from their parent prototype, instance methods may access class properties through this.constructor.classProperty:

Constructor.prototype.instanceMethod = function() {
  this.constructor.classMethod()
}

ES6 gives us the static keyword to define a class method with less code:

class Constructor {
  instanceMethod() { ... }
  static classMethod() { ... }
}

Again the JavaScript VM will silently convert a static method to a function property on the constructor functions, as JavaScript has no concept of classes at runtime.

In ES6, class methods are inherited

In an ES6 class, class methods are inherited from our superclass:

class Parent {
  static fromParent() {
    console.log('static method from parent')
  }
}

class Child extends Parent {
  static fromChild() {
    console.log('static method from child')
  }
}

Child.fromChild()  // logs "static method from child"
Child.fromParent() // logs "static method from parent"

That's seems surprising at first, because for that the functions Child and Parent would need to be in an inheritance chain. And in fact they are! Transpilers like Babel use Object.setPrototypeOf(Child, Parent) to achieve that:

function Parent() {}

Parent.fromParent = function() {
  console.log('static method from parent')
}

function Child() {}

Child.fromChild = function() {
  console.log('static method from child')
}

// Inherit instance properties from Parent.prototype
Child.prototype = Object.create(Parent.prototype)

// Inherit static properties from Parent.
// To do so we change Child's parent from Function.prototype to Parent:
Object.setPrototypeOf(Child, Parent)

Note how we now have two separate prototype chains:

  1. For class properties, Child inherits from Parent inherits from Function.prototype
  2. For instance properties, Child.prototype inherits from Parent.prototype inherits from Object.prototype

Image

Reveal

Note that Object.getPrototypeOf(Parent) is Function.prototype, not Object.

Exercise: Multiple inheritance

Ruby supports multiple inheritance through modules and include:

module Loggable
  def log(message)
    let prefix = "[" + self.class.name + "] "
    puts prefix + message
  end  
end

class Foo
  def foo
    puts "this is foo"
  end
end

class Bar < Foo
  include Loggable
  
  def bar
    puts "this is bar"
  end
end

bar = Bar.new
bar.foo               # prints "this is foo"
bar.bar               # prints "this is bar"
bar.log("debug info") # prints "[Bar] debug info" 

ES6 does not have a keyword like include. However, now that we know how classes work internally, we can build a mix() function that works similiarly:

Loggable = {
  log: function(message) {
    let prefix = "[" + this.constructor.name + "] "
    console.log(prefix + message)
  }
}

class Foo {
  foo() {
    console.log("this is foo")
  }
}

class Bar extends mix(Foo, Loggable) {
  bar() {
    console.log("this is bar")
  }
}

let bar = new Bar()
bar.foo()             // "this is foo"
bar.bar()             // "this is bar"
bar.log("debug info") // logs "[Bar] debug info"



function mix(parent, module) {
  // ???
}

Here is one way to implement mix():

function mix(parent, module) {
  let intermediate = function() {}
  
  // Inherit class methods
  Object.setPrototypeOf(intermediate, parent)
  
  // Inherit instance methods
  intermediate.prototype = Object.create(parent.prototype)
  
  // Copy module methods into our prototype
  Object.assign(intermediate.prototype, module)
  
  return intermediate
}

We can also leverage the class and extends keywords to automatically wire our two prototype chains:

function mix(parent, module) {
  class intermediate extends parent {}
  Object.assign(intermediate.prototype, module)
  return intermediate
}

Level 4: Properties

Earlier in this talk I mentioned that most JavaScript objects behave like a dumb Hash in Ruby and less like an actual Object.

However, that is not true for all JavaScript objects. A JavaScript property may have various modifiers that change how the property behaves. The set of modifiers that make up a property's behavior is called a property descriptor.

The following modifiers dictate how a stored property behaves:

Attribute Effect Default
value The property's current value. undefined
writable Whether the property may be assigned a new value. true
configurable Whether this property descriptor may be changed after creation. true
enumerable Whether this property appears in enumerations, like for ... in. true

When we define a property on an object, we get a default property descriptor:

let obj = { foo: 'bar' }
Object.getOwnPropertyDescriptor(obj, 'foo')
// => { value: 'bar', writable: true, configurable: true, enumerable: true }

To define a property with a non-default property descriptor, use Object.defineProperty(object, key, descriptor).

Let's use that to define a read-only property user.name:

let user = {}
Object.defineProperty(user, 'name', { value: 'Max', writable: false })

user.name          // => 'Max'
user.name = 'Anna' //
user.name          // => 'Max'

Defining computed properties

You may also define a property whose value is computed instead of stored. Such a property has the following modifiers (instead of value):

Attribute Effect
get A function that is called when the property is read ("getter")
set A function that is called when the property is written ("Setter")

Here is an example for a computed property { fullName }:

let user = { 
  firstName: 'Max',
  lastName: 'Muster'
}

Object.defineProperty(user, 'fullName', {
  get: function() { 
    return this.firstName + ' ' + this.lastName
  },
  set: function(value) {
    [this.firstName, this.lastName] = value.split(' ')
  }
})

user.fullName  // => 'Max Muster'

user.fullName = 'Berta Beispiel'
user.firstName // => 'Berta'
user.lastName  // => 'Beispiel'

To access a property's descriptor (instead of getting its value), use Object.getOwnPropertyDescriptor(object, key):

user.fullName  // => 'Max Muster'
Object.getOwnPropertyDescriptor(object, 'fullName') // => { get: function() { ... }, set: function() { ... } }

Enumerability

An enumerable property appears in enumerations, like for...in or Object.keys(o). Properties are enumerable by default. You can use Object.defineProperty() to make a property non-enumerable.

A good default is to make stored attributes enumerable, but to make methods non-enumerable. Think ActiveRecord's attributes hash vs. all methods of an ActiveRecord instance.

Let's take an object that is a mix of stored values and computed methods:

let user = { 
  firstName: 'Max',
  lastName: 'Muster',
  fullName: function() { return this.firstName + ' ' + this.lastName }
}

Copying user includes the fullName() method in the copy, which you might not want:

let copy = {}
Object.assign(copy, user)
// => { "firstName": "Max", "lastName": "Muster", fullName: function() { ... } }

If we make the fullName() method non-enumerable it will disappear from copies and loops:

Object.defineProperty(user, 'fullName', { enumerable: false })
let copy2 = {}
Object.assign(copy2, user)
// => { "firstName": "Max", "lastName": "Muster" }

Method properties defined in an ES6 class are not enumerable by default.

There is no consistency how JS features honor property metadata

Every JavaScript feature has its own rules whether it respects inheritance or enumerability.

For example, Object.keys() lists only the own enumerable properties of an object, but ignores inherited properties:

let parent = { parentKey: 1 }
let child = Object.create(parent)
child.childKey = 2

Object.keys(child) // => ['childKey']

On the other hand, for...in also iterates over inherited properties:

for (let key in child) {
  console.log(key) // prints 'childKey', then 'parentKey'
}

There is an epic table on MDN Show archive.org snapshot that lists all the property-related functions and how they behave.
You can print it out as a poster and pin it next to your JavaScript equality matrix:

Thank you!

Any questions?

You can reach me at:

These slides will be made available under https://makandracards.com/makandra/481040

Don't miss future Full Stack Hour events by joining our Meetup group:
https://www.meetup.com/de-DE/Full-Stack-Hour-by-makandra/ Show archive.org snapshot

Henning Koch
June 25, 2020Software engineer at makandra GmbH
Posted by Henning Koch to makandra dev (2020-06-25 10:23)