ES6 - JavaScript Improved course lesson 3/4
Udacity Google Mobile Web Specialist Nanodegree program part 3 lesson 06
Udacity Grow with Google Scholarship challenge course lesson 08
Brendon Smith
- Symbols are a new "primitive data type" for JavaScript. A symbol is used to uniquely identify properties within an object.
- Previous primitive data types were:
- numbers
- strings
- booleans
- null
- undefined
A symbol is a unique and immutable data type that is often used to identify object properties.
To create a symbol, you write
Symbol()
with an optional string as its description.const sym1 = Symbol("apple") console.log(sym1)Symbol(apple)
This will create a unique symbol and store it in
sym1
. The description"apple"
is just a way to describe the symbol, but it can’t be used to access the symbol itself.And just to show you how this works, if you compare two symbols with the same description…
const sym2 = Symbol("banana") const sym3 = Symbol("banana") console.log(sym2 === sym3)false
...then the result is
false
because the description is only used to describe the symbol. It’s not used as part of the symbol itself. Each time, a new symbol is created, regardless of the description.Still, this can be hard to wrap your head around, so let’s use the example from the previous section to see how symbols can be useful. Here’s the code to represent the bowl from the example.
const bowl = { apple: { color: "red", weight: 136.078 }, banana: { color: "yellow", weight: 183.15 }, orange: { color: "orange", weight: 170.097 } }The bowl contains fruit which are objects that are properties of the bowl. But, we run into a problem when the second banana gets added.
const bowl = { apple: { color: "red", weight: 136.078 }, banana: { color: "yellow", weight: 183.151 }, orange: { color: "orange", weight: 170.097 }, banana: { color: "yellow", weight: 176.845 } } console.log(bowl)Object {apple: Object, banana: Object, orange: Object}
Instead of adding another banana to the bowl, our previous banana is overwritten by the new banana being added to the bowl. To fix this problem, we can use symbols.
const bowl = { [Symbol("apple")]: { color: "red", weight: 136.078 }, [Symbol("banana")]: { color: "yellow", weight: 183.15 }, [Symbol("orange")]: { color: "orange", weight: 170.097 }, [Symbol("banana")]: { color: "yellow", weight: 176.845 } } console.log(bowl)Object {Symbol(apple): Object, Symbol(banana): Object, Symbol(orange): Object, Symbol(banana): Object}
By changing the bowl’s properties to use symbols, each property is a unique Symbol and the first banana doesn’t get overwritten by the second banana.
Before you move on, let’s spend some time looking at two new protocols in ES6:
- the iterable protocol
- the iterator protocol
These protocols aren’t built-ins, but they will help you understand the new concept of iteration in ES6, as well as show you a use case for symbols.
The iterable protocol is used for defining and customizing the iteration behavior of objects. What that really means is you now have the flexibility in ES6 to specify a way for iterating through values in an object. For some objects, they already come built-in with this behavior. For example, strings and arrays are examples of built-in iterables.
const digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] for (const digit of digits) { console.log(digit) }0 1 2 3 4 5 6 7 8 9
If you recall from earlier lesson 1, any object that is iterable can use the new
for..of
loop. Later in this lesson, you’ll also learn about Sets and Maps which are other examples of built-in iterables.
In order for an object to be iterable, it must implement the iterable interface. If you come from a language like Java or C, then you’re probably familiar with interfaces, but for those of you who aren’t, that basically means that in order for an object to be iterable it must contain a default iterator method. This method will define how the object should be iterated.
The iterator method, which is available via the constant
[Symbol.iterator]
, is a zero arguments function that returns an iterator object. An iterator object is an object that conforms to the iterator protocol.
The iterator protocol is used to define a standard way that an object produces a sequence of values. What that really means is you now have a process for defining how an object will iterate. This is done through implementing the
.next()
method.
An object becomes an iterator when it implements the
.next()
method. The.next()
method is a zero arguments function that returns an object with two properties:
value
: the data representing the next value in the sequence of values within the objectdone
: a boolean representing if the iterator is done going through the sequence of values
- If done is true, then the iterator has reached the end of its sequence of values.
- If done is false, then the iterator is able to produce another value in its sequence of values.
Here’s the example from earlier, but instead we are using the array’s default iterator to step through each value in the array.
const digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] const arrayIterator = digits[Symbol.iterator]() console.log(arrayIterator.next()) console.log(arrayIterator.next()) console.log(arrayIterator.next())Object {value: 0, done: false} Object {value: 1, done: false} Object {value: 2, done: false}
If you think back to mathematics, a set is a collection of distinct items. For example,
{2, 4, 5, 6}
is a set because each number is unique and appears only once. However,{1, 1, 2, 4}
is not a set because it contains duplicate entries (the 1 is in there more than once!).In JavaScript, we can already represent something similar to a mathematical set using an array.
const nums = [2, 4, 5, 6]However, arrays do no enforce items to be unique. If we try to add another
2
tonums
, JavaScript won't complain and will add it without any issue.nums.push(2) console.log(nums)[2, 4, 5, 6, 2]
…and now
nums
is no longer a set in the mathematical sense.
In ES6, there’s a new built-in object that behaves like a mathematical set and works similarly to an array. This new object is conveniently called a "Set". The biggest differences between a set and an array are:
- Sets are not indexed-based - you do not refer to items in a set based on their position in the set
- items in a Set can’t be accessed individually
Basically, a Set is an object that lets you store unique items. You can add items to a Set, remove items from a Set, and loop over a Set. These items can be either primitive values or objects.
There’s a couple of different ways to create a Set. The first way, is pretty straightforward:
const games = new Set() console.log(games)Set {}
This creates an empty Set
games
with no items.If you want to create a Set from a list of values, you use an array:
const games = new Set([ "Super Mario Bros.", "Banjo-Kazooie", "Mario Kart", "Super Mario Bros." ]) console.log(games)Set {'Super Mario Bros.', 'Banjo-Kazooie', 'Mario Kart'}
Notice the example above automatically removes the duplicate entry
"Super Mario Bros."
when the Set is created. Pretty neat!
Select the collections below that represent a Set in JavaScript.
{1, 'Basketball', true, false, '1'}
{}
{1, 1, 1, 1}
{false, '0', 0, 'Soccer', 3.14, 25, 0}
{'Gymnastics', 'Swimming', 2}
Solution
First try. Sets make a lot of sense. Thank you ES6!
{1, 'Basketball', true, false, '1'}
{}
{'Gymnastics', 'Swimming', 2}
Nice work! The choices you've selected represent sets since all their items are unique.
After you’ve created a Set, you’ll probably want to add and delete items from the Set. So how do you that? You use the appropriately named,
.add()
and.delete()
methods:const games = new Set([ "Super Mario Bros.", "Banjo-Kazooie", "Mario Kart", "Super Mario Bros." ]) games.add("Banjo-Tooie") games.add("Age of Empires") games.delete("Super Mario Bros.") console.log(games)Set {'Banjo-Kazooie', 'Mario Kart', 'Banjo-Tooie', 'Age of Empires'}
On the other hand, if you want to delete all the items from a Set, you can use the
.clear()
method.games.clear() console.log(games)Set {}
TIP: If you attempt to
.add()
a duplicate item to a Set, you won’t receive an error, but the item will not be added to the Set. Also, if you try to `.delete() an item that is not in a Set, you won’t receive an error, and the Set will remain unchanged.
.add()
returns theSet
if an item is successfully added. On the other hand,.delete()
returns a Boolean (true
orfalse
) depending on successful deletion.
Once you’ve constructed your Set, there are a couple of different properties and methods you can use to work with Sets.
Use the
.size
property to return the number of items in a Set:const months = new Set([ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ]) console.log(months.size)12
Remember, Sets can’t be accessed by their index like an array, so you use the
.size
property instead of.length
property to get the size of the Set.
Use the
.has()
method to check if an item exists in a Set. If the item is in the Set, then.has()
will returntrue
. If the item doesn’t exist in the Set, then.has()
will returnfalse
.console.log(months.has("September"))true
Finally, use the
.values()
method to return the values in a Set. The return value of the.values()
method is aSetIterator
object.console.log(months.values())SetIterator {'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'}
More on the
SetIterator
object in a second!TIP: The
.keys()
method will behave the exact same way as the.values()
method by returning the values of a Set within a new Iterator Object. The.keys()
method is an alias for the .values() method for similarity with maps. You’ll see the.keys()
method later in this lesson during the Maps section.
The last step to working with Sets is looping over them.
If you remember back to our discussion on the new iterable and iterator protocols in ES6, then you’ll recall that Sets are built-in iterables. This means two things in terms of looping:
- You can use the Set’s default iterator to step through each item in a Set, one by one.
- You can use the new
for...of
loop to loop through each item in a Set.
Because the
.values()
method returns a new iterator object (calledSetIterator
), you can store that iterator object in a variable and loop through each item in the Set using.next()
.const iterator = months.values() iterator.next()Object {value: 'January', done: false}
And if you run
.next()
again?iterator.next()Object {value: 'February', done: false}
And so on until
done
equalstrue
which marks the end of the Set.const days = new Set(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]) const iterator = days.values() iterator.next() // {value: 'Mon', done: false} iterator.next() // {value: 'Tue', done: false} iterator.next() // {value: 'Wed', done: false} iterator.next() // {value: 'Thu', done: false} iterator.next() // {value: 'Fri', done: false} iterator.next() // {value: 'Sat', done: false} iterator.next() // {value: 'Mon', done: false} iterator.next() // {value: undefined, done: true}
An easier method to loop through the items in a Set is the for...of loop.
const colors = new Set([ "red", "orange", "yellow", "green", "blue", "violet", "brown", "black" ]) for (const color of colors) { console.log(color) }red orange yellow green blue violet brown black
Quiz
Create a variable with the name myFavoriteFlavors
and give it the value of an empty Set
object. Then use the .add()
method to add the following strings to it:
- "chocolate chip"
- "cookies and cream"
- "strawberry"
- "vanilla"
Then use the .delete()
method to remove "strawberry" from the set.
/*
- Programming Quiz: Using Sets (3-1)
*
- Create a Set object and store it in a variable named `myFavoriteFlavors`. Add the following strings to the set:
- - chocolate chip
- - cookies and cream
- - strawberry
- - vanilla
*
- Then use the `.delete()` method to remove "strawberry" from the set.
*/
Solution
/*
- Programming Quiz: Using Sets (3-1)
*
- Create a Set object and store it in a variable named `myFavoriteFlavors`.
- Add the following strings to the set:
- - chocolate chip
- - cookies and cream
- - strawberry
- - vanilla
*
- Then use the `.delete()` method to remove "strawberry" from the set.
*/
const myFavoriteFlavors = new Set()
myFavoriteFlavors.add("chocolate chip")
myFavoriteFlavors.add("cookies and cream")
myFavoriteFlavors.add("strawberry")
myFavoriteFlavors.add("vanilla")
myFavoriteFlavors.delete("strawberry")
console.log(myFavoriteFlavors)
Set { 'chocolate chip', 'cookies and cream', 'vanilla' }
What Went Well
- Your code should have a variable myFavoriteFlavors
- Your code should use the .add() method to add required items to the set
- Your code should use the .delete() method to remove "strawberry"
- Your code should use the .delete() method only once
- The myFavoriteFlavors object should contain "chocolate chip"
- The myFavoriteFlavors object should contain "cookies and cream"
- The myFavoriteFlavors object should contain "vanilla"
- The myFavoriteFlavors object should not contain "strawberry"
Feedback
Your answer passed all our tests! Awesome job!
A WeakSet is just like a normal Set with a few key differences:
- a WeakSet can only contain objects
- a WeakSet is not iterable which means it can’t be looped over
- a WeakSet does not have a
.clear()
methodYou can create a WeakSet just like you would a normal Set, except that you use the
WeakSet
constructor.const student1 = { name: "James", age: 26, gender: "male" } const student2 = { name: "Julia", age: 27, gender: "female" } const student3 = { name: "Richard", age: 31, gender: "male" } const roster = new WeakSet([student1, student2, student3]) console.log(roster)WeakSet {Object {name: 'Julia', age: 27, gender: 'female'}, Object {name: 'Richard', age: 31, gender: 'male'}, Object {name: 'James', age: 26, gender: 'male'}}
…but if you try to add something other than an object, you’ll get an error!
roster.add("Amanda")Uncaught TypeError: Invalid value used in weak set(…)
This is expected behavior because WeakSets can only contain objects. But why should it only contain objects? Why would you even use a WeakSet if normal Sets can contain objects and other types of data? Well, the answer to that question has more to do with why WeakSets do not have a
.clear()
method...
In JavaScript, memory is allocated when new values are created and is "automatically" freed up when those values are no longer needed. This process of freeing up memory after it is no longer needed is what is known as garbage collection.
WeakSets take advantage of this by exclusively working with objects. If you set an object to
null
, then you’re essentially deleting the object. And when JavaScript’s garbage collector runs, the memory that object previously occupied will be freed up to be used later in your program.let studentA = { name: "Richard", age: 31 } let studentB = { name: "Alexis", age: 28 } let studentC = { name: "Jasmine", age: 29 } let roster = new WeakSet([studentA, studentB, studentC]) studentC = null console.log(roster)WeakSet {Object {name: 'Julia', age: 27, gender: 'female'}, Object {name: 'James', age: 26, gender: 'male'}}
What makes this so useful is you don’t have to worry about deleting references to deleted objects in your WeakSets, JavaScript does it for you! When an object is deleted, the object will also be deleted from the WeakSet when garbage collection runs.
This makes WeakSets useful in situations where you want an efficient, lightweight solution for creating groups of objects.
The point in time when garbage collection happens depends on a lot of different factors. Check out MDN’s documentation to learn more about the algorithms used to handle garbage collection in JavaScript.
Quiz
Create the following variables:
uniqueFlavors
and give it the value of an emptyWeakSet
objectflavor1
, and set it to the object{ flavor: 'chocolate' }
flavor2
, and set it to an object with a property offlavor
and a value of your choice
Use the .add()
method to add the objects flavor1
and flavor2
to uniqueFlavors
.
Use the .add()
method to add the flavor1
object to the uniqueFlavors
set, again.
/*
- Programming Quiz: Using Sets (3-2)
*
- Create the following variables:
- - uniqueFlavors and set it to a new WeakSet object
- - flavor1 and set it equal to `{ flavor: 'chocolate' }`
- - flavor2 and set it equal to an object with property 'flavor'
- and value of your choice!
*
- Use the `.add()` method to add the objects `flavor1` and `flavor2`
- to `uniqueFlavors`
- Use the `.add()` method to add the `flavor1` object (again!) to
- the `uniqueFlavors` set
*/
Solution
let uniqueFlavors = new WeakSet()
let flavor1 = { flavor: "chocolate" }
let flavor2 = { flavor: "peanut butter" }
uniqueFlavors.add(flavor1)
uniqueFlavors.add(flavor2)
uniqueFlavors.add(flavor1)
console.log(uniqueFlavors)
What Went Well
- Your code should have a variable uniqueFlavors
- Your code should have a variable flavor1
- Your code should have a variable flavor2
- Your code should use the .add() method three times to add required items to the set
- The uniqueFlavors object should contain a "chocolate" flavor object
- The uniqueFlavors object should contain a custom flavor object
Feedback
Your answer passed all our tests! Awesome job!
Maps and Sets are both iterable which means you can loop over them
WeakMaps and WeakSets don't prevent objects from being garbage collected.
Maps are collections of key-value pairs,
{ key1: value1 richard: "is awesome" james: "is also cool" }whereas Sets are collections of unique values.
;[val1, val2, val3]You could say that Sets are to arrays as maps are to objects.
Sets :: Arrays Maps :: Objects
If Sets are similar to Arrays, then Maps are similar to Objects because Maps store key-value pairs similar to how objects contain named properties with values.
Essentially, a Map is an object that lets you store key-value pairs where both the keys and the values can be objects, primitive values, or a combination of the two.
To create a Map, simply type:
const employees = new Map() console.log(employees)Map {}
This creates an empty Map
employee
with no key-value pairs.
Unlike Sets, you can’t create Maps from a list of values; instead, you add key-values by using the Map’s
.set()
method.const employees = new Map() employees.set("[email protected]", { firstName: "James", lastName: "Parkes", role: "Content Developer" }) employees.set("[email protected]", { firstName: "Julia", lastName: "Van Cleve", role: "Content Developer" }) employees.set("[email protected]", { firstName: "Richard", lastName: "Kalehoff", role: "Content Developer" }) console.log(employees)Map {'[email protected]' => Object {...}, '[email protected]' => Object {...}, '[email protected]' => Object {...}}
The
.set()
method takes two arguments. The first argument is the key, which is used to reference the second argument, the value.To remove key-value pairs, simply use the
.delete()
method.employees.delete("[email protected]") employees.delete("[email protected]") console.log(employees)Map {'[email protected]' => Object {firstName: 'James', lastName: 'Parkes', role: 'Course Developer'}}
Again, similar to Sets, you can use the
.clear()
method to remove all key-value pairs from the Map.employees.clear() console.log(employees)Map {}
TIP: If you
.set()
a key-value pair to a Map that already uses the same key, you won’t receive an error, but the key-value pair will overwrite what currently exists in the Map. Also, if you try to.delete()
a key-value that is not in a Map, you won’t receive an error, and the Map will remain unchanged.The
.delete()
method returnstrue
if a key-value pair is successfully deleted from theMap
object, andfalse
if unsuccessful. The return value of.set()
is theMap
object itself if successful.
After you’ve built your Map, you can use the
.has()
method to check if a key-value pair exists in your Map by passing it a key.const members = new Map() members.set("Evelyn", 75.68) members.set("Liam", 20.16) members.set("Sophia", 0) members.set("Marcus", 10.25) console.log(members.has("Xavier")) console.log(members.has("Marcus"))false true
And you can also retrieve values from a Map, by passing a key to the
.get()
method.console.log(members.get("Evelyn"))75.68
You’ve created a Map, added some key-value pairs, and now you want to loop through your Map. Thankfully, you’ve got three different options to choose from:
- Step through each key or value using the Map’s default iterator
- Loop through each key-value pair using the new
for..of
loop- Loop through each key-value pair using the Map’s
.forEach()
method
Using both the
.keys()
and.values()
methods on a Map will return a new iterator object called MapIterator. You can store that iterator object in a new variable and use.next()
to loop through each key or value. Depending on which method you use, will determine if your iterator has access to the Map’s keys or the Map’s values.let iteratorObjForKeys = members.keys() iteratorObjForKeys.next()Object {value: 'Evelyn', done: false}
Use
.next()
to the get the next key value.iteratorObjForKeys.next()Object {value: 'Liam', done: false}
And so on.
iteratorObjForKeys.next()Object {value: 'Sophia', done: false}
On the flipside, use the
.values()
method to access the Map’s values, and then repeat the same process.let iteratorObjForValues = members.values() iteratorObjForValues.next()Object {value: 75.68, done: false}
Your second option for looping through a Map is with a
for..of
loop.for (const member of members) { console.log(member) }['Evelyn', 75.68] ['Liam', 20.16] ['Sophia', 0] ['Marcus', 10.25]
However, when you use a
for..of
loop with a Map, you don’t exactly get back a key or a value. Instead, the key-value pair is split up into an array where the first element is the key and the second element is the value. If only there were a way to fix this?
/*
- Using array destructuring, fix the following code to print the
- keys and values of the `members` Map to the console.
*/
const members = new Map()
members.set("Evelyn", 75.68)
members.set("Liam", 20.16)
members.set("Sophia", 0)
members.set("Marcus", 10.25)
for (const member of members) {
// console.log(key, value);
}
Solution
/*
- Using array destructuring, fix the following code to print the
- keys and values of the `members` Map to the console.
*/
const members = new Map()
members.set("Evelyn", 75.68)
members.set("Liam", 20.16)
members.set("Sophia", 0)
members.set("Marcus", 10.25)
for (const member of members) {
const [key, value] = member
console.log(key, value)
}
['Evelyn', 75.68]
['Liam', 20.16]
['Sophia', 0]
['Marcus', 10.25]
What Went Well
- Your code should have a members variable
- Your code should use destructuring
- members should be a Map
Feedback
Your answer passed all our tests! Awesome job!
Your last option for looping through a Map is with the
.forEach()
method.members.forEach((key, value) => console.log(key, value))['Evelyn', 75.68] ['Liam', 20.16] ['Sophia', 0] ['Marcus', 10.25]
Notice how with the help of an arrow function, the
forEach
loop reads fairly straightforward. For eachvalue
andkey
inmembers
, log thevalue
andkey
to the console.
TIP: If you’ve gone through the WeakSets section, then this section should be somewhat of a review. WeakMaps exhibit the same behavior as a WeakSets, except WeakMaps work with key-values pairs instead of individual items.
A WeakMap is just like a normal Map with a few key differences:
- a WeakMap can only contain objects as keys,
- a WeakMap is not iterable which means it can’t be looped and
- a WeakMap does not have a
.clear()
method.You can create a WeakMap just like you would a normal Map, except that you use the
WeakMap
constructor.let book1 = { title: "Pride and Prejudice", author: "Jane Austen" } let book2 = { title: "The Catcher in the Rye", author: "J.D. Salinger" } let book3 = { title: "Gulliver’s Travels", author: "Jonathan Swift" } const library = new WeakMap() library.set(book1, true) library.set(book2, false) library.set(book3, true) console.log(library)WeakMap {Object {title: 'Pride and Prejudice', author: 'Jane Austen'} => true, Object {title: 'The Catcher in the Rye', author: 'J.D. Salinger'} => false, Object {title: 'Gulliver’s Travels', author: 'Jonathan Swift'} => true}
...but if you try to add something other than an object as a key, you’ll get an error!
library.set("The Grapes of Wrath", false)Uncaught TypeError: Invalid value used as weak map key(…)
This is expected behavior because WeakMap can only contain objects as keys. Again, similar to WeakSets, WeakMaps leverage garbage collection for easier use and maintainability.
In JavaScript, memory is allocated when new values are created and is "automatically" freed up when those values are no longer needed. This process of freeing up memory after it is no longer needed is what is known as garbage collection.
WeakMaps take advantage of this by exclusively working with objects as keys. If you set an object to null, then you’re essentially deleting the object. And when JavaScript’s garbage collector runs, the memory that object previously occupied will be freed up to be used later in your program.
book1 = null console.log(library)WeakMap {Object {title: 'The Catcher in the Rye', author: 'J.D. Salinger'} => false, Object {title: 'Gulliver’s Travels', author: 'Jonathan Swift'} => true}
What makes this so useful is you don’t have to worry about deleting keys that are referencing deleted objects in your WeakMaps, JavaScript does it for you! When an object is deleted, the object key will also be deleted from the WeakMap when garbage collection runs.
This makes WeakMaps useful in situations where you want an efficient, lightweight solution for creating groupings of objects with metadata.
The point in time when garbage collection happens is dependent on a lot of different factors. Check out MDN’s documentation to learn more about the algorithms used to handle garbage collection in JavaScript.
Promises are used to handle asynchronous requests. It's like an IOU for a task. The computer will work on it and get back to you with the response.
Do this thing now, then notify me when it's done so I can pick up where I left off.
A JavaScript Promise is created with the new Promise constructor function -
new Promise()
. A promise will let you start some work that will be done asynchronously and let you get back to your regular work.When you create the promise, you must give it the code that will be run asynchronously. You provide this code as the argument of the constructor function:
new Promise(function() { window.setTimeout(function createSundae(flavor = "chocolate") { const sundae = {} // request ice cream // get cone // warm up ice cream scoop // scoop generous portion into cone! }, Math.random() - 2000) })This code creates a promise that will start in a few seconds after I make the request. Then there are a number of steps that need to be made in the
createSundae
function.
But once that's all done, how does JavaScript notify us that it's finished and ready for us to pick back up? It does that by passing two functions into our initial function. Typically we call these
resolve
andreject
.The function gets passed to the function we provide the Promise constructor - typically the word "resolve" is used to indicate that this function should be called when the request completes successfully. Notice the
resolve
on the first line:new Promise(function(resolve, reject) { window.setTimeout(function createSundae(flavor = "chocolate") { const sundae = {} // request ice cream // get cone // warm up ice cream scoop // scoop generous portion into cone! resolve(sundae) }, Math.random() - 2000) })Now when the sundae has been successfully created, it calls the
resolve
method and passes it the data we want to return - in this case the data that's being returned is the completed sundae. So theresolve
method is used to indicate that the request is complete and that it completed successfully.If there is a problem with the request and it couldn't be completed, then we could use the second function that's passed to the function. Typically, this function is stored in an identifier called "reject" to indicate that this function should be used if the request fails for some reason. Check out the
reject
on the first line:new Promise(function(resolve, reject) { window.setTimeout(function createSundae(flavor = "chocolate") { const sundae = {} // request ice cream // get cone // warm up ice cream scoop // scoop generous portion into cone! if (/- iceCreamConeIsEmpty(flavor) */) { reject(`Sorry, we're out of that flavor :-(`) } resolve(sundae) }, Math.random() - 2000) })So the
reject
method is used when the request could not be completed. Notice that even though the request fails, we can still return data - in this case we're just returning text that says we don't have the desired ice cream flavor.A Promise constructor takes a function that will run and then, after some amount of time, will either complete successfully (using the
resolve
method) or unsuccessfully (using thereject
method). When the outcome has been finalized (the request has either completed successfully or unsuccessfully), the promise is now fulfilled and will notify us so we can decide what to do with the response.
The first thing to understand is that a Promise will immediately return an object.
const myPromiseObj = new Promise(function(resolve, reject) { // sundae creation code })That object has a
.then()
method on it that we can use to have it notify us if the request we made in the promise was either successful or failed. The.then()
method takes two functions:
- the function to run if the request completed successfully
- the function to run if the request failed to complete
mySundae.then( function(sundae) { console.log(`Time to eat my delicious ${sundae}`) }, function(msg) { console.log(msg) self.goCry() // not a real method } )As you can see, the first function that's passed to
.then()
will be called and passed the data that the Promise'sresolve
function used. In this case, the function would receive thesundae
object. The second function will be called and passed the data that the Promise'sreject
function was called with. In this case, the function receives the error message "Sorry, we're out of that flavor :-(" that thereject
function was called with in the Promise code above.
Promises are an incredibly powerful addition to the language. A ton of both existing and future changes make use of them. So being able to work with and write JavaScript Promises is vital.
The reason is that Promises make it so much easier to do asynchronous code. With promises your code is gonna be easier to read, easier to write, and most importantly, it's going to be a lot easier to debug.
They're so important Udacity has actually created a standalone course dedicated just to JavaScript Promises.
Check out our Promises course where we'll take a deep dive into:
- JavaScript Promises
- how to handle returned data and errors
- build an app called Exoplanet Explorer that uses JavaScript Promises to fetch remote data asynchronously
Proxies are objects that stand in for other objects and handle all their interactions.
To create a proxy object, we use the Proxy constructor -
new Proxy();
. The proxy constructor takes two items:
- the object that it will be the proxy for
- an object containing the list of methods it will handle for the proxied object
The second object is called the handler.
The simplest way to create a proxy is to provide an object and then an empty handler object.
var richard = { status: "looking for work" } var agent = new Proxy(richard, {}) agent.status // returns 'looking for work'The above doesn't actually do anything special with the proxy - it just passes the request directly to the source object! If we want the proxy object to actually intercept the request, that's what the handler object is for!
The key to making Proxies useful is the handler object that's passed as the second object to the Proxy constructor. The handler object is made up of a methods that will be used for property access. Let's look at the
get
:
The
get
trap is used to "intercept" calls to properties:const richard = { status: "looking for work" } const handler = { get(target, propName) { console.log(target) // the `richard` object, not `handler`, not `agent` console.log(propName) // the name of the property the proxy is checking // (`agent` in this case) } } const agent = new Proxy(richard, handler) agent.status // logs out the richard object (not the agent object!) // and logs out the name of the property being accessed (`status`)In the code above, the
handler
object has aget
method (called a "trap" since it's being used in a Proxy). When the codeagent.status;
is run on the last line, because theget
trap exists, it "intercepts" the call to get the status property and runs theget
trap function.This will log out the target object of the proxy (the
richard
object) and then logs out the name of the property being requested (thestatus
property). And that's all it does! It doesn't actually log out the property!This is important - if a trap is used, you need to make sure you provide all the functionality for that specific trap.
If we wanted to actually provide the real result, we would need to return the property on the target object:
const richard = { status: "looking for work" } const handler = { get(target, propName) { console.log(target) console.log(propName) return target[propName] } } const agent = new Proxy(richard, handler) agent.status // (1)logs the richard object, (2)logs the property being accessed, // (3)returns the text in richard.statusNotice we added the
return target[propName];
as the last line of theget
trap. This will access the property on the target object and will return it.
Alternatively, we could use the proxy to provide direct feedback:
const richard = { status: "looking for work" } const handler = { get(target, propName) { return `He's following many leads, so you should offer a contract ASAP!` } } const agent = new Proxy(richard, handler) agent.status // returns the text `He's following many leads, so you should ...`With this code, the Proxy doesn't even check the target object, it just directly responds to the calling code.
So the
get
trap will take over whenever any property on the proxy is accessed. If we want to intercept calls to change properties, then theset
trap needs to be used!The
set
trap is used for intercepting code that will change a property. Theset
trap receives: the object it proxies the property that is being set the new value for the proxy.const richard = { status: "looking for work" } const handler = { set(target, propName, value) { // if the pay is being set, take 15% as commission if (propName === "payRate") { value = value - 0.85 } target[propName] = value } } const agent = new Proxy(richard, handler) agent.payRate = 1000 // set the actor's pay to $1,000 agent.payRate // $850 the actor's actual payIn the code above, notice that the
set
trap checks to see if thepayRate
property is being set. If it is, then the proxy (the agent) takes 15 percent off the top for her own commission! Then, when the actor's pay is set to one thousand dollars, since thepayRate
property was used, the code took 15% off the top and set the actualpayRate
property to850
;
So we've looked at the
get
andset
traps (which are probably the ones you'll use most often), but there are actually a total of 13 different traps that can be used in a handler!
- the get trap - lets the proxy handle calls to property access
- the set trap - lets the proxy handle setting the property to a new value
- the apply trap - lets the proxy handle being invoked (the object being proxied is a function)
- the has trap - lets the proxy handle the using
in
operator- the deleteProperty trap - lets the proxy handle if a property is deleted
- the ownKeys trap - lets the proxy handle when all keys are requested
- the construct trap - lets the proxy handle when the proxy is used with the
new
keyword as a constructor- the defineProperty trap - lets the proxy handle when defineProperty is used to create a new property on the object
- the getOwnPropertyDescriptor trap - lets the proxy handle getting the property's descriptors
- the preventExtenions trap - lets the proxy handle calls to
Object.preventExtensions()
on the proxy object- the isExtensible trap - lets the proxy handle calls to
Object.isExtensible
on the proxy object- the getPrototypeOf trap - lets the proxy handle calls to
Object.getPrototypeOf
on the proxy object- the setPrototypeOf trap - lets the proxy handle calls to
Object.setPrototypeOf
on the proxy objectAs you can see, there are a lot of traps that let the proxy manage how it handles calls back and forth to the proxied object.
Initially, it can be a bit unclear as to why proxies are all that beneficial when there are already getter and setter methods provided in ES5. With ES5's getter and setter methods, you need to know beforehand the properties that are going to be get/set:
var obj = { _age: 5, _height: 4, get age() { console.log(`getting the "age" property`) console.log(this._age) }, get height() { console.log(`getting the "height" property`) console.log(this._height) } }With the code above, notice that we have to set
get age()
andget height()
when initializing the object. So when we call the code below, we'll get the following results:obj.age // logs 'getting the "age" property' & 5 obj.height // logs 'getting the "height" property' & 4But look what happens when we now add a new property to the object:
obj.weight = 120 // set a new property on the object obj.weight // logs just 120Notice that a
getting the "weight" property
message wasn't displayed like theage
andheight
properties produced.With ES6 Proxies, we do not need to know the properties beforehand:
const proxyObj = new Proxy( { age: 5, height: 4 }, { get(targetObj, property) { console.log(`getting the ${property} property`) console.log(targetObj[property]) } } ) proxyObj.age // logs 'getting the age property' & 5 proxyObj.height // logs 'getting the height property' & 4All well and good, just like the ES5 code, but look what happens when we add a new property:
proxyObj.weight = 120 // set a new property on the object proxyObj.weight // logs 'getting the weight property' & 120See that?!? A
weight
property was added to the proxy object, and when it was later retrieved, it displayed a log message!So some functionality of proxy objects may seem similar to existing ES5 getter/setter methods. But with proxies, you do not need to initialize the object with getters/setters for each property when the object is initialized.
A proxy object sits between a real object and the calling code. The calling code interacts with the proxy instead of the real object. To create a proxy:
- use the
new Proxy()
constructor
- pass the object being proxied as the first item
- the second object is a handler object
- the handler object is made up of 1 of 13 different "traps"
- a trap is a function that will intercept calls to properties and let you run code
- if a trap is not defined, the default behavior is sent to the target object
Proxies are a powerful new way to create and manage the interactions between objects.
Whenever a function is invoked, the JavaScript engine starts at the top of the function and runs every line of code until it gets to the bottom. There's no way to stop the execution of the function in the middle and pick up again at some later point. This "run-to-completion" is the way it's always been:
function getEmployee() { console.log("the function has started") const names = [ "Amanda", "Diego", "Joe", "James", "Kagure", "Kavita", "Orit" ] for (const name of names) { console.log(name) } console.log("the function has ended") } getEmployee()Running the code above produces the following output to the console:
the function has started Amanda Diego Joe James Kagure Kavita Orit the function has ended
But what if you want to print out the first 3 employee names then stop for a bit, then, at some later point, you want to continue where you left off and print out more employee names. With a regular function, you can't do this since there's no way to "pause" a function in the middle of its execution.
If we do want to be able to pause a function mid-execution, then we'll need a new type of function available to us in ES6 - generator functions! Let's look at one:
function* getEmployee() { console.log("the function has started") const names = [ "Amanda", "Diego", "Joe", "James", "Kagure", "Kavita", "Orit" ] for (const name of names) { console.log(name) } console.log("the function has ended") }Notice the asterisk (i.e.
*
) right after thefunction
keyword? That asterisk indicates that this function is actually a generator!Now check out what happens when we try running this function:
getEmployee(); // this is the response I get in Chrome: getEmployee {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}...umm, what? Where's the "the function has started" text from the top of the function? And why didn't we get any names printed to the console? Those are good questions, but first, a quiz.
Which of the following are valid generators? Pay attention to the placement of the asterisk.
If you're not sure, try running them in your browser's console.
function* names() { /* */ }
function * names() { /* */ }
function *names() { /* */ }
Solution
My first try was just the first option (which is actually the community consensus syntax), but I realized it would be all of them and got it on my second try.
function* names() { /* */ }
function * names() { /* */ }
function *names() { /* */ }
The asterisk of the generator can actually be placed anywhere between the
function
keyword and the function's name. So all three of these are valid generator declarations!The community has coalesced into having the asterisk appear right next to the
function
keyword (i.e.function* name() { … }
). But there others that recommend having the asterisk touch the function's name instead. So it's important to realize that the asterisk indicates that it is a generator but that the placement of the asterisk is not important.
WARNING: We looked at iteration in a previous section, so if you're rusty on it, better check it out again because they're resurfacing here with generators!
When a generator is invoked, it doesn't actually run any of the code inside the function. Instead, it creates and returns an iterator. This iterator can then be used to execute the actual generator's inner code.
const generatorIterator = getEmployee() generatorIterator.next()Produces the code we expect:
the function has started Amanda Diego Joe James Kagure Kavita Orit the function has ended
Now if you tried the code out for yourself, the first time the iterator's
.next()
method was called it ran all of the code inside the generator. Did you notice anything? The code never paused! So how do we get this magical, pausing functionality?
The
yield
keyword is new and was introduced with ES6. It can only be used inside generator functions.yield
is what causes the generator to pause. Let's add yield to our generator and give it a try:function* getEmployee() { console.log("the function has started") const names = [ "Amanda", "Diego", "Joe", "James", "Kagure", "Kavita", "Orit" ] for (const name of names) { console.log(name) yield } console.log("the function has ended") }Notice that there's now a
yield
inside thefor..of
loop. If we invoke the generator (which produces an iterator) and then call.next()
, we'll get the following output:const generatorIterator = getEmployee() generatorIterator.next()Logs the following to the console:
the function has started Amanda
It's paused! But to really be sure, let's check out the next iteration:
generatorIterator.next()Logs the following to the console:
Diego
So it remembered exactly where we left off! It took the next item in the array (Diego), logged it, and then hit the
yield
again, so it paused again.Now pausing is all well and good, but what if we could send data from the generator back to the "outside" world? We can do this with
yield
.
Instead of logging the names to the console and then pausing, let's have the code "return" the name and then pause.
function* getEmployee() { console.log("the function has started") const names = [ "Amanda", "Diego", "Joe", "James", "Kagure", "Kavita", "Orit" ] for (const name of names) { yield name } console.log("the function has ended") }Notice that now instead of
console.log(name)
; that it's been switched toyield name;
. With this change, when the generator is run, it will "yield" the name back out to the function and then pause its execution. Let's see this in action:const generatorIterator = getEmployee() let result = generatorIterator.next() result.value // "Amanda" generatorIterator.next().value // "Diego" generatorIterator.next().value // "Farrin"
How many times will the iterator's .next() method need to be called to fully complete/"use up" the udacity generator function below:
function* udacity() {
yield "Richard"
yield "James"
}
- 0 times
- 1 time
- 2 times
- 3 times
3 times
It will be called one more time than there are
yield
expressions in the generator function.The first call to
.next()
will start the function and run to the firstyield
. The second call to.next()
will pick up where things left off and run to the secondyield
. The third and final call to.next()
will pick up where things left off again and run to the end of the function.
So we can get data out of a generator by using the
yield
keyword. We can also send data back into the generator, too. We do this using the.next()
method:function* displayResponse() { const response = yield console.log(`Your response is "${response}"!`) } const iterator = displayResponse() iterator.next() // starts running the generator function iterator.next("Hello Udacity Student") // send data into the generator // the line above logs to the console: Your response is "Hello Udacity Student"!Calling
.next()
with data (i.e..next('Richard')
) will send data into the generator function where it last left off. It will "replace" the yield keyword with the data that you provided.So the
yield
keyword is used to pause a generator and used to send data outside of the generator, and then the.next()
method is used to pass datainto
the generator. Here's an example that makes use of both of these to cycle through a list of names one at a time:function* getEmployee() { const names = [ "Amanda", "Diego", "Joe", "James", "Kagure", "Kavita", "Orit" ] const facts = [] for (const name of names) { // yield *out- each name AND store the returned data into the facts array facts.push(yield name) } return facts } const generatorIterator = getEmployee() // get the first name out of the generator let name = generatorIterator.next().value // pass data in *and* get the next name name = generatorIterator.next(`${name} is cool!`).value // pass data in *and* get the next name name = generatorIterator.next(`${name} is awesome!`).value // pass data in *and* get the next name name = generatorIterator.next(`${name} is stupendous!`).value // you get the idea name = generatorIterator.next(`${name} is impressive!`).value name = generatorIterator.next(`${name} is stunning!`).value name = generatorIterator.next(`${name} is awe-inspiring!`).value // pass the last data in, generator ends and returns the array const positions = generatorIterator.next(`${name} is magnificent!`).value // displays each name with description on its own line positions.join("\n")
What will happen if the following code is run?
function* createSundae() {
const toppings = []
toppings.push(yield)
toppings.push(yield)
toppings.push(yield)
return toppings
}
var it = createSundae()
it.next("hot fudge")
it.next("sprinkles")
it.next("whipped cream")
it.next()
- The
toppings
array will haveundefined
as its last item - An error will occur
- The generator will be paused, waiting for it's last call to
.next()
Solution
The toppings
array will have undefined
as its last item
Because the first call to
.next()
passes in some data. But that data doesn't get stored anywhere. The last call to.next()
should have some data since it's being yielded into the last call totoppings.push()
.
Additional notes from James Priest:
Remember that the first call to
.next()
will initiate the generator which will stop at the firstyield
. The second call to.next()
is the call that will supply that `yield with the data.Count how many yields there are and when data is passed into each of the calls to
.next()
.
Generators are a powerful new kind of function that is able to pause its execution while also maintaining its own state. Generators are great for iterating over a list of items one at a time so you can handle each item on its own before moving on to the next one. You can also use generators to handle nested callbacks. For example, let's say that an app needs to get a list of all repositories and the number of times they've been starred. Well, before you can get the number of stars for each repository, you'd need to get the user's information. Then after retrieving the user's profile the code can then take that information to find all of the repositories.
Generators will also be used heavily in upcoming additions to the JavaScript language. One upcoming feature that will make use of them is async functions.
Informative and helpful lesson.
Lesson 3.26 on generators mentions async functions. Async/await was introduced in ES2017. See my notes in ajax-3-fetch.md.