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'
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.
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
{}
(ornew Object
), which will create a plain object that gets its behaviour fromObject.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].
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~!
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.
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
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 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
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.