Skip to content

Instantly share code, notes, and snippets.

@robotlolita
Created June 20, 2011 00:15
Show Gist options
  • Save robotlolita/1034936 to your computer and use it in GitHub Desktop.
Save robotlolita/1034936 to your computer and use it in GitHub Desktop.
Attempt at explaining prototypical OO.

Objects

Objects are the most basic structure in JavaScript. Everything is an object: functions, arrays, numbers, strings, booleans, you-name-it. And yet we have no classes. That is, despite being an Object Oriented language, JavaScript does not use a separated layer of meta-data just to define how all these objects need to be constructed.

In fact, they are just collections of key/value pairs, that define their own behaviour. Thus, an object with properties foo = 1 and bar = 2 is simply:

:::javascript
var my_object = { foo: 1
                , bar: 2 }

The properties in the object can be later on retrieved by the dot notation, or bracket notation. In the latter case you can use arbitrary strings for the key (which does not need to be a valid identifier either), and even keys stored in variables:

:::javascript
>>> my_object.foo     // dot notation
1
>>> my_object['foo']  // bracket notation
1

Trying to access properties that don't exist, will simply return undefined, which can be quite handy:

:::javascript
>>> my_object.y_u_no
undefined

Setting properties is done by assigning any value to the property. If the key does not exist yet in the object, it will be created and the value will be stored for that property. At any rate, the property will now point to the given value:

:::javascript
>>> my_object.foo = 2
>>> my_object.foo
2

>>> my_object.y_u_no = 'be awesomer'
>>> my_object.y_u_no
'be awesomer'

Inheritance

As noted in the previous section, JavaScript has absolutely no classes. Instead of having a separate layer that defines how a group of object has to look and behave, JavaScript lets each object define its own behaviour directly.

On top of that, inheritance is a first-class citizen in JavaScript. That is, objects can inherit behaviours directly from other objects, at run time.

This opens up several new possibilities on how to organise your program. So rather than asking you to go through constricted ways to define common behaviours, you can let the program build naturally as it evolves.

Prototypes

Prototypical inheritance happens by cloning the behaviours of other objects, and extending that behaviour:

:::javascript
Person = { }
Person.speak = function(phrase) {
    return this.name + ': ' + phrase
}

Student = Object.create(Person)
Student.studies_at = function() {
    return this.speak('I study at ' + this.school)
}

In this case, Student is just a plain object that gets its behaviours from the Person object. Thus, if a Person can speak, so can a Student.

Cloning behaviours of other objects in JavaScript can be done in a few different ways:

  • Using the object literal notation {} (or new Object), which will create a plain object that gets its behaviour from Object.prototype.

  • Using the [Object.create][] method, added in ECMAScript 5, which allows you to tell the engine which object you want to inherit the behaviours from.

By using Object.create, you can pass in the object you want to clone as the first argument (you can pass null if you want a really empty Object). You can also pass a set of property descriptors to be added to the fresh object as the second argument[^1].

More on specialised behaviours

Going back to the example, it should be a little more clear how you'd go about creating specialised Persons (or Students) by now. For example, if you wanted to have a Person Bob, you would only need to clone Person and extend the object to add Bob's data:

:::javascript
>>> var bob  = Object.create(Person)
>>> bob.name = 'Bob'
>>> bob.speak('Hello, world!')
Bob: Hello, world!

Creating a new Student Alice would work similarly:

:::javascript
>>> var alice    = Object.create(Student)
>>> alice.name   = 'Alice'
>>> alice.school = 'MIT'
>>> alice.studies_at()
Alice: I study at MIT

You could go even further, obviously. By making a specialised Bob, for example. The possibilities of extensibility are quite endless:

:::javascript
>>> var future_bob = Object.create(bob)
>>> future_bob.name
'Bob'

>>> future_bob.hows_future = function() {
...    return this.speak("It's nice, man."
...                    + "We don't have "
...                    + "Java anymoar~!")
... }

>>> future_bob.hows_future()
Bob: It's nice, man. We don't have Java anymoar~!

Understanding [[Prototype]]

There's one interesting aspect about JavaScript's implementation of prototypical inheritance — property accesses are delegated. This means that when you ask a JavaScript engine to retrieve a property, it'll search through all the ancestors of the object until that property is found.

Furthermore, modifying these master objects after they've been cloned means that the descendants will get all those changes for free, programmer-work wise.

To do this, each object in JavaScript keeps a pointer to the master object, from which it inherits its behaviours. This pointer is an internal property called [[Prototype]], although some implementations do expose this (SpiderMonkey and v8 let you change or access the pointer with the __proto__ property of any object). It's usually not a good idea to mess up with it, as it defeats any optimisation the engine would be able to do for the prototype chain.

If we go back to our lil' Bob's example, we can see this concept in action. Imagine that, somehow, future_bob sent a message back to the past asking Bob to change his name to Real Bob. Sure we want that future_bob also get that change because... well, they're the same person:

:::javascript
>>> bob.name = 'Real Bob'

>>> future_bob.name
Real Bob

The property lookup basically goes on like this:

:::text
1. Does the object have the property?
     1.1. Return the value stored in the property;
2. Does [[Prototype]] point to a valid object?
     2.1. Set object to [[Prototype]];
     2.2. Go back to 1;
3. Return undefined.

Initialisation

Prototypes allow you to share behaviours, which you can later extend as you please. However, having the user manually conforming with what's expected by those behaviours is not always practical.

Freshly created objects may be initialised by hand, yes, but it's quite a given that such initialisation can also be made through a function. So, given all Person objects should have a name, we could write a simple function that sets such name for us:

:::javascript
Person.setup = function(name) {
    this.name = name
    return this
}
function make_person(name) {
    return Object.create(Person).setup(name)
}

>>> var joe = make_person('Joe')
>>> joe.speak('Hai, warudo')
Joe: Hai, warudo

Specialised initialisation functions can just call the generic setup functions passing the right context, by using either Function#call or Function#apply:

:::javascript
Student.setup = function(name, school) {
    Person.setup.call(this, name)
    this.school = school
    return this
}
function make_student(name, school) {
    return Object.create(Student).setup(name, school)
}

>>> var ben = make_student('Ben', 'MIT')
>>> ben.studies_at()
Ben: I study at MIT

Using constructors

The other way of constructing objects in JavaScript is to use Constructors. These are plain functions that, when invoked with the new operator, create a fresh object by cloning the object referenced by the prototype property of that function, and later aplying the function to the new object.

The existence of constructors and the new operator is actually a side-effect of designing a language to be close (on the surface) to Java — though it's clunky, it may suit some coding styles.

Back then, when Object.create didn't exist, this was the only way of achieving inheritance in JavaScript, and it has many, many flaws (not just talking about the it-must-look-like-java BBB[^2] here).

So, let's go back to our Person/Student example. One of the "advantages" of constructors, is that you can do initialisation and cloning with one single operation. This means that instead of having a make_person in addition to a Person.setup method, we could simple have a Person constructor:

:::javascript
function Person(name) {
    this.name = name
}
Person.prototype.speak = function(phrase) {
    return this.name + ': ' + phrase
}

Then you could go on about constructing objects by using the new operator with that function:

>>> var bob = new Person('Bob')
>>> bob.speak('Hello, world!')
Bob: Hello, world!

It should be noted, though, that while still prototypical, the constructor approach feels much more like classical OOP. You have a single "thing" that holds how its descendants should behave, and have it handle the construction of new objects. So the focus is much more on designing relationships than designing behaviours.

The Function's .prototype

The prototype property for a function is not entirely magical, but it has special meaning when (and only when) used with the new operator: it's used as the master object that'll have its behaviours cloned.

All functions come with prototype pointing to a fresh object, which has a constructor property pointing back to the constructor function.

Being a plain object, you should know by now how to port the old Student object to the constructor approach: just use Object.create!

function Student(name, school) {
    Person.call(this, name)
    this.school = school
}
Student.prototype = Object.create(Person.prototype)
Student.prototype.studies_at = function() {
    return 'I study at ' + this.school
}

>>> var alice = new Student('Alice', 'MIT')
>>> alice.studies_at()
Alice: I study at MIT

As you see, without the magical new operator, Person and Student are just plain functions, that you're already familiar with.

The new magic

The new operator can be used with a function to construct objects that clone the behaviours of other objects. It works mostly the same way as Object.create:

1. Create a fresh Object, say { }

2. Set the [[Prototype]] of the new object to the constructor's
   prototype property, so it inherits that behaviour.

3. Calls the constructor in the context of the new object (such that
   this inside the constructor will be the new object), and pass any
   parameters that have been passed in the function.
   
4. Return the new object.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment