ES6 - JavaScript Improved course lesson 2/4
Udacity Google Mobile Web Specialist Nanodegree program part 3 lesson 05
Udacity Grow with Google Scholarship challenge course lesson 07
Brendon Smith
- JavaScript Arrow Functions
- JavaScript "this"
- JavaScript Classes
- 2.12. Class Preview
- 2.13. JavaScript's Illusion of Classes
- 2.14. JavaScript Classes
- 2.15. Convert a Function to a Class
- 2.16. Working with JavaScript Classes
- 2.17. Super and Extends
- 2.18. Extending Classes from ES5 to ES6
- 2.19. Working with JavaScript Subclasses
- 2.20. Quiz: Building Classes and Subclasses (2-3)
- 2.21. Lesson 2 Summary
- Feedback on JavaScript ES6 lesson 2/4
Functions are one of the primary data structures in JavaScript; they've been around forever.
ES6 introduces a new kind of function called the arrow function.
Arrow functions are very similar to regular functions in behavior, but are quite different syntactically. The following code takes a list of names and converts each one to uppercase using a regular function:
const upperizedNames = ["Farrin", "Kagure", "Asser"].map(function(name) { return name.toUpperCase() })The code below does the same thing except instead of passing a regular function to the
map()
method, it passes an arrow function. Notice the arrow in the arrow function (=>
) in the code below:const upperizedNames = ["Farrin", "Kagure", "Asser"].map(name => name.toUpperCase() )The only change to the code above is the code inside the
map()
method. It takes a regular function and changes it to use an arrow function.NOTE: Not sure how
map()
works? It's a method on the Array prototype. You pass a function to it, and it calls that function once on every element in the array. It then gathers the returned values from each function call and makes a new array with those results. For more info, check out MDN's documentation.
const upperizedNames = ["Farrin", "Kagure", "Asser"].map(function(name) {
return name.toUpperCase()
})
With the function above, there are only a few steps for converting the existing "normal" function into an arrow function:
- remove the
function
keyword - remove the parentheses
- remove the opening and closing curly braces
- remove the
return
keyword - remove the semicolon
- add an arrow (
=>
) between the parameter list and the function body
Converting a normal function into an arrow function:
// es5
const upperizedNames = ["Farrin", "Kagure", "Asser"].map(function(name) {
return name.toUpperCase()
})
// es6
const upperizedNames = ["Farrin", "Kagure", "Asser"].map(name =>
name.toUpperCase()
)
Take a look at the following code:
const names = [
"Afghanistan",
"Aruba",
"Bahamas",
"Chile",
"Fiji",
"Gabon",
"Luxembourg",
"Nepal",
"Singapore",
"Uganda",
"Zimbabwe"
]
const longNames = names.filter(function(name) {
return name.length > 6
})
Which of the following choices does the same thing, but replaces .filter()'s function with an arrow function?
const longNames = names.filter( function(name) => return name.length > 6; );
const longNames = names.filter( return name.length > 6 );
const longNames = names.filter( name => {names.length > 6} );
const longNames = names.filter( name => name.length > 6 );
Solution
const longNames = names.filter(name => name.length > 6)
Got it on my first try by evaluating the checklist above.
This arrow function returns country names that are six characters or longer.
Regular functions can be either function declarations or function expressions, but arrow functions are always expressions. In fact, their full name is "arrow function expressions", so they can only be used where an expression is valid. This includes being:
- stored in a variable,
- passed as an argument to a function,
- and stored in an object's property.
One confusing syntax is when an arrow function is stored in a variable.
const greet = name => `Hello ${name}!`In the code above, the arrow function is stored in the greet variable and you'd call it like this:
greet("Asser")Returns:
Hello Asser!
You might have noticed the arrow function from the
greet()
function looks like this:name => `Hello ${name}!`If you recall, the parameter list appears before the arrow function's arrow (i.e.
=>
). If there's only one parameter in the list, then you can write it just like the example above. But, if there are two or more items in the parameter list, or if there are zero items in the list, then you need to wrap the list in parentheses:// empty parameter list requires parentheses const sayHi = () => console.log("Hello Udacity Student!") sayHi()Prints:
Hello Udacity Student!
// multiple parameters requires parentheses const orderIceCream = (flavor, cone) => console.log(`Here's your ${flavor} ice cream in a ${cone} cone.`) orderIceCream("chocolate", "waffle")Prints:
Here's your chocolate ice cream in a waffle cone.
Which of the following choices have correctly formatted arrow functions?
setTimeout(() => {
console.log("starting the test")
test.start()
}, 2000)
setTimeout(_ => {
console.log("starting the test")
test.start()
}, 2000)
const vowels = "aeiou".split("")
const bigVowels = vowels.map(letter => letter.toUpperCase())
const vowels = "aeiou".split("")
const bigVowels = vowels.map(letter => letter.toUpperCase())
Solution
All four options are valid uses of arrow functions.
Additional notes from James Priest:
If there's no parameter to the function, you just use a pair of empty parentheses like option 1.
Alternatively, some developers choose to use an underscore as their single parameter. The underscore never gets used, so it's
undefined
inside the function, but it's a common technique.The only difference between options 3 and 4 is the use of the parentheses around
letter
. Typically, if there's only one parameter, then no parentheses are used, but it's not wrong.
All of the arrow functions we've been looking at have only had a single expression as the function body:
const upperizedNames = ["Farrin", "Kagure", "Asser"].map(name => name.toUpperCase() )This format of the function body is called the "concise body syntax". The concise syntax:
- has no curly braces surrounding the function body
- and automatically returns the expression.
If you need more than just a single line of code in your arrow function's body, then you can use the "block body syntax".
const upperizedNames = ["Farrin", "Kagure", "Asser"].map(name => { name = name.toUpperCase() return `${name} has ${name.length} characters in their name` })Important things to keep in mind with the block syntax:
- it uses curly braces to wrap the function body
- and a return statement needs to be used to actually return something from the function.
Using your knowledge of how arrow functions work with automatic returns and curly braces, which of the following choices have correctly formatted arrow functions?
const colors = ["red", "blue", "yellow", "orange", "black"]
const crazyColors = color.map(color => {
const jumble = color.split("").reverse()
return jumble.join("") + "!"
})
const colors = ["red", "blue", "yellow", "orange", "black"]
const crazyColors = color.map(color => {
color
.split("")
.reverse()
.join("") + "!"
})
const colors = ['red', 'blue', 'yellow', 'orange', 'black'];
const crazyColors = color.map( color => return color.split('').reverse().join('') + '!' );
const colors = ["red", "blue", "yellow", "orange", "black"]
const crazyColors = color.map(
color =>
color
.split("")
.reverse()
.join("") + "!"
)
Solution
Again, I evaluated the checklist above, along with the new info.
Options 1 and 4 both use correct syntax for arrow functions.
Additional notes from James Priest:
- Option 1 is correct. Because the arrow function uses curly braces, there has to be a
return
in there somewhere for something to actually be returned.- Option 2 is not correct because it has curly braces and no
return
. This function runs, but nothing gets returned to crazyColors.- Option 3 doesn't have curly braces. This means it needs to be in the concise syntax and automatically return the expression so it should not have a
return
keyword, so this one isn't correct.- Option 4 is correct. This is the most common way you'll see arrow functions written—as one-liners that automatically return.
So arrow functions are awesome!
- The syntax is a lot shorter,
- it's easier to write and read short, single-line functions,
- and they automatically return when using the concise body syntax!
WARNING: Everything's not all ponies and rainbows though, and there are definitely times when you might not want to use an arrow function. So before you wipe from your memory how to write a traditional function, check out these implications:
there's a gotcha with the
this
keyword in arrow functions
- go to the next lesson to find out the details!
arrow functions are only expressions
- there's no such thing as an arrow function declaration
Convert the function passed to the map()
method into an arrow function.
/*
- Programming Quiz: Convert Function into an Arrow Function (2-1)
*/
// convert to an arrow function
const squares = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(function(square) {
return square - square
})
console.log(...squares)
Solution
I walked through the steps and example code above to get the solution:
- remove the
function
keyword - remove the parentheses
- remove the opening and closing curly braces
- remove the
return
keyword - remove the semicolon
- add an arrow (
=>
) between the parameter list and the function body
// es5
const upperizedNames = ["Farrin", "Kagure", "Asser"].map(function(name) {
return name.toUpperCase()
})
// es6
const upperizedNames = ["Farrin", "Kagure", "Asser"].map(name =>
name.toUpperCase()
)
Got it on my first try!
// converted from normal to arrow function
const squares = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(square => square - square)
console.log(...squares)
1 4 9 16 25 36 49 64 81 100
What Went Well
- Your code should have a variable squares
- The variable squares should be an array
- Your code should replace the function expression with an arrow function
- Your arrow function should have one parameter called square
- Your arrow function should square each element in the squares array
Feedback
Your answer passed all our tests! Awesome job!
Arrow functions allow us to remove
function
return
{ }
- Finally, the mystical "this"!
- The value of "this" depends on how the function is called.
- In arrow functions, the value of "this" depends on where it's located in the code.
To get a handle on how
this
works differently with arrow functions, let's do a quick recap of howthis
works in a standard function. If you have a solid grasp of howthis
works already, feel free to jump over this section.The value of the
this
keyword is based completely on how its function (or method) is called.this
could be any of the following:
If the function is called with
new
:const mySundae = new Sundae("Chocolate", ["Sprinkles", "Hot Fudge"])In the code above, the value of
this
inside theSundae
constructor function is a new object because it was called withnew
.
If the function is invoked with
call
/apply
:const result = obj1.printName.call(obj2)In the code above, the value of
this
insideprintName()
will refer toobj2
since the first parameter ofcall()
is to explicitly set whatthis
refers to.
If the function is a method of an object:
data.teleport()In the code above, the value of
this
insideteleport()
will refer todata
.
If the function is called with no context:
teleport()In the code above, the value of
this
insideteleport()
is either the global object or, if in strict mode, it'sundefined
.
TIP:
this
in JavaScript is a complicated topic. We just did a quick overview, but for an in-depth look at howthis
is determined, check out this All Makes Sense Now! from Kyle Simpson's book series You Don't Know JS.
What is the value of this
inside the Train
constructor function below?
const redTrain = new Train("red")
- the
window
object - a new object
undefined
Solution
a new object
Since the
new
keyword was used, the correct answer is a new object.
What is the value of this
inside the increaseSpeed()
function below?
const redTrain = new Train("red")
redTrain.increaseSpeed(25)
- the
window
object - a new object
- the
redTrain
object undefined
Solution
the redTrain
object
Since the
increaseSpeed()
function is called from a context object (redTrain
) that context object will be the value ofthis
in the function.
I didn't follow 2.08 very well. I worked on it early in the morning and I wasn't really awake yet.
With regular functions, the value of
this
is set based on how the function is called. With arrow functions, the value ofthis
is based on the function's surrounding context. In other words, the value ofthis
inside an arrow function is the same as the value ofthis
outside the function.Let's check out an example with
this
in regular functions and then look at how arrow functions will work.// constructor function IceCream() { this.scoops = 0 } // adds scoop to ice cream IceCream.prototype.addScoop = function() { setTimeout(function() { this.scoops++ console.log("scoop added!") }, 500) } const dessert = new IceCream() dessert.addScoop()Prints:
scoop added!
After running the code above, you'd think that
dessert.scoops
would be 1 after half a millisecond. But, unfortunately, it's not:console.log(dessert.scoops)Prints:
0
Can you tell why?
The function passed to
setTimeout()
is called withoutnew
, withoutcall()
, withoutapply()
, and without a context object. That means the value ofthis
inside the function is the global object and NOT thedessert
object. So what actually happened was that a newscoops
variable was created (with a default value ofundefined
) and was then incremented (undefined + 1
results inNaN
):console.log(scoops)Prints:
NaN
One way around this is to use closure:
// constructor function IceCream() { this.scoops = 0 } // adds scoop to ice cream IceCream.prototype.addScoop = function() { const cone = this // sets `this` to the `cone` variable setTimeout(function() { cone.scoops++ // references the `cone` variable console.log("scoop added!") }, 0.5) } const dessert = new IceCream() dessert.addScoop()The code above will work because instead of using
this
inside the function, it sets the cone variable to this and then looks up the cone variable when the function is called. This works because it's using the value of the this outside the function. So if we check the number of scoops in our dessert right now, we'll see the correct value of1
:console.log(dessert.scoops)Prints:
1
Well that's exactly what arrow functions do, so let's replace the function passed to
setTimeout()
with an arrow function:// constructor function IceCream() { this.scoops = 0 } // adds scoop to ice cream IceCream.prototype.addScoop = function() { setTimeout(() => { // an arrow function is passed to setTimeout this.scoops++ console.log("scoop added!") }, 0.5) } const dessert = new IceCream() dessert.addScoop()Since arrow functions inherit their this value from the surrounding context, this code works!
console.log(dessert.scoops)Prints:
1When
addScoop()
is called, the value ofthis
insideaddScoop()
refers todessert
. Since an arrow function is passed tosetTimeout()
, it's using its surrounding context to determine whatthis
refers to inside itself. So sincethis
outside of the arrow function refers todessert
, the value ofthis
inside the arrow function will also refer todessert
.`Now what do you think would happen if we changed the
addScoop()
method to an arrow function?// constructor function IceCream() { this.scoops = 0 } // adds scoop to ice cream IceCream.prototype.addScoop = () => { // addScoop is now an arrow function setTimeout(() => { this.scoops++ console.log("scoop added!") }, 0.5) } const dessert = new IceCream() dessert.addScoop()Yeah, this doesn't work for the same reason - arrow functions inherit their
this
value from their surrounding context. Outside of theaddScoop()
method, the value ofthis
is the global object. So ifaddScoop()
is an arrow function, the value ofthis
insideaddScoop()
is the global object. Which then makes the value ofthis
in the function passed tosetTimeout()
also set to the global object!
Take a look at this code:
function greet(name, greeting) { name = typeof name !== "undefined" ? name : "Student" greeting = typeof greeting !== "undefined" ? greeting : "Welcome" return `${greeting} ${name}!` } greet() // Welcome Student! greet("James") // Welcome James! greet("Richard", "Howdy") // Howdy Richard!Returns:
Welcome Student! Welcome James! Howdy Richard!
What is all that horrible mess in the first two lines of the
greet()
function? All of that is there to provide default values for the function if the required arguments aren't provided. It's pretty ugly, though...Fortunately, ES6 has introduced a new way to create defaults. It's called default function parameters.
Default function parameters are quite easy to read since they're placed in the function's parameter list:
function greet(name = "Student", greeting = "Welcome") { return `${greeting} ${name}!` } greet() // Welcome Student! greet("James") // Welcome James! greet("Richard", "Howdy") // Howdy Richard!Returns:
Welcome Student! Welcome James! Howdy Richard!
Wow, that's a lot less code, so much cleaner, and significantly easier to read!
To create a default parameter, you add an equal sign and then whatever you want the parameter to default to if an argument is not provided. In the code above, both parameters have default values of strings, but they can be any JavaScript type!
Take a look at the following code:
function shippingLabel(name, address) {
name = typeof name !== "undefined" ? name : "Richard"
address = typeof address !== "undefined" ? address : "Mountain View"
return `To: ${name} In: ${address}`
}
Which of the following choices is the correct way to write the shippingLabel()
function using default function parameters?
function shippingLabel(name = "", address = "") {
return `To ${name} In: ${address}`
}
function shippingLabel(name, address) {
name = name || "Richard"
address = address || "Mountain View"
return `To: ${name} In: ${address}`
}
function shippingLabel(name, address) {
return `To: ${name} In: ${address}`
}
function shippingLabel(name = "Richard", address = "Mountain View") {
return `To: ${name} In: ${address}`
}
Solution
Option 4 uses default function parameters correctly by setting the defaults directly to the parameters.
function shippingLabel(name = "Richard", address = "Mountain View") {
return `To: ${name} In: ${address}`
}
You can combine default function parameters with destructuring to create some pretty powerful functions!
function createGrid([width = 5, height = 5]) { return `Generates a ${width} x ${height} grid` } createGrid([]) // Generates a 5 x 5 grid createGrid([2]) // Generates a 2 x 5 grid createGrid([2, 3]) // Generates a 2 x 3 grid createGrid([undefined, 3]) // Generates a 5 x 3 gridReturns:
Generates a 5 x 5 grid Generates a 2 x 5 grid Generates a 2 x 3 grid Generates a 5 x 3 grid
The
createGrid()
function expects an array to be passed to it. It uses destructuring to set the first item in the array to thewidth
and the second item to be theheight
. If the array is empty or if it has only one item in it, then the default parameters kick in and give the missing parameters a default value of5
.There is a problem with this though, the following code will not work:
createGrid() // throws an errorUncaught TypeError:** Cannot read property 'Symbol(Symbol.iterator)' of undefined
This throws an error because
createGrid()
expects an array to be passed in that it will then destructure. Since the function was called without passing an array, it breaks. But, we can use default function parameters for this!function createGrid([width = 5, height = 5] = []) { return `Generates a ${width} x ${height} grid` }See that new
= []
in the function's parameter? IfcreateGrid()
is called without any argument then it will use this default empty array. And since the array is empty, there's nothing to destructure intowidth
andheight
, so their default values will apply! So by adding= []
to give the entire parameter a default, the following code will now work:createGrid() // Generates a 5 x 5 gridReturns:
Generates a 5 x 5 grid
Take a look at the following code:
function houseDescriptor([houseColor = "green", shutterColors = ["red"]]) {
return `I've a ${houseColor} house w/ ${shutterColors.join(" and ")} shutters`
}
Which of the following choices will run without throwing an error?
houseDescriptor('red', ['white', 'gray', 'pink']);
houseDescriptor(['green', ['white', 'gray', 'pink']]);
houseDescriptor(['blue', 'purple']);
houseDescriptor(['green]);
Solution
Options 2 and 4 are the only choices that will run correctly without throwing an error.
Additional notes from James Priest:
- Since
houseDescriptor
is expecting only a single argument (an array) to be passed in, Option 1 has to be incorrect since it's calling the function with two arguments.- Option 2 is correct.
- Option 3 does call the function with a single array argument, but the second item in the list is a string and
.join()
is not a method of strings, so the code throws an error.- Option 4 is correct.
Just like array destructuring with array defaults, a function can have an object be a default parameter and use object destructuring:
function createSundae({ scoops = 1, toppings = ["Hot Fudge"] }) { const scoopText = scoops === 1 ? "scoop" : "scoops" return `Your sundae has ${scoops} ${scoopText} with ${toppings.join( " and " )} toppings.` } createSundae({}) // Your sundae has 1 scoop with Hot Fudge toppings. createSundae({ scoops: 2 }) // Your sundae has 2 scoops with Hot Fudge toppings. createSundae({ scoops: 2, toppings: ["Sprinkles"] }) // Your sundae has 2 scoops with Sprinkles toppings. createSundae({ toppings: ["Cookie Dough"] }) // Your sundae has 1 scoop with Cookie Dough toppings.Returns:
Your sundae has 1 scoop with Hot Fudge toppings. Your sundae has 2 scoops with Hot Fudge toppings. Your sundae has 2 scoops with Sprinkles toppings. Your sundae has 1 scoop with Cookie Dough toppings.
Just like the array example before, if you try calling the function without any arguments it won't work:
createSundae() // throws an errorUncaught TypeError: Cannot match against 'undefined' or 'null'.
We can prevent this issue by providing a default object to the function:
function createSundae({ scoops = 1, toppings = ["Hot Fudge"] } = {}) { const scoopText = scoops === 1 ? "scoop" : "scoops" return `Your sundae has ${scoops} ${scoopText} with ${toppings.join( " and " )} toppings.` }By adding an empty object as the default parameter in case no arguments are provided, calling the function without any arguments now works.
createSundae() // Your sundae has 1 scoop with Hot Fudge toppings.Returns:
Your sundae has 1 scoop with Hot Fudge toppings.
Take a look at the following code:
function houseDescriptor({
houseColor = "green",
shutterColors = ["red"]
} = {}) {
return `I have a ${houseColor} house with ${shutterColors.join(
" and "
)} shutters`
}
Which of the following choices will run without throwing an error?
houseDescriptor({houseColor: 'red', shutterColors: ['white', 'gray', 'pink']});
houseDescriptor({houseColor: 'red'});
houseDescriptor();
houseDescriptor({shutterColors: ['orange', 'blue']});
houseDescriptor({});
Solution
Actually, every single one of these function calls will work correctly!
Additional notes from James Priest:
The only option that would NOT work is:
- houseDescriptor({houseColor: 'red', shutterColors: 'white'});
Uncaught TypeError:.join
is not a function of String. The function is expecting an array as theshutterColors
property.
Default function parameters are a simple addition, but it makes our lives so much easier! One benefit of object defaults over array defaults is how they handle skipped options. Check this out:
function createSundae({ scoops = 1, toppings = ["Hot Fudge"] } = {}) {}With the
createSundae()
function using object defaults with destructuring, if you want to use the default value forscoops
but change thetoppings
, then all you need to do is pass in an object withtoppings
:createSundae({ toppings: ["Hot Fudge", "Sprinkles", "Caramel"] })Compare the above example with the same function that uses array defaults with destructuring.
function createSundae([scoops = 1, toppings = ["Hot Fudge"]] = []) {}With this function setup, if you want to use the default number of scoops but change the toppings, you'd have to call your function a little...oddly:
createSundae([undefined, ["Hot Fudge", "Sprinkles", "Caramel"]])Since arrays are positionally based, we have to pass
undefined
to "skip" over the first argument (and accept the default) to get to the second argument.Unless you've got a strong reason to use array defaults with array destructuring, we recommend going with object defaults with object destructuring!
Create a buildHouse()
function that accepts an object as a default parameter. The object should set the following properties to these default values:
floors = 1
color = 'red'
walls = 'brick'
The function should return the following if no arguments or any empty object is passed to the function.
Your house has 1 floor(s) with red brick walls.
Code:
/*
- Programming Quiz: Using Default Function Parameters (2-2)
*/
// your code goes here
// tests
console.log(buildHouse())
console.log(buildHouse({}))
console.log(buildHouse({ floors: 3, color: "yellow" }))
Output:
Your house has 1 floor(s) with red brick walls.
Your house has 1 floor(s) with red brick walls.
Your house has 3 floor(s) with yellow brick walls.
Solution
You basically have to tell the function two things:
- What it should contain
- What the output should be
The sample code from the tests helps suggest the function formatting.
// function
function buildHouse({ floors = 1, color = "red", walls = "brick" } = {}) {
return `Your house has ${floors} floor(s) with ${color} ${walls} walls.`
}
// tests
console.log(buildHouse())
console.log(buildHouse({}))
console.log(buildHouse({ floors: 3, color: "yellow" }))
Your house has 1 floor(s) with red brick walls.
Your house has 1 floor(s) with red brick walls.
Your house has 3 floor(s) with yellow brick walls.
What Went Well
- Your code should have a function
buildHouse()
- Your
buildHouse()
function should have one parameter- Your
buildHouse()
function should accept an object and an empty object as a default parameter- Your
buildHouse()
function should set thefloors
,color
, andwalls
properties to default values- Your
buildHouse()
function should produce the correct output when no arguments or any empty object is passed to it- Your
buildHouse()
function should produce the correct output when a valid object is passed to itFeedback
Your answer passed all our tests! Awesome job!
Here's a quick peek of what a JavaScript class looks like:
class Dessert { constructor(calories = 250) { this.calories = calories } } class IceCream extends Dessert { constructor(flavor, calories, toppings = []) { super(calories) this.flavor = flavor this.toppings = toppings } addTopping(topping) { this.toppings.push(topping) } }Notice the new
class
keyword right in front ofDessert
andIceCream
, or the newextends
keyword inclass IceCream extends Dessert
? What about the call tosuper()
inside the IceCream'sconstructor()
method.There are a bunch of new keywords and syntax to play with when creating JavaScript classes. But, before we jump into the specifics of how to write JavaScript classes, we want to point out a rather confusing part about JavaScript compared with class-based languages.
- In other languages, we use functions to create classes and provide inheritance.
- JavaScript is not a class-based language. The classes are just a mirage over prototypal inheritance.
- In JavaScript, we use functions to create objects.
- JavaScript links objects together by "prototypal inheritance."
Since ES6 classes are just a mirage and hide the fact that prototypal inheritance is actually going on under the hood, let's quickly look at how to create a "class" with ES5 code:
function Plane(numEngines) { this.numEngines = numEngines this.enginesActive = false } // methods "inherited" by all instances Plane.prototype.startEngines = function() { console.log("starting engines...") this.enginesActive = true } const richardsPlane = new Plane(1) richardsPlane.startEngines() const jamesPlane = new Plane(4) jamesPlane.startEngines()In the code above, the
Plane
function is a constructor function that will create new Plane objects. The data for a specific Plane object is passed to thePlane
function and is set on the object. Methods that are "inherited" by each Plane object are placed on thePlane.prototype
object. ThenrichardsPlane
is created with one engine whilejamesPlane
is created with 4 engines. Both objects, however, use the samestartEngines
method to activate their respective engines.Things to note:
- the constructor function is called with the
new
keyword- the constructor function, by convention, starts with a capital letter
- the constructor function controls the setting of data on the objects that will be created
- "inherited" methods are placed on the constructor function's prototype object
Keep these in mind as we look at how ES6 classes work because, remember, ES6 classes set up all of this for you under the hood.
Here's what that same Plane class would look like if it were written using the new class syntax:
class Plane { constructor(numEngines) { this.numEngines = numEngines this.enginesActive = false } startEngines() { console.log("starting engines…") this.enginesActive = true } }
Let's convert this function into a class.
// ES5 Syntax function Plane(numEngines) { this.numEngines = numEngines this.enginesActive = false } Plane.prototype.startEngines = function() { console.log("starting engines...") this.enginesActive = true } var richardsPlane = new Plane(1) richardsPlane.startEngines() var jamesPlane = new Plane(4) jamesPlane.startEngines()Everything inside the constructor function is now placed inside a method with the name constructor.
// ES6 Syntax class Plane { constructor(numEngines) { this.numEngines = numEngines this.enginesActive = false } }This constructor method will automatically run when a new object is constructed from this class. If any data is needed to create the object then it needs to be included here.
So this takes care of creating an object. Now the methods that all objects inherit are placed inside the class.
// ES6 Syntax class Plane { constructor(numEngines) { this.numEngines = numEngines this.enginesActive = false } startEngines() { console.log("starting engines...") this.enginesActive = true } }
startEngines()
exists on the prototype explicitly in the pre-class way of writing it. Now it appears inside the class but the functionality is exactly the same.Also it looks like
startEngines()
and thisconstructor()
method are the same kind of method but the constructor method is not on the prototype. It's a new special method that exists in a class and is used to initialize new objects.To drive this home, the functionality of these two is exactly the same. The class syntax is just a nicer way of writing it. In fact, we create new objects in exactly the same way with this new class syntax.
// ES6 Syntax class Plane { constructor(numEngines) { this.numEngines = numEngines this.enginesActive = false } startEngines() { console.log("starting engines...") this.enginesActive = true } } var richardsPlane = new Plane(1) richardsPlane.startEngines() var jamesPlane = new Plane(1) jamesPlane.startEngines()If you already understand prototypal inheritance then you already have a good understanding of how
class
and class methods work.
Just to prove that there isn't anything special about
class
, check out this code:class Plane { constructor(numEngines) { this.numEngines = numEngines this.enginesActive = false } startEngines() { console.log("starting engines…") this.enginesActive = true } } typeof Plane // functionReturns:
function
That's right—it's just a function! There isn't even a new type added to JavaScript.
⚠️ Where Are All The Commas?⚠️ Did you notice that there aren't any commas between the method definitions in the Class? Commas are not used to separate properties or methods in a Class. If you add them, you'll get a
SyntaxError
ofunexpected token ,
Take a look at the following code:
class Animal {
constructor(name = "Sprinkles", energy = 100) {
this.name = name
this.energy = energy
}
eat(food) {
this.energy += food / 3
}
}
Which of the following are true?
- the
eat()
method ends uponAnimal.prototype
typeof Animal === 'class'
typeof Animal === 'function'
Solution
Options 1 and 3 are both true. Methods that appear in the class definition are placed on that class's prototype object and a class is just a function.
To add a static method, the keyword
static
is placed in front of the method name. Look at thebadWeather()
method in the code below.class Plane { constructor(numEngines) { this.numEngines = numEngines this.enginesActive = false } static badWeather(planes) { for (plane of planes) { plane.enginesActive = false } } startEngines() { console.log("starting engines…") this.enginesActive = true } }See how
badWeather()
has the wordstatic
in front of it whilestartEngines()
doesn't? That makesbadWeather()
a method that's accessed directly on thePlane
class, so you can call it like this:Plane.badWeather([plane1, plane2, plane3])NOTE: A little hazy on how constructor functions, class methods, or prototypal inheritance works? We've got a course on it! Check out Object Oriented JavaScript.
- Less setup
- There's a lot less code that you need to write to create a function
- Clearly defined constructor function
- Inside the class definition, you can clearly specify the constructor function.
- Everything's contained
- All code that's needed for the class is contained in the class declaration. Instead of having the constructor function in one place, then adding methods to the prototype one-by-one, you can do everything all at once!
class
is not magic
- The
class
keyword brings with it a lot of mental constructs from other, class-based languages. It doesn't magically add this functionality to JavaScript classes.class
is a mirage over prototypal inheritance
- We've said this many times before, but under the hood, a JavaScript class just uses prototypal inheritance.
- Using classes requires the use of
new
- When creating a new instance of a JavaScript class, the
new
keyword must be usedFor example,
class Toy { // some class code } const myToy1 = Toy() // throws an errorUncaught TypeError: Class constructor Toy cannot be invoked without 'new'
const myToy2 = new Toy() // this works!
Now that we've looked at creating classes in JavaScript. Let's use the new
super
andextends
keywords to extend a class.// ES6 -------------------------------------- class Tree { constructor( size = "10", leaves = { spring: "green", summer: "green", fall: "orange", winter: null } ) { this.size = size this.leaves = leaves this.leafColor = null } changeSeason(season) { this.leafColor = this.leaves[season] if (season === "spring") { this.size += 1 } } } class Maple extends Tree { constructor(syrupQty = 15, size, leaves) { super(size, leaves) this.syrupQty = syrupQty } changeSeason(season) { super.changeSeason(season) if (season === "spring") { this.syrupQty += 1 } } gatherSyrup() { this.syrupQty -= 3 } } const myMaple = new Maple(15, 5) myMaple.changeSeason("fall") myMaple.gatherSyrup() myMaple.changeSeason("spring")Both
Tree
andMaple
are JavaScript classes. TheMaple
class is a "subclass" ofTree
and uses theextends
keyword to set itself as a "subclass".To get from the "subclass" to the parent class, the
super
keyword is used. Did you notice thatsuper
was used in two different ways? InMaple
's constructor method,super
is used as a function. InMaple
'schangeSeason()
method,super
is used as an object!
Let's see this same functionality, but written in ES5 code:
// ES5 -------------------------------------- function Tree() { this.size = size || 10 this.leaves = leaves || { spring: "green", summer: "green", fall: "orange", winter: null } this.leafColor } Tree.prototype.changeSeason = function(season) { this.leafColor = this.leaves[season] if (season === "spring") { this.size += 1 } } function Maple(syrupQty, size, leaves) { Tree.call(this, size, leaves) this.syrupQty = syrupQty || 15 } Maple.prototype = Object.create(Tree.prototype) Maple.prototype.constructor = Maple Maple.prototype.changeSeason = function(season) { Tree.prototype.changeSeason.call(this, season) if (season === "spring") { this.syrupQty += 1 } } Maple.prototype.gatherSyrup = function() { this.syrupQty -= 3 } const myMaple = new Maple(15, 5) myMaple.changeSeason("fall") myMaple.gatherSyrup() myMaple.changeSeason("spring")Both this code and the class-style code above achieve the same functionality.
Let's hide the inner workings of these classes to compare how they're constructed.
// ES5 -------------------------------------- function Tree(size, leaves) {...} Tree.prototype.changeSeason = function (season) {...} function Maple(syrupQty, size, barkColor, leaves) {...} Maple.prototype = Object.create(Tree.prototype); Maple.prototype.constructor = Maple; Maple.prototype.changeSeason = function (season) {...} Maple.prototype.gatherSyrup = function () {...} // ES6 -------------------------------------- class Tree { constructor(size = '10', leaves = {...}) {...} changeSeason(season) {...} } class Maple extends Tree { constructor(syrupQty = 15, size, leaves) {...} changeSeason(season) {...} gatherSyrup() {...} }Remember that there's a new special method called the
constructor()
that's run whenever the class is called. It's doing the same thing as theTree
constructor in ES5.Also remember that a method inside of a class definition (
changeSeason
) is the same as adding that method to the prototype. That takes care of the base class which looks pretty similar to before.The bigger difference comes when extending the base class with a subclass. With the older ES5 code we'd have to:
- Create another constructor function
- Then set the function's prototype to the base class' prototype
- Since we've overwritten the original prototype object, we need to set/reset the connection between the constructor property and the original constructor function.
Then we're back to the normal routine of adding methods to the prototype object.
Now compare all of the code it took to get these two functions connected and prototype linked in ES5 to the class code of ES6.
It's just another class definition but it uses the extends keyword to connect the
Maple
class to the base classTree
.Significantly nicer right? It's also a lot easier to call the base class from the subclass.
The es6 code uses the new
super
keyword while you have to use.call
in the es5 code and passthis
as the first argument.Also, calling a prototype method also takes a lot less code in the new class format too.
Like most of the new additions, there's a lot less setup code and it's a lot cleaner syntax to create a subclass using
class
,super
, andextends
.Just remember that, under the hood, the same connections are made between functions and prototypes.
In a subclass constructor function, before
this
can be used, a call to the super class must be made.class Apple {} class GrannySmith extends Apple { constructor(tartnessLevel, energy) { this.tartnessLevel = tartnessLevel // `this` before `super` throws an error! super(energy) } }
Take a look at the following code:
class Toy {}
class Dragon extends Toy {}
const dragon1 = new Dragon()
Given the code above, is the following statement true or false?
dragon1 instanceof Toy
Solution
Got it on my first try.
The
dragon1
variable is an object created by theDragon
class, and since theDragon
class extends theToy
class,dragon1
is also considered an instance ofToy
.
Let's say that a Toy
class exists and that a Dragon
class extends the Toy
class.
What is the correct way to create a Toy
object from inside the Dragon
class's constructor
method?
super();
super.call(this)
parent();
Toy();
Solution
Got it on my first try again.
Option 1 is the correct way to call the super class from within the subclass's constructor function.
Quiz
Create a Bicycle
subclass that extends the Vehicle
class. The Bicycle
subclass should override Vehicle
's constructor function by changing the default values for wheels
from 4
to 2
and horn
from 'beep beep'
to 'honk honk'
.
/*
- Programming Quiz: Building Classes and Subclasses (2-3)
*/
class Vehicle {
constructor(color = 'blue', wheels = 4, horn = 'beep beep') {
this.color = color;
this.wheels = wheels;
this.horn = horn;
}
honkHorn() {
console.log(this.horn);
}
}
// your code goes here
/- tests
const myVehicle = new Vehicle();
myVehicle.honkHorn(); // beep beep
const myBike = new Bicycle();
myBike.honkHorn(); // honk honk
*/
Output:
beep beep
honk honk
Solution
/*
- Programming Quiz: Building Classes and Subclasses (2-3)
*/
class Vehicle {
constructor(color = "blue", wheels = 4, horn = "beep beep") {
this.color = color
this.wheels = wheels
this.horn = horn
}
honkHorn() {
console.log(this.horn)
}
}
// your code goes here
class Bicycle extends Vehicle {
constructor(color, wheels = 2, horn = "honk honk") {
super(color, wheels, horn)
this.horn = horn
}
honkHorn() {
super.honkHorn()
}
}
// tests
const myVehicle = new Vehicle()
myVehicle.honkHorn() // beep beep
const myBike = new Bicycle()
myBike.honkHorn() // honk honk
What Went Well
- Your code should have a class Vehicle
- Your code should have a class Bicycle
- Your class Bicycle should be a subclass of the class Vehicle
- Your class Bicycle should have a constructor
- Your Bicycle's constructor should set default values for color, wheels, and horn
- Your Bicycle's constructor should override Vehicle's constructor as specified in the directions
Feedback
Your answer passed all our tests! Awesome job!
Very helpful! Thank you!