We have updated the content of our program. To access the current Software Engineering curriculum visit curriculum.turing.edu.
An Introduction to Object-Oriented JavaScript
Learning Goals
By the end of this lesson you should…
- be able to apply OO patterns to JavaScript functions (mastery)
- explain the difference between OOP and JavaScript’s Prototypal Inheritance (functional)
- better understand and make use of
this
in multiple contexts (functional)
Warm Up
- What are the main components of Object Oriented Programming?
- What do you like/dislike about OOP?
- What’s one way to create a new object in JavaScript?
- What’s your experience in making your JavaScript object-oriented thus far?
Vocabulary
- Object Oriented Programming
- Classical Inheritance
- constructor functions
- prototype
- Prototypal Inheritance
this
Introduction
JavaScript can behave as an object-oriented programming language, but it follows a slightly different approach than what we know from Ruby. Instead of creating classes, constructor functions can be used to construct new objects in JavaScript.
It’s not a rule baked into the language, but — by convention — most JavaScript developers capitalize the names of functions that they intend on using as object constructors.
Constructors
A constructor function or object constructor can be thought of as a blueprint (similar to classes), or—better yet—as a casting mold from which new objects are minted. The constructor function includes basic information about the properties of an object and uses a special syntax that allows us to build new objects quickly using the template defined by the constructor.
Object constructors can be called using the new
keyword.
function Dog() {}
var sodie = new Dog();
sodie
in the example above will be a new object — albeit, a very simple one.
Let’s add to our Dog()
constructor.
function Dog(name) {
this.name = name;
this.legs = 4;
}
var sodie = new Dog('Sodie');
var oscar = new Dog('Oscar');
sodie.name; // 'Sodie'
sodie.legs; // 4
oscar.name; // 'Oscar'
Functions and this
revisited
You may have noticed that we’re using this
in our function and that it isn’t bound to the global object, like it ought to be.
Dog
is just a regular function. But, we call it a little differently than we did in previous section on functions. If you recall, there are a few ways we can call a function:
- Using a pair of parenthesis as the end of the functions name (e.g.
someFunction()
). - Using the
call()
method (e.g.someFunction.call()
). - Using the
apply()
method (e.g.someFunction.apply()
).
When we are writing object-oriented JavaScript, we have a fourth way of invoking a function: the new
keyword. The new
keyword invokes the function as a constructor, which causes it to behave in a fundamentally different way.
When we use the new
keyword to call our function as a constructor, a few things happen under the scenes:
this
is set to a new empty object- The prototype property of the constructor function (
Dog.prototype
in the example above) is set as the prototype of the new object, which was set tothis
in the first step - the body of our function is run
- our new object,
this
, is returned from the constructor
The prototype
Property
Let’s take a look at this
in the context of our Dog()
constructor:
function Dog(name) {
// `this` is set to a new object
// the prototype is set to `Dog.prototype`
this.name = name;
this.legs = 4;
// return this;
}
var sodie = new Dog("Sodie");
What is Dog.prototype
and where does it come from? Functions are objects and all functions in JavaScript have a prototype
property.
This property is set to an empty object — {}
— by default.
function Dog() {}
function Cat() {}
Dog.prototype; // {}
Cat.prototype; // {}
With regular functions, we generally don’t use the prototype
property — it’s like an appendix. But, this special little object comes in to play when we use the function as a constructor.
You may have heard that JavaScript has something called prototypal inheritance. This is a very complicated term for a relatively simple concept.
Classical Inheritance v. JS Prototypal Inheritance Whiteboard Activity
When we call a property on an object (e.g. sodie.name
), JavaScript checks the object to see if it has a name
property. If it does, then it hands us that property. If not, then it checks the object’s prototype. If the object’s prototype doesn’t have that property, then it check’s the prototype’s prototype, and so on. It continues this process until it reaches the top of the chain. If it still hasn’t defined this property, then it returns undefined
.
By default, all objects inherit from Object
, which has a few methods on it. One of these methods is toString()
.
function Dog(name) {
this.name = name;
this.legs = 4;
}
var sodie = new Dog('Sodie');
sodie.legs; // 4
sodie.toString(); // [object Object]
When we call sodie.legs
in the example above, JavaScript checks sodie
to see if it has a legs
property. It does, so JavaScript returns the value, 4
. In the next line, we call toString()
.
Well, sodie
doesn’t have a toString
property, so we check sodie
’s prototype, which is Dog.prototype
. That’s an empty object, so it certainly doesn’t have that property.
Eventually, we work our way up to Object.prototype
, which has a to toString
property set to a built-in function. JavaScript calls the toString()
method that it found up the chain, which returns [object Object]
.
We could, however, set our own toString()
method that would return something a little more helpful.
function Dog(name) {
this.name = name;
}
var sodie = new Dog('Fido');
sodie.toString = function() {
return '[Dog: ' + this.name + ']';
};
sodie.toString(); // [Dog: Fido]
JavaScript finds the toString
property immediately and doesn’t have to look up the chain of prototypes. But, only sodie
has this fancy new toString
property. It would be nice if all dogs could share this new functionality.
Side note: our custom
toString()
function is an example of the Template Method Pattern in practice
Each dog constructed by the Dog()
constructor has Dog.prototype
set as its prototype. This means that each dog looks immediately to Dog.prototype
if we ask for a property that it doesn’t have.
Consider the following:
function Dog(name) {
this.name = name;
}
Dog.prototype.toString = function () {
return '[Dog: ' + this.name + ']';
};
var sodie = new Dog('Sodie');
var oscar = new Dog('Oscar');
sodie.toString(); // [Dog: Sodie]
oscar.toString(); // [Dog: Oscar]
Prototypes are a great way to share functionality between related objects. We can define any properties we want on Dog.prototype
.
function Dog(name) {
this.name = name;
}
Dog.prototype.sayHello = function () {
return 'Hello, my name is ' + this.name + '.'
};
var sodie = new Dog('Sodie');
var oscar = new Dog('Oscar');
sodie.sayHello(); // Hello, my name is Sodie.
oscar.sayHello(); // Hello, my name is Oscar.
ES6 class
Syntax
The ES6 class
keyword has gotten a lot of excitement from newer JS devs, but there are a lot of critics out there. Some things to know:
- syntax looks a lot more like a class system; it is still bound by the prototype system
- classes pretend that copying is going on - it’s not.
- use the
extends
keyword to inherit super
keyword allows you to go up prototype chain (relative polymorphism)static
keyword (the only thing that’s not just syntactic sugar) - adds to class, not just instance.
Here is the same implementation in ES6 syntax:
class Dog {
constructor(name) {
this.name = name
}
sayHello() {
return `Hello, my name is ${this.name}.`
}
}
const fido = new Dog('Fido')
const spot = new Dog('Spot')
fido.sayHello() // Hello, my name is Fido.
spot.sayHello() // Hello, my name is Spot.
Don’t let the class
keyword fool you too much. It still compiles down to a Dog.prototype
object, it’s just wrapped in a container so the syntax is more familiar to other OO languages.
A couple of it’s limitations:
- An object cannot be extended
- Classes and Objects cannot be mixed
- all classes or no classes
- Classes cannot just be called like functions, they can only be used with
new
keyword this
context can be changed with.call
or.apply
, but super is static. (This was done for performance, but really messes people up)
Pro tip: shortcuts in code, such as this
class
syntax is often referred to as syntactic sugar
A OO jQuery Example
You are now familiar with the classic $(document).ready(() => ... )
setup, which tells the browser to wait for the DOM to load before running your scripts. Perhaps you’ve also ended up with a big ol’ list of functions in and out of this ready
block, not really organized in any object-oriented way.
A potential solution for organizing your jQuery setup looks like this:
// some higher-level .js file
$(document).ready(() => {
new EventHandler()
})
// in a separate "eventHandler.js" file:
// ES5
function EventHandler() {
$("button").on("click", this.doSomething) // this is "listening" upon its construction
}
EventHandler.prototype.doSomething = function() {
console.log("Handled!")
}
// ES6
class EventHandler {
constructor() {
$("button").on("click", this.doSomething.bind(this))
}
doSomething() {
console.log("Handled!")
}
}
Your Turn: Mod 1 Final Returns!
Go to this repo and follow the instructions to get set up. Let’s take thirty minutes to implement (some of) the Mod 1 final using object-oriented JavaScript.
Prototypes vs. Classes
This is a topic that has been hashed out to great length on the internet. In some ways there are a lot of similarities between classes in a language like Ruby and prototypes in a language like JavaScript:
- Both allow child instances of their type to access their methods and behavior
- Both use a “chain” mechanism to continue searching for requested properties in their parent
- Both can be used (via constructor invocation) to “set up” new objects when they are created
However, there are also some major differences:
- Prototypes don’t really provide a mechanism for encapsulation of state, which is one of the major principles of most OO languages.
- JavaScript doesn’t provide an OO-style mechanism for “private” functions (although we can achieve something similar with closures).
- Prototypes don’t distinguish between their own methods and the methods provided to their children (i.e. class methods vs. instance methods).
- We don’t have relative polymorphism like we do in OOP - it’s always absolute to the bottom of the chain. You cannot override a function with the prototype system.
Closing
In your notebook, answer the following:
- What happens when the
new
keyword is used? - How would describe the differences between “OO” in JavaScript and OO in Ruby?
- What’s your definition for each of our vocab words for this lesson?
- Object Oriented Programming
- Classical Inheritance
- constructor functions (JS)
- prototype (JS)
- Prototypal Inheritance (JS)
this
(JS)