Skip to content

Instantly share code, notes, and snippets.

@simonexmachina
Created May 8, 2014 23:17
Show Gist options
  • Star 20 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save simonexmachina/2148c6e3a06674fa187b to your computer and use it in GitHub Desktop.
Save simonexmachina/2148c6e3a06674fa187b to your computer and use it in GitHub Desktop.
JavaScript and Object Models

JavaScript and Object Models

A "choose your own adventure" story

JavaScript is has both object-oriented and functional heritage, thanks to its two parents: Scheme and Self.

It provides first class functions and makes it simple to compose these function objects into bundles of awesome. Even though I'm an OO "true believer" at heart, I find myself composing my code using functional concepts, and use the OO approach where there's a clear benefit or where I feel that it's the best way to communicate the interface.

Object-oriented software design is by no means the only way to do software design, but it's been an immensely successful model for a very long time now, and provides a clear and well-understood mental model for thinking and communicating about software. Lots of good ideas like encapsulation, delegation, traits and composition fit well into OO design.

In this post I'll examine how OO works in JavaScript, and present a variety of different approaches that can be built on its simple foundations.

Objects 101

There are two ways to create an object in JavaScript:

Using an object literal:

var foo = {};

Using the new keyword:

var constructor = function() {};
var bar = new constructor();

You can attach properties and methods to these objects, and JavaScript provides a very simple prototypal system that provides the a basic primitive for object-oriented software design. This provides two things:

  • Objects delegate property lookups to their prototype
  • A prototype can be shared between objects

There's no "types", no "inheritence" - just objects and prototypes.

An example

var Thing = function() {};
var a = new Thing();
// functions have a `prototype` which is used when `new` is
// called we can define properties on the prototype and they 
// become part of the object
console.log(a.foo); // => undefined
Thing.prototype.foo = 'bar';
console.log(a.foo); // => 'bar'

Inheritance

var Thing = function() {};
Thing.prototype.be = function() {
  return "I'm a thing";
};
Thing.prototype.do = function() {
  console.log("Done");
};
var Person = function() {};
Person.prototype = new Thing(); // Person extends Thing
Person.prototype.be = function() {
  return "I'm a person!";
};
var foo = new Person();
console.log(foo.be()); // => "I'm a person!"
foo.do(); // => "Done"
var HappyPerson = function() {};
HappyPerson.prototype = new Person(); // HappyPerson extends Person
var bar = new HappyPerson();
Thing.prototype.do = function() {
  console.log(this.be());
};

In fact, whenever you access a property on an object the VM performs a lookup on the object and its prototype chain and returns the first matching property it finds:

  1. looks for the property on the object
  2. then look at its prototype
  3. then the prototype's prototype
  4. and so on until it reaches the end of the prototype chain (at which point it returns undefined.)

This simple feature gives us awesome power:

  • Calls to the object can be delegated to the prototype
  • This delegation happens at call-time, so changes you make to the prototype also apply to instances that have already been created
  • We can model "classes" of objects and share functionality between them
  • It provides the basis for inheritance
  • We can use this to implement:
    • Classical, class-based OO
    • Object composition
    • Mixins
    • Other, wild and crazy object models

"What more could I possibly want?" I hear you ask. Well, and here's the rub - there's a whole variety of ways to harness this awesome, and that's the topic of today's post.

Some Different Approaches

  • POJOs - Plain Old JavaScript Objects
  • util.inherits
  • Classical
  • ES6
  • OOPOOP (tee hee)
  • ES7

POJOs - Plain Old JavaScript Objects

The most basic form is:

var Thing = {
  title: null,
  printTitle: function() {
    console.log(this.title);
  }
};
var thing = Object.create(Thing);
thing.title = 'one';
thing.printTitle(); // => 'one'

How about constructors? Okay, so there's two ways:

Using constructor functions

var Thing = function(title) {
  this.title = title;
};
Thing.prototype = function() {
  console.log(this.title);
};
var thing = new Thing('two');
thing.printTitle(); // => 'two'

Roll your own

var Thing = {
  init: function(title) {
    this.title = title;
    return this;
  },
  printTitle: function() {
    console.log(this.title);
  }
};
var thing = Object.create(Thing).init('three');
thing.printTitle(); // => 'three'

Prototypal Inheritance

We can achieve inheritance using prototype chaining:

var Model = {
  data: null,
  save: function() {
    db.save(this.data);
  }
};
var IndexedModel = {
  save: function() {
    Model.save.call(this); // call the superclass
    index.update(data);
  }
};
// create the subclass
IndexedModel.prototype = Object.create(Model);

However there's something I don't like here: the IndexedModel.save() method has to refer to its parent class Model by name (because the language doesn't provide a super keyword). That's where the "util.inherits" approach comes in...

util.inherits

This is a really simple technique that is used in the Node.js util module. It simply sets up the prototype chain and creates a _super property that points to the superclass:

var Model = function() {};
Model.prototype = {
  data: null,
  save: function() {
    db.save(this.data);
  }
};
var IndexedModel = function() {};
util.inherits(IndexedModel, Model);
IndexedModel.prototype.save = function() {
  this.constructor.super_.prototype.save.call(this); // call the superclass
  index.update(this.data);
};

Hmmn, so we've avoided hard-coding the superclass into IndexedModel but I hardly think that this.constructor.super_.prototype.save.call(this) is more elegant than Model.prototype.save.call().

So, onwards and upwards...

Classical OO

Classical OO is single-inheritance and provides a simple method to talk to the superclass.

var Class = ...; // an exercise for the reader :)
var Model = Class.extend({
  data: null,
  save: function() {
    db.save(this.data);
  }
});
var IndexedModel = Model.extend({
  save: function() {
    this._super(); // call the superclass
    index.update(data);
  }
});

However, to make this work we need to wrap every function in the prototype so that it set this._super every time the function is invoked.

While it does add overhead to provide the _super() method, I've never found this to be a problem, and being able to call _super() is definitely a win.

However, there's no need to restrict ourselves to single-inheritance and the static "classist" conceptions of Java here. This is JavaScript after all...

By manipulating objects and prototypes, JavaScript gives up great support for multiple inheritance (most often through mixins) and for object composition (where we augment the functionality of an object) using an "OOP implemented using OOP" approach which I affectionately refer to as...

OOPOOP

SmallTalk and Ruby are languages that implement their OO object model using OO (it's very meta), so each class is an object - an instance of Class - and the properties of these objects define the behaviour of objects of that class.

This approach can be reproduced in JavaScript using the prototype as its foundation. This is the approach taken in both Ember.js and uberproto.

var Class = Proto; // this example uses uberproto
var Person = Class.extend({
  init : function(name) {
    this.name = name;
  },
  fullName : function() {
    return this.name;
  }
});
var BetterPerson = Person.extend({
  init : function(name, lastname) {
    this._super(name);
    this.lastname = lastname;
  },
  fullName : function() {
    return this._super() + ' ' + this.lastname;
  }
});
var dave = BetterPerson.create('Dave', 'Doe');
console.log(dave.fullName()); // => 'Dave Doe'

Person.mixin({
  init : function() {
    this._super.apply(this, arguments);
  },
  sing : function() {
    return 'Laaaa';
  }
});
var dude = Person.create('Dude');
console.log(dude.sing()); // => 'Laaaa'

ES6

The next version of JavaScript (called ECMAScript 6 or "ES6") provides some syntactic sugar in the form of a class keyword, as well as a super keyword. At its core this is very similar to the implementation of classes in CoffeeScript, although ES6 also provides some extra awesome.

The following code blocks are equivalent:

// ES5
var Thing = function() {};
Thing.prototype.wildThing = function();

// ES6
class Thing {
  wildThing: function() {}
}

There's also quite a few other cool features of ES6 classes:

class Monster {
  // The keyword "constructor" followed by an argument list 
  // and a body defines the constructor function
  constructor(name, health) {
  // public and private declarations in the constructor
    // declare and initialize per-instance properties
    public name = name;
    private health = health;
  }
 
  // The following defines a method on the prototype
  attack(target) {
    log('The monster attacks ' + target);
  }
 
  // The keyword "get" defines a getter
  get isAlive() {
    return private(this).health > 0;
  }
 
  // Likewise, "set" can be used to define setters.
  set health(value) {
    if (value < 0) {
      throw new Error('Health must be non-negative.')
    }
    private(this).health = value
  }
 
  // After a "public" modifier,
  // an identifier optionally followed by "=" and an expression
  // declares a prototype property and initializes it to the value
  // of that expression. 
  public numAttacks = 0;
 
  // After a "public" modifier,
  // the keyword "const" followed by an identifier and an
  // initializer declares a constant prototype property.
  public const attackMessage = 'The monster hits you!';
}

Proxies

Proxies are another feature of ES6, that provide control over the behaviour of the object model in JavaScript. The Proxy API provides a bunch of "traps" that you can hook into to monkey with the object model. Most notable of these are get, set, delete, enumerate, iterate, and keys, and the API also provides has, hasOwn, defineProperty, getPropertyNames, getOwnPropertyNames, getPropertyDescriptor, getOwnPropertyDescriptor.

This provides us with the ability to perform the method_missing magic so beloved of Ruby developers (and god knows what else). Some obvious example uses for this are defining what happens when an undefined property is get, set or called, and performing validation when properties are set.

ES7 - Experimental

Brendan Eich is already working on the addition of "value objects" to JavaScript's object model for ES7 (the version after the (upcoming) ES6). This adds support for new primitive types such as int32, int64, bignum, decimal, and sets of 4 and 8 numbers for SIMD optimisations, and allow operator overloading (>, >=, +, -) and the ability to control boolean algebra (==, &&, ||).

ZOMG

Yes, complicated isn't it. JavaScript provides a very simple set of primitives, and over the last 20 years developers have found many wonderful ways to use these simple primitives to implement OO.

Of the options here, I personally prefer the OOPOOP approach when I need to do any serious modelling, although simple prototypal modelling often does the trick when there's only simple inheritance and polymorphism.

I'm really glad to see JavaScript providing some syntactic suger to support the most common patterns in ES6, and now that ES6 is becoming usable through transpilers (traceur-compiler and jstransform) I've started using ES6 classes. The OO features in ES6 have been the source of some controversy, but I must say that I think they achieve a nice balance of providing syntactic sugar for the most common case, without hiding too much of what's going on under the hood - it's all just objects and prototypes after all.

References

  1. http://raganwald.com/2014/03/10/writing-oop-using-oop.html
  2. https://github.com/daffl/uberproto
  3. https://gist.github.com/rahulkmr/9482010
  4. http://www.slideshare.net/BrendanEich/value-objects2
  5. https://gist.github.com/stefanpenner/384554
@Jeffrey04
Copy link

In https://gist.github.com/aexmachina/2148c6e3a06674fa187b#using-constructor-functions
Should it be

Thing.prototype.printTitle = function() {
  console.log(this.title);
};

instead of

Thing.prototype = function() {
  console.log(this.title);
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment