Object Prototypes and this
- Software design approaches and patterns, to identify reusable solutions to commonly occurring problems
- Apply an appropriate software development approach according to the relevant paradigm (for example object-oriented, event-driven or procedural)
You have probably heard of “object-oriented” programming languages. Javascript allows you to apply an object-oriented paradigm in your development, but it behaves differently from pure object-oriented languages. This topic explains the principles underlying Javascript’s object model.
By the end of this topic, you should understand object prototypes, [[Prototype]]
links, and [[Prototype]]
chains; and exactly what this means in any particular context.
Object prototypes
Objects in JavaScript have an internal property usually written as [[Prototype]]
. These [[Prototype]]
properties are not accessible directly; instead, they are references to another object. Almost all objects are given a non-null
value for this property when they’re created. So what’s [[Prototype]]
used for?
var foo = {
fizz: 42,
};
var bar = Object.create(foo);
console.log(bar.fizz); // 42
What Object.create
does will become clear shortly. For now, you can treat it as creating an empty object that has a [[Prototype]]
link to the object passed to it — in this case, foo
.
So what’s a [[Prototype]]
link?
You’ll notice that bar.fizz
exists, which isn’t something that normally happens with empty objects, and in fact is 42
. What’s happening is that when fizz
isn’t found on bar
, the linked object is checked for fizz
, at which point foo.fizz
is found.
In general, when an object property is accessed, the object itself is checked for the property, and then the linked object, and the linked object’s linked object, and so forth, up the [[Prototype]]
chain, until there isn’t a linked object. If it’s still not found, only then is undefined returned as the property’s value.
So where is the “end” of the [[Prototype]]
chain? At the top of every (normal) chain is the object Object.prototype
, which has various utility functions, like valueOf
and toString
— this explains why objects can be coerced since they are all by default linked to Object.prototype
, which has these methods.
Now look at the following example:
var foo = {
fizz: 42,
};
var bar = Object.create(foo);
console.log(bar.fizz); // 42
foo.fizz = 11;
console.log(bar.fizz); // 11
Hopefully, this result shouldn’t surprise you! When we access bar.fizz
for the second time, bar
hasn’t changed at all, so it as usual goes up the [[Prototype]]
chain, comes to foo, and discovers foo.fizz
exists, and has value 11
. What this illustrates is the concept of changing object [[Prototype]]
s after creating the link.
This gives you the power to do clever things — for example, it’s a common way to polyfill features that are missing from an engine on which you need the JavaScript to run, in other words, to implement those missing features. For example, Arrays
have a method map that exists in ES5 onwards; if you need to support an engine that doesn’t support ES5 and want to use map
, you’ll need to polyfill the functionality. The next step is to realise that all Arrays
in JavaScript are automatically [[Prototype]]
linked to Array.prototype
, which means if you add the map method to Array.prototype
, all your Arrays
will gain the map feature!
if (!Array.prototype.map) {
Array.prototype.map = function (callback, thisArg) {
/* ... Implementation here ... */
};
}
However, this power also allows you to change data in unexpected ways after it’s been declared and assigned. In our example with foo
and bar
, if you were reading the code and needed to debug a problem, it’s not immediately obvious that assigning foo.fizz
would also change the value we get back when accessing bar.fizz
. This makes it harder both to understand written code and to find and fix bugs. Use this power responsibly and with the care it deserves.
Shadowed Properties
Consider the following example:
var foo = {
fizz: 42,
};
var bar = Object.create(foo);
console.log(bar.fizz); // 42
bar.fizz = 11;
console.log(bar.fizz); // 11
console.log(foo.fizz); // 42
If bar
already has a property fizz
when we try bar.fizz = 11
, then the existing property is changed, just like you’re used to.
If bar
doesn’t have the property fizz
, then the [[Prototype]]
chain is traversed. If none of the linked objects have fizz
, then it’s created on bar
and assigned the value 11
, also just like you’re used to.
What happens when bar
doesn’t have fizz
but an object in the [[Prototype]]
chain does? In this case, a new property is added to bar
, resulting in a shadowed property. Sound familiar? A shadowed variable is created if a variable is re-declared, and in that scope, the shadowed variable is accessed rather than the variable in the parent scope. For our shadowed properties, the analogy holds. When checking the [[Prototype]]
chain, the shadowed property is found first and accessed first. (There are a couple of exceptions where shadowed properties aren’t created, to do with read-only properties and setters, but they’re not important for us right now.)
As a final note, know that shadowing can occur implicitly as well. It can be very subtle and hard to spot.
var foo = {
fizz: 42,
};
var bar = Object.create(foo);
bar.fizz++;
console.log(foo.fizz); // 42
console.log(bar.fizz); // 43
At first glance, you could easily think that what bar.fizz++
does is look up fizz
, find it on foo
, and then increment that. However, the ++
operation is actually shorthand for bar.fizz = bar.fizz + 1
, which accesses the current value of fizz
, 42
, adds 1
to it, and then assigns it to bar.fizz
, and since foo.fizz
already exists, a new shadowed property fizz
is created on bar
. Remember, if you wanted to modify foo.fizz
, you should have modified foo.fizz
and not something else.
this
this is a common word in English – if it appears like this
, we’re talking about the keyword. In other cases, like “this” or this or this, we’re just speaking plain English.
If you’ve ever done any programming in an object-oriented language, you might think that this
refers to the current instance of a class, which might translate to JavaScript as referring to the method’s parent object. Alternatively, you could intuit that this
refers to the currently executing function or the current scope. Sadly, this
is a complicated beast that often causes confusion if you don’t have a clear understanding — the answer is d) None of the above! It is context-sensitive.
The first thing to realise about this
is that what it does is dynamic rather than lexical, or alternatively that it’s a runtime binding rather than an author-time binding. It is contextual based on the conditions of the function’s invocation: this
has nothing to do with the function where it’s used, but instead has everything to do with the manner in which the function is called. This is where we look at the call-site: the location from which a function is called — we have to look at this exact spot to find what this
is a reference to.
fizz();
function fizz() {
// Call-site is "fizz()" in the global scope
foo.bar();
}
var foo = {
bar: function bar() {
// Call-site is "foo.bar()" in fizz()
buzz();
},
};
function buzz() {
// Call-site is "buzz()" in bar()
}
So we know where the call-site is, and what’s at the call-site. How does this help us with what this
is? There are four rules which govern this
.
1. Default Binding
This first rule is the most common case for function calls when they’re standalone function invocations. It’s the “default” in that you can think of it as the rule which applies when none of the other rules apply.
function think() {
console.log(this.expectedAnswer);
}
var expectedAnswer = "42";
think(); // 42, but only in browsers
The call-site is the line think();
, which leads to this
referring to the global object. The global object depends on the platform on which the JavaScript is run. For instance, on browsers, it’s the window
object, and additionally, variables declared with var
in the global scope are also on the global
object; whereas in Node, for example, it’s the global object, and variables in the global scope aren’t on the global object. Additionally, there are differences in behaviour between when this
is used while in the global context and when it’s the call-site that’s in the global context. You’ll need to research what the global object is for whatever engine you’re developing for.
Additionally, check whether you’re in strict
mode (usually signified by "use strict"
; appearing in your code somewhere). If you’re in strict
mode, default binding is just undefined
.
The takeaway from this is that what you get back from this
when default bound depends on your engine and the state of strict
mode, which can make it more trouble than it’s worth. You’ll find you rarely have much need for using this
in this way: however, this rule is a common source of bugs with this
– it’s important to know about this rule so you can tell when it’s happening.
2. Implicit Binding
This next rule is for when the call-site has a context object. Intuitively, this is when you’re calling an object’s method.
function think() {
console.log(this.expectedAnswer);
}
var deepThought = {
expectedAnswer: 42,
think: think,
};
deepThought.think(); // 42
The call-site deepThought.think()
uses deepThought
as the context object, and this rule says that it’s that context object which is used for this
. You’ll also notice that the function think()
is declared separately and then added by reference to deepThought
. Whether or not the function is initially declared on deepThought
or added as a reference, the call-site uses the deepThought
context to reference the function.
Loss of Implicit Binding
This binding might seem familiar to those who have experience in other languages, as this
looks like it refers to the object the method “owned by”. However, remember that functions aren’t really “owned” by objects, they’re just referred to, just like in the example. Sticking to that (incorrect) way of thinking can lead to one of the most common frustrations with this
, which is when an implicitly bound function loses the binding, which usually means it ends up going back to the default binding.
function think() {
console.log(this.expectedAnswer);
}
var deepThought = {
expectedAnswer: 42,
think: think,
};
var logAnswer = deepThought.think;
logAnswer(); // undefined
Although logAnswer
looks like it’s a reference to deepThought.think
, it’s actually a reference to the function think, which deepThought.think
was referring to.
One of the most common examples of this happening is when callbacks are involved. Things like:
setTimeout(deepThought.think, 3000); // undefined (in three seconds' time)
If you think about how setTimeout
(and other callbacks) work, you can imagine them a bit like this:
function setTimeout(callback, delay) {
/* Do some waiting */
callback();
}
The call-site doesn’t have any context objects, it’s just using the default binding.
If you wanted to have this
refer to deepThought
, this could make it very awkward if you ever need to pass think
around as a value. Thankfully, there are ways to force this
to refer to specific objects.
3. Explicit Binding
Functions in JavaScript have on their [[Prototype]]
some utilities that help with our predicament.
call
and apply
Both of these methods take as their first parameter an object to use for this
, and then invoke the function with it.
function think() {
console.log(this.expectedAnswer);
}
var deepThought = {
expectedAnswer: 42,
};
think.call(deepThought); // 42
They do have slight differences, but that’s the important bit for us. Check out their documentation: call and apply.
However, this doesn’t help us with the problem of passing methods while keeping this
referring to the original object.
bind
This method, again, takes this
as the first parameter, but what it does is return a new function with this
referring to the object.
function think() {
console.log(this.expectedAnswer);
}
var deepThought = {
expectedAnswer: 42,
think: think,
};
setTimeout(deepThought.think.bind(deepThought), 3000); // 42 (in three seconds' time)
Like call
and apply
, bind
does a little more than that; you can find the docs here) You’ll also notice that the call-site deepThought.think.bind(deepThought)
looks like it’s using both implicit binding and explicit binding. We’ll get to how the rules interact with each other shortly.
this
or that
Another common situation is when you have (possibly nested) callback functions:
var sherlock = {
name: "Sherlock Holmes",
inspect: function () {
setTimeout(function () {
console.log("Deducing...");
setTimeout(function () {
console.log(this.name); // What is `this`?
}, 1000);
}, 1000);
},
};
sherlock.inspect(); // Deducing... undefined
You should be able to deduce yourself why this
is bound incorrectly inside setTimeout
.
Because there are two functions introduced, fixing the problem requires two bind
calls:
var sherlock = {
name: "Sherlock Holmes",
inspect: function () {
setTimeout(
function () {
console.log("Deducing...");
setTimeout(
function () {
console.log(this.name);
}.bind(this),
1000
);
}.bind(this),
1000
);
},
};
sherlock.inspect(); // Deducing... Sherlock Holmes
Using bind
repeatedly can start to look a little ugly, so a common alternative pattern is to explicitly assign this
to a new variable (often that
or self
).
var sherlock = {
name: "Sherlock Holmes",
inspect: function () {
var that = this;
setTimeout(function () {
console.log("Deducing...");
setTimeout(function () {
console.log(that.name);
}, 1000);
}, 1000);
},
};
sherlock.inspect(); // Deducing... Sherlock Holmes
(This is rarely required with ES6 arrow functions, as you’ll see in a later topic).
4. new
Binding
Again, a common source of misconceptions for those with experience in other languages. The syntax for the use of new
is basically identical to other languages but with some differences. In other languages, you might use new to instantiate a new instance of a class by calling its constructor; but JavaScript doesn’t have classes (or at least not in the same sense. More details in the ES6 section coming up).
Let’s first make sense of constructors in JavaScript — there is no such thing as a constructor function in JavaScript. You can call any function and use a new
in front of it, which makes that function call a constructor call. It’s a subtle distinction to be sure, but it’s important to remember.
So what does new
do? When you invoke a function with new in front of it, this is what happens:
- A new (empty) object is created.
- The new object is
[[Prototype]]
linked to the function’s prototype property. - The function is called with
this
referring to the new object. - Unless the function returns its own alternate
object
, it will then return the newly constructed object.
Let’s put this all together and do something interesting with it:
function Planet(name, answer) {
this.name = name;
this.answer = answer;
}
Planet.prototype.logAnswer = function () {
console.log(this.answer);
};
var earth = new Planet("Earth", "42");
var magrathea = new Planet(
"Magrathea",
"This is a recorded announcement as I'm afraid we're all out at the moment."
);
earth.logAnswer(); // 42
magrathea.logAnswer(); // This is a recorded announcement as I'm afraid we're all out at the moment.
Putting the rules together
After the above, it’s relatively straightforward how to deduce what this
is when more than one rule applies. Look at the call-site and apply them in order:
- Is the function called with a new binding? If so,
this
is the newly constructed object. - Is the function called with an explicit binding? This can be either a
call
orapply
at the call-site, or a hiddenbind
at declaration. If so,this
is the specified object. - Is the function called with an implicit binding? If so,
this
is the context object. - If none of the above, the function was called with the default binding. If in
strict mode
,this
isundefined
, otherwise it’s the global object.
(Remember, this
in the global scope — not in a function at all — can work differently than when it is in a function. Check your engine’s documentation!)
ES6 continued
“Classes”
Go back over that last example with Planet
. If you’ve used classes either in JavaScript or in other languages, you’ll notice that var earth = new Planet(...)
looks very much like an object instantiation, and the bit before defining Planet
looks like a constructor and a method. Now look at the following code, written using ES6 classes:
class Planet {
constructor(name, answer) {
this.name = name;
this.answer = answer;
}
logAnswer() {
console.log(this.answer);
}
}
var earth = new Planet("Earth", "42");
var magrathea = new Planet(
"Magrathea",
"This is a recorded announcement as I'm afraid we're all out at the moment."
);
earth.logAnswer(); // 42
magrathea.logAnswer(); // This is a recorded announcement as I'm afraid we're all out at the moment.
The keyword class
is what is called syntactic sugar for creating a function intended for use with new
, and then adding methods to the function’s prototype
property — in other words, the implementation is still the same, while its appearance is neater and easier to read.
The syntactic sugar can cause subtle problems down the line, though, due to it not actually implementing classes, but being a cover over [[Prototype]]
linking.
class Planet {
constructor(name, answer) {
this.name = name;
this.answer = answer;
}
logAnswer() {
console.log(this.answer);
}
}
const earth = new Planet("Earth", "42");
earth.logAnswer(); // 42
Planet.prototype.logAnswer = function () {
console.log("EXTERMINATE");
};
earth.logAnswer(); // EXTERMINATE
The use of class
has the implication that the class’s properties and methods are copied onto a completely separate object. However, in JavaScript, you can change or replace methods in these “classes” after declaration, and all instances of this “class” that were previously instantiated will still be affected. Now you know about how it actually works under the hood, you know this behaviour makes sense (you’re updating the object to which the instances are linked), but the fact of the matter is that it’s surprising that a class can be changed later and affect all its instances.
You should write your code with deliberation and care, taking into account the benefits and drawbacks of your options. class
has its place — it makes your code easier to read, and makes the mental model with the hierarchy of objects in your code easier to comprehend — just make sure you know how to use it well.
Object oriented design patterns
Design patterns are common ways of solving problems in software development. Each pattern is a structure that can be followed in your own code, that you implement for your specific situation. Following established design patterns is advantageous because:
- in general they have evolved as best practice, and so avoid the need for you to solve every problem anew – although “best practice” changes over time so some established patterns fall out of favour; and
- other developers will probably be familiar with the patterns so it will be easier for them to understand and maintain your code.
Although design patterns aren’t specifically tied to object oriented programming, most of the common design patterns that have arisen fit the OOP paradigm. The following are some common OOP design patterns.
-
Singleton: This pattern ensures that there is only ever one instance of a class. This could be important if your application needs a single shared configuration object or an object manages access to an external resource. This would be implemented by:
- Having a static property in the class that can hold the singleton
- Making the class constructor private so that new objects of that class cannot be created
- Having a static public method that is used to access the singleton; if the shared object doesn’t exist yet then it is created
-
Factory: This is a pattern for creating objects that share an interface, without the caller needing to know about the various classes that implement that interface. For example, a factory pattern for creating objects that have the interface
Product
could look like:- Two classes that implement
Product
arePen
andBook
- The interface
ProductFactory
has the methodcreateProduct()
, which creates and returnsProduct
objects - The class
PenFactory
implementsProductFactory
, and itscreateProduct()
method creates and returns a newPen
- The class
BookFactory
implementsProductFactory
, and itscreateProduct()
method creates and returns a newBook
- If code elsewhere in the application is given a
ProductFactory
for creating products, it doesn’t need to know what the products are or be told when new products are created
- Two classes that implement
-
Model-View-Controller: You’ll be familiar with the MVC pattern from the Bootcamp Bookish exercise; it’s a very common way to structure user interfaces.
- Model objects contains the data that is to be displayed (e.g., an entity that has been fetched from a database)
- The View is a representation of the Model to the user (such as a table or graphical representation)
- The Controller processes commands from the user and tells the Model objects to change appropriately
-
Adapter: This pattern would be suitable for the Bootcamp SupportBank exercise, in which you had to process data from files in different formats (CSV, JSON and XML). An adapter is a class that allows incompatible interfaces to interact. In the case of SupportBank, you might decide that:
TransactionReader
is an interface that has the methodloadTransactions()
, which returns an array ofTransaction
objects- There are three implementations of
TransactionReader
, each of which knows how to parse its particular file type, convert values where necessary and produceTransaction
objects - Note that you might use a Factory pattern to create the
TransactionReader
s, so it’s easy to add support for new file types in future
Strengths of OOP
The following are strengths of the object oriented programmming paradigm.
Abstraction refers to the fact that object oriented code hides information that you don’t need to know, so if you’re using an object all you know about it is its publicly visible characteristics. Your code doesn’t know about how the object’s data and methods are implemented. This is especially clear when dealing with interfaces – code that interacts with an interface knows only the contract that the interface publishes, and that interface might be implemented by lots of different classes that have completely different structures and behaviour.
Encapsulation refers to the fact that the data and behaviour of an object are tied together into a single entity. If you think about programming without encapsulation, if you have a simple data structure with a set of fields in it and then want to run a function on it, you need to make sure that you find the function that correctly handles that specific data structure. With encapsulation, you just call the object’s own method.
Polymorphism refers to the fact that an object can be interacted with as if it were an instance of an ancestor class, while its behaviour will come from its actual class. When a new descendent class is defined with its own implementation of ancestor class methods, code that interacts with the base class will automatically call the new implementations without having to be updated.