JavaScript & ES6
Hi, I'm Tom
- Teaching fellow at Fullstack Academy
- I'm all about JavaScript!
- Fun facts
- Was an English major in college
- Grew up in South Jersey
- Favorite animal: otters
What You're Going to Learn
- What is ES6?
- Some of the most useful new features of ES6
- How to use ES6 in your projects
What You Should Have
- A basic to intermediate understanding of JavaScript, Node and npm
- Your laptop (if you want to follow along)
Questions
- Please post them to the comment thread at this event on meetup.com:
- http://www.meetup.com/BeginnerProgrammers/events/229236906/
- I'll answer questions with any time we have left at the end
- Any questions I don't have time for, I'll answer by responding to the comment on meetup.com in the next several days
Node
-
"
Node.js® is a JavaScript runtime built on
Chrome's V8 JavaScript engine
. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient. Node.js' package ecosystem,
npm
, is the largest ecosystem of open source libraries in the world"
- JavaScript's original environment is the browser, but Node brings JavaScript server-side
- We'll be using Node for this workshop so that we can work exclusively with JavaScript, and not worry about HTML/CSS
Let's get started
So What's ES6, Anyway?
- ECMAScript 6, also known as ECMAScript 2015, is the latest version of the ECMAScript standard. ES6 is a significant update to the language, and the first update to the language since ES5 was standardized in 2009.
- ECMAScript is a language specification that JavaScript follows (as do other languages like JScript, ActionScript, and more).
- Standards like ECMAScript are pretty great - imagine having to learn a whole different dialect of JavaScript depending on which browser your code was running in. Thanks, ECMAScript!
Browser Compatibility
- Features of the ES6 specification are not available in every browser -
why is that?
- JavaScript is compiled and executed at runtime by the environment's JavaScript interpreter (a.ka. JavaScript engine)
- The most common environment for JavaScript is a browser - most browsers ship with different JS engines, claiming various advantages
- The browser wars live on ¯\_(ツ)_/¯
So what can we do?
Transpilation
- Transpilers evaluate and compile your JavaScript into browser-compatible ES5 JavaScript
- Implemented during a build process that runs on your code before deployment
- The two most popular are Babel and Traceur
- For this workshop, we'll be using Babel
Problems with transpilation
- Runtime errors can be more difficult to debug
- Potential decrease in performance
- Not every new feature can be transpiled
- Why should browsers bother to support ES6 if you're just going to compile to ES5-compliant code anyway?
- Left pad!?!
Some Opinions
- For most business purposes, the end-user experience should always be your number one priority
- It's also really important to make sure that your codebase is clear and understandable (so that it's easy to onboard new developers, or get open source contributions), and scalable/extensible (so that it's easy to grow)
- Tools for transpilation will only get better in the future, and support for new features in JavaScript are only going to increase
Some Opinions
So:
- Don't be afraid to transpile your code and use new features
- I would rather have my codebase be up-to-date and ready for wider support in the future
- Address performance problems as they come up - a few milliseconds here or there is not worth the loss in productivity that comes from writing your entire application using older patterns
Without further ado
What we'll be building
- A todo list, of course! Only this todo list will be for concerts that we want to attend!
- We'll implement this as a simple node application using the Bandsintown API that will help us find concerts to go to, allow us to add them to a schedule, and then view our schedule
- Starting point:
- git clone https://github.com/tmkelly28/es6-starting-point.git
- cd es6-starting-point (or whatever you decide to call it)
- npm install
- npm start
Goal
- When we start the application, we should see a list of concerts that we're going to
- If we type the command 'search', we should be able to search for a new concert near us by artist name
- For every concert near us for that artist, we should be prompted to add the concert to our list
- We can see the schedule again by typing 'view schedule'
- This data should persist in a .json file for us to use again and again
- If we type the command 'search', we should be able to search for a new concert near us by artist name
Let's get crackin'!
Your project
/ project_root
/ app
index.js
/ node_modules
...
.babelrc
.gitignore
index.js
package.json
.babelrc
{
"presets": ["es2015", "stage-0"]
}
index.js
'use strict';
/*
* Only our entry point needs to use the 'require' pattern
* Everything after babel-register will be able to use ES6!
*/
require("babel-register");
require('babel-polyfill');
require('./app');
app/index.js
'use strict';
console.log('Hello ES6!');
Const & Let
Scope
- Variables are declared using the var keyword
- Scope is the space where variables you've declared are available for use
- Scopes inherit from each other with a parent-child hierarchy
- Attempting to use a variable that has not been declared within a function's scope, or the scope of one of its parents will cause a ReferenceError
- Using var to declare a variable makes that variable available to the entire function it's declared within - "function scoping"
In scope
// we've declared foo in the global scope - all functions in your program have access
var foo = 'foo';
function bar () {
var myBar = 'my bar';
console.log(foo) // foo
// this is fine, because foo is declared in the function bar's parent scope
}
console.log(foo) // foo
console.log(myBar) // ReferenceError!
Block scope v. Function scope
- Const and let set a tighter scoping than var - "block" scoping
- Rather than being available to an entire function's scope, variables declared with const and let can be restricted to the scope of a code block
- For loops, if and while all create code blocks
- You can also create arbitrary code blocks
Blocked Out
function fooWithVar (bar) {
if (bar) {
var baz = 'baz';
}
console.log(baz);
}
foo('baz') // baz
function fooWithLet (bar) {
if (bar) {
let baz = 'baz';
}
console.log(baz);
}
fooWithVar('baz'); // 'baz'
fooWithLet('baz'); // ReferenceError: baz is not defined
Const
- Variables declared with "const" cannot be reassigned
- Note that you can still mutate the content of the variable (ex. if you assign the variable to an object, you can still modify the object - you just can't assign that variable to something else, like another object)
Const-ant Comment
// index.js
// we'll use the core readline library to write our CLI
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
function main () {
console.log("Welcome to ConcertGoer!");
rl.question('Ready to learn ES6? ', function (answer) {
console.log('You said: ', answer);
rl.close();
});
}
// invoke main to kick off our app
main();
Why Use Let and Const?
- Safer, more descriptive way to declare your data
- Enforces good practice
Why Continue using Var?
- Maybe browser compatibility, if you're in an environment where you can't use Babel
- Otherwise...you can actually replace var with const and let entirely. The Airbnb javascript style guide actually recommends doing just that!
Arrow Functions
"This"
- This can be a tricky concept to grasp in JavaScript
- Within a function,
this is defined based on the execution context of a function
- If a function is executed as a method on an object, this will refer to the object itself, so that you can intuitively access other properties/methods on the object
What if I told you...
// say we have an object called Neo...
const Neo = {};
// and we also have a function
const bendSpoon = function () {
console.log(this.spoon);
};
// let's give our object a property and a method
Neo.spoon = 'spoon';
Neo.bendSpoon = bendSpoon;
// the execution context of bendSpoon is Neo, so this refers to Neo
Neo.bendSpoon(); // spoon
// the execution context of bendSpoon is the global context - there is no spoon
bendSpoon(); // undefined
Mr. Anderson...
const Neo = {};
const agentSmiths = ['Smith1', 'Smith2', 'Smith3'];
Neo.kick = function (agent) {
console.log('Neo kicked ', agent);
};
Neo.kickAgentSmiths = function () {
agentSmiths.forEach(function (agent) {
this.kick(agent);
});
};
Neo.kickAgentSmiths(); // TypeError: this.kick is not a function
// How can we help Neo?
Arrow functions
- A more concise way to write function expressions
-
Reminder:
- var fn = function () {} === function expression
- function fn () {} === function declaration
-
Reminder:
- The big difference: the body of arrow functions do not get their own dynamic
this value. Instead, arrow functions have lexical
this: their
this context is the enclosing context
- This solves some issues you may have experienced with callback functions in methods like Array.prototype.forEach
- However, with great power comes great responsibility....
Syntax
/* if you have one argument and don't use brackets
you can just write a snappy one-liner
This is called the 'concise body'
*/
arr.map(i => i * 2);
// multiple arguments go in parentheses
arr.reduce((prev, next) => prev + next);
// no arguments are just an empty ()
// note that you can use this for any function expression, not just callbacks!
let foo = () => console.log('bar');
/* if you need more than one line, then you need to use brackets
and can't omit the return statement
This is called the 'block body'
*/
let baz = (x) => {
if (x > 1) return x;
else return 1;
}
Using Arrow Functions
const Neo = {};
const agentSmiths = ['Smith1', 'Smith2', 'Smith3'];
Neo.kick = function (agent) {
console.log('Neo kicked ', agent);
};
Neo.kickAgentSmiths = function () {
agentSmiths.forEach(agent => this.kick(agent));
};
Neo.kickAgentSmiths();
/*
Neo kicked Smith1
Neo kicked Smith2
Neo kicked Smith3
*/
Don't overdo it
const Neo = {};
const agentSmiths = ['Smith1', 'Smith2', 'Smith3'];
Neo.kick = function (agent) {
console.log('Neo kicked ', agent);
};
Neo.kickAgentSmiths = () => {
agentSmiths.forEach(agent => this.kick(agent));
};
Neo.kickAgentSmiths(); // TypeError: the Matrix has you
// index.js
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
function main () {
rl.question('What would you like to do > ', command => {
switch (command) {
case 'search':
// we'll fill this in next...
break;
default:
console.log('Not a valid command');
main();
}
});
}
Refactoring
// index.js
const http = require('http'); // let's pull in http to talk to Bandisintown
// ....
case 'search':
rl.question('Enter an artist to search for > ', bandName => {
let responseBody = '';
http.get({
host: 'api.bandsintown.com',
// at the end there, where it says ES6_TEST - make up something of your own!
path: '/artists/' + bandName.replace(/\s/g, '%20') + '/events.json?app_id=ES6_TEST'
}, res => {
res.on('data', data => responseBody += data);
res.on('end', () => {
console.log(JSON.parse(responseBody));
main();
});
}) // CAREFUL! no semicolon here
.on('error', error => console.error(error));
});
// ...
Using => with http
// index.js
// ....
case 'search':
rl.question('Enter an artist to search for > ', function (bandName) {
let responseBody = '';
http.get({
host: 'api.bandsintown.com',
path: '/artists/' + bandName.replace(/\s/g, '%20') + '/events.json?app_id=ES6_TEST'
}, res => {
res.on('data', function (data) {
responseBody += data;
});
res.on('end', function () {
console.log(JSON.parse(responseBody));
main();
});
})
.on('error', error => console.error(error))
});
// ...
Without =>
Why Use Arrow Functions?
- They look rad and can help turn your shorter functions into clean one-liners
- Intuitive this context eliminates the need to use self = this in some callbacks, which is better for OO style
When to be careful
- Whenever this is involved - make sure you know what kind of behavior you're getting
Yay ES6!
String Interpolation
String Interpolation
- Backticks (`) making working with strings a joy again!
- Using backticks, you can compose a string across multiple lines, without needing to concatenate them using the addition operator
- You can also interpolate variables on the current scope into backtick strings using ${}
- You can even create "tagged" template strings - template strings that you can modify the output for using a function - we won't cover this because it's fairly advanced, but check it out afterwards!
What makes you tick?
console.log(`
Hello
World!
`);
/*
Hello
World
*/
// Returns and spaces/tabs are included in the console output!
function stayClassy (city) {
console.log(`Stay classy, ${city}`);
}
stayClassy('San Diego'); // => Stay classy, San Diego
// index.js
// ....
case 'search':
rl.question('Enter an artist to search for > ', bandName => {
let responseBody = '';
http.get({
host: 'api.bandsintown.com',
path: `/artists/${bandName.replace(/\s/g, '%20')}/events.json?app_id=ES6_TEST`
}, res => {
res.on('data', data => responseBody += data);
res.on('end', () => {
let events = JSON.parse(responseBody);
console.log(`${bandName} has ${events.length} concerts coming up`);
main();
});
}) // CAREFUL! no semicolon here
.on('error', error => console.error(error));
});
// ...
Let's refactor...
Import/Export
Modules
- JavaScript has not had a built-in module loader until ES6
- The two main options up until now: CommonJS (using 'require', which Node implements, and AMD (Asynchronous Module Definition)
- CommonJS - generally used server-side, optimized for synchronous loading
- AMD - generally used client-side, optimized for asynchronous loading
ES6 Modules
- Optimized for both synchronous and asynchronous loading - can be used on client or server
- Static structure - because the compiler can tell what the imports/exports are, it can perform various performance optimizations
- Supports cyclic dependencies
- Note: Node does not yet have a native solution for this - after transpilation, we are still using CommonJS
Import/Export
/* export.js */
// you can export multiple values
export const foo = 'foo';
export function bar () {
console.log('bar');
}
// you can also specify a default export
export default function baz () {
console.log('baz');
}
/* import.js */
// we can import default values by name
import baz from './export';
// we can specify which exports to import using brackets
import {foo, bar} from './export';
// we can also import all by assigning a namespace
import * as foobar from './export';
Let's modularize
/* index.js */
import * as readline from 'readline';
import search from './search';
// we'll create a new file, search.js, in our app folder
Let's modularize
/* search.js */
import * as http from 'http';
export default function search (rl, done) {
rl.question('Enter an artist to search for > ', bandName => {
let responseBody = '';
http.get({
host: 'api.bandsintown.com',
path: `/artists/${bandName.replace(/\s/g, '%20')}/events.json?app_id=ES6_TEST`
}, res => {
res.on('data', data => responseBody += data);
res.on('end', () => {
let events = JSON.parse(responseBody);
console.log(`${bandName} has ${events.length} concerts coming up`);
done();
});
})
.on('error', error => {
console.error(error);
done();
});
});
}
Let's modularize
/* index.js */
import * as readline from 'readline';
import search from './search';
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log("Welcome to ConcertGoer!");
function main () {
rl.question('What would you like to do > ', command => {
switch (command) {
case 'search':
search(rl, main);
break;
default:
console.log('Not a valid command');
main();
}
});
}
main();
Asynchronous Control Flow
Don't worry...
- This is the hard stuff - if this is your first time encountering this, don't worry about understanding everything
Promises
Async
- JavaScript is single-threaded - that means that only one line of code can execute at a time
- Each instruction must complete before the next instruction can execute
- For this reason, operations that require getting resources from the "outside" (file I/O, network requests, etc) are asynchronous - they don't block the thread
- Here's a story to tell you how this works
Cast of Characters
- The JavaScript Thread - this is our single thread of JavaScript, where only one thing can happen a time.
- The Environment - like browsers, or in our case, Node. The environment has access to other processes that can do things like access the internet, read from your filesystem or database, etc.
- The Event Loop - waits for messages from the Javascript Environment, and when the JavaScript Thread is clear, it will give its message to the JavaScript Thread to execute
Our story
fs.readFile('file.txt', 'utf8', function (err, data) {
if (err) throw err;
console.log(data);
});
Once upon a time...
- JavaScript Thread: I want to read file.txt. Hey, Environment, go get me file.txt, and take this function with you. When you get the file, put it in the second parameter, and it'll know to log it to the console. If you mess up, put the error in the first parameter.
- Environment: Okie-dokie!
- JavaScript Thread: Cool, I'm going to keep going on my merry way.
Sometime later...
- Environment: Phew, got that file! It took longer than I thought! Hey, Event Loop, I'm done reading that file. Here's the file and the function that the JavaScript Thread asked for.
- Event Loop: Cool! I'll give this to the JavaScript Thread as soon as it's free!
- Event Loop: Hey, JavaScript Thread? Are you free?
- JavaScript Thread: Yup, I don't have anything left to do
- Event Loop: Here's that function you wanted the Environment to call you back with. It's got the data you want.
- JavaScript Thread: Cool, I'll invoke the function right now!
Async Behavior
/* index.js */
// we head on down the main thread
console.log('a')
/*
fs.readFile is always ASYNC
*/
fs.readFile('file.txt', 'utf8', function (err, data) {
if (err) throw err;
console.log(data)
});
// back on the main thread...
console.log('c');
/*
When this logs to the console, we'll see
'a'
'c'
'I am the contents of file.txt!'
*/
The Problem with Callbacks
-
In JavaScript, the way of dealing with async behavior has been through callbacks
- When a function does something asynchronous, we give it another function to call us back with when the asynchronous behavior completes
- This creates several problems
- Loss of control/trust - its up to somebody else's code to call us back
- Error handling - we need to handle each error on our own
- The Pyramid of Doom - callbacks do not compose well, and we wind up stacking callbacks on callbacks on callbacks
Highway to Hell
foo(function (err1, data) {
if (err1) throw err1;
bar(data, function (err2, moarData) {
if (err2) throw err2;
baz(moarData, function (err3, evenMoarData) {
if (err3) throw err3;
killMeNow(/* ... */)
});
});
});
Save us!
Promises
- MDN: The Promise object is used for deferred and asynchronous computations. A Promise represents an operation that hasn't completed yet, but is expected in the future
- A promise is an object that waits to be resolved by the result that an async operation returns
- If the async operation returns an error, the promise is rejected
- The promise handles being resolved or rejected with its two methods...
.then
- Promise.prototype.then has two parameters
- Each parameter accepts a function
- The function in the first parameter is a success callback - it will be called with the data when the promise resolves
- The function in the second parameter is an error callback - it will be called with the error if the promise rejects
ourPromise
.then(successHandlingFunction, errorHandlingFunction)
Promises in action
// Let's say that fetch does something async, like make an http request to a certain api
// Instead of using a callback, fetch will return a Promise!
// With this http request, let's say we want an array of events, just like the one in our application
let promiseForEvents = fetch('http://api/artists/events')
promiseForEvents
.then(
function (events) {
// if the request is successful, the success handler will get us the events
console.log(events);
},
function (error) {
// if the request errors, the error handler will log an error
console.log(error)
});
.catch
- Promise.prototype.catch is similar to Promise.prototype.then - it takes a callback function, but this callback function will only handle errors if the promise is rejected.
- This is nice, because then we don't even need to use the second parameter in .then if we don't want, we can just use .then for successes, and .catch for fails
- Catching errors is also very powerful when we chain promises together - a pattern that we're about to see...
ourPromise
.catch(errorHandlingFunction)
Catch me if you can
let promiseForEvents = fetch('http://api/artists/events')
promiseForEvents
.then(function (events) {
// if the request is successful, calling .then will get us the events
console.log(events);
})
.catch(function (error) {
// if the request is unsuccessful, the error will be caught by the .catch
console.log(error)
});
// we can simplify this syntax further
fetch('http://api/artists/events')
.then(events => console.log(events)) // watch out! no semicolon, we're chaining!
.catch(error => console.error(error));
Chaining Promises
// chaining promises
fetch('http://artist_api/artist/kanye')
.then(kanyeInfo => {
// we use the info we got back to make another asynchronous call
// fetch returns another promise, so we can keep using .then!
return fetch(`http://other_api/events_by_artist/${kanyeInfo.id}`)
})
.then(kanyeEvents => console.log("Here are Kanye's events: ", kanyeEvents)
.catch(error => console.error(error));
// If either of those calls err'd, we would catch it here! Wow!
Promises vs. callbacks
- Promises allow us to describe asynchronous behavior in a synchronous way
- Promises invert control back to our program, instead of the program calling our callback
- Promises make error handling easier and more dependable
Promisify-ing
- We can easily turn any asynchronous function that takes a callback into a function that returns a Promise
- We use the Promise constructor function
- The Promise constructor takes a callback function with two parameters
- resolve: a function that will receive the result we want the Promise to resolve to if successful
- reject: a function that will receive the error we get if something goes wrong
Promisifying our prompt
function prompt (ques) {
return new Promise(function (resolve, reject) {
rl.question(ques, function(command) {
if (!command) reject('No command given');
resolve(command);
});
});
}
// we can also use arrow functions!
function prompt (ques) {
return new Promise((resolve, reject) => {
rl.question(ques, command => {
if (!command) reject('No command given');
resolve(command);
});
});
}
// now, here's how we use our new prompt
prompt('What would you like to do > ')
.then(command => {
switch (command) {
// etc...
}
})
.catch(err => console.error(err));
prompt.js
/* Create a new file in /app called prompt.js */
'use strict';
import * as readline from 'readline'; // now, our prompt file will control readline
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
export default function prompt (ques) {
return new Promise((resolve, reject) => {
rl.question(ques, answer => {
// ternary control operators are just shorthand for if...else
answer ? resolve(answer) : reject('No command given');
});
});
}
index.js
/* index.js */
'use strict';
import prompt from './prompt'; // import prompt instead!
import search from './search';
console.log("Welcome to ConcertGoer!");
function main () {
prompt('What would you like to do > ')
.then(command => {
switch (command) {
case 'search':
search(main); // we no longer have to pass rl!
break;
default:
console.log('Not a valid command');
main();
}
})
.catch(error => {
console.error(error);
main();
});
}
main();
fetch.js
/* create a new file in /app called fetch.js */
'use strict';
import * as http from 'http';
export default function fetch (bandName) {
return new Promise((resolve, reject) => {
let responseBody = '';
http.get({
host: 'api.bandsintown.com',
path: `/artists/${bandName.replace(/\s/g, '%20')}/events.json?app_id=ES6_TEST`
}, res => {
res.on('data', data => responseBody += data);
res.on('end', () => {
resolve(JSON.parse(responseBody)); // resolve our JSON!
});
res.on('error', error => reject(error)); // reject our error
});
});
}
search.js
/* search.js */
'use strict';
import fetch from './fetch';
import prompt from './prompt'; // new imports!
export default function search (done) { // no more rl!
let _bandName; // we'll cache bandName here
prompt('Enter an artist to search for > ')
.then(bandName => {
_bandName = bandName;
return fetch(bandName);
})
.then(events => {
console.log(`${_bandName} has ${events.length} concerts coming up`);
done();
})
.catch(error => {
console.error(error);
done();
});
};
// This is SO much more readable than before!
Recap
- Promises invert control flow back to you, the developer
- Promises have a then and a catch method
- You can create a promise using the Promise constructor, which takes a callback function with a resolve and a reject
- Chaining promises is a powerful pattern for dealing with multiple asynchronous operations
Mind.Blown.
Iterators/Iterables
Iterator protocol
- MDN: An object is an iterator when it knows how to access items from a collection one at a time, while keeping track of its current position within that sequence
- An iterator is an object that knows about a specific collection of data (ex. items in an array)
- An iterator has a method that, when invoked, will iterate to the next item in that collection and return it
Iterating
- An iterator has a next method
- The next method returns an object that looks like this:
- {value: someValue, done: boolean}
- value is the value of the current item in the collection
- done is a boolean for whether the collection is complete or not
- Each call to next moves the iterator on to the next item in its collection
- So, how is an iterator associated with a collection?
let array = [1, 2, 3];
let iteratorForArray = {};
iteratorForArray.next()
// {value: 1, done: false}
iteratorForArray.next()
// {value: 2, done: false}
iteratorForArray.next()
// {value: 3, done: false}
iteratorForArray.next()
// {value: undefined, done: true}
Iterable protocol
- MDN: An object is an iterable if it defines its iteration behavior
- Put simply: an object/collection is an iterable if it contains a special property (called Symbol.iterator) that returns its iterator
- Strings and Arrays are iterables (built-in iterables), as are several other new collection types in ES6
- There are several consumers of iterables, such as the new for...of loop
Using iterators
'use strict';
// an array is a built-in iterable
let numberArray = [1, 2, 3];
// Symbol.iterator is the special property that contains a function that returns an iterator
let it = numberArray[Symbol.iterator]();
// we can then use the iterator to manually iterate through the array's values
it.next(); // Object { value: 1, done: false }
it.next(); // Object { value: 2, done: false }
it.next(); // Object { value: 3, done: false }
it.next(); // // Object { value: undefined, done: true }
let stringArray = ['a', 'b', 'c'];
// the for...of loop consumes an iterable
for (let ch of stringArray) {
console.log(ch) // a b c
}
Roll your own
let strArray = ['a', 'b', 'c'];
strArray[Symbol.iterator] = function () {
let idx = 0,
collection = this,
len = this.length;
return {
next: function () {
if (idx < len) {
let value = collection[idx];
idx++;
return {
value: value + '!',
done: false
};
} else {
return {
value: undefined,
done: true
};
}
}
};
};
for (let ch of strArray) {
console.log(ch); // a! b! c!
}
Displaying our event data
/* search.js */
.then(events => {
events = events.filter(event => event.venue.city === 'New York');
console.log(`\nUpcoming concerts for ${_bandName} in New York`);
for (let event of events) {
console.log(`
${event.artists[0].name}: ${new Date(event.datetime)}
${event.venue.name} ${event.venue.city}, ${event.venue.region}
`);
}
done();
})
/*
A bit anticlimactic? Don't worry - we'll use iterators more in a bit!
*/
Recap
- Our events Array is an iterable, with a Symbol.iterator method
- Symbol.iterator returns an iterator, which the for...of loop uses under the hood to iterate through the values in the array
- However, the iterator that the events Array returns is accessible to use - we could use it the same way the for...of loop does
- In fact, there is a special kind of function that creates iterators, just so that we can use them...
Generators
Our app so far...
- We can search for artists by name and list all of their upcoming concerts in New York
- We still need a way to add these concerts to a schedule that we can save for later
- We're using a command line interface - how are our users going to add concerts to the schedule?
Some ideas...
- Well, right now we can list out all of the concerts:
- We could just have users type in the name of the venue!
- Hrm, but what if a band is playing the same venue on two nights?
- We could have the user type in the date?
-
Well, we could theoretically still have two shows on the same date
- Argh, maybe the time, too, then? Why do you have to be so difficult?
-
Well, we could theoretically still have two shows on the same date
Let's do this instead
- We'll show the user each concert, one after another, and have the user decide whether to go to the concert or not before moving on to the next
- This is actually fairly difficult
- We don't know how many concerts we're going to get
- We need to do something asynchronous (run the prompt) for each concert - simply looping over the concerts won't cut it!
- If only we didn't have keep looping through each concert...if only there was some way to hit the pause button
Generators
- A generator is a new type of function in ES6 that can maintain its own state - that is, it can pause, and then resume
- Generator functions always return an iterator (that is, an object which adheres to the Iterator Protocol)
- In the generator function body, the yield keyword signifies a "breakpoint" in the iterator's iteration. Whenever you call the next method, the function executes up to the next yield keyword, and yields that value to the next invocation
Generation station
// we declare a generator using an asterisk
function* myGenerator (n) {
// we yield values with the yield keyword
yield n;
n++;
// we can yield as many times as we want
// each yield is a "pause" in the generator function's execution
yield n;
}
/*
invoking a generator function returns an iterator!
we've passed in a value for the parameter "n", but
no other code in the generator has executed yet
*/
let myIterator = myGenerator(41);
// each call to next advances us to the next yield
let firstYield = myIterator.next();
console.log(firstYield.value); // 41
let secondYield = myIterator.next();
console.log(secondYield.value); //42
Call and Answer
// because we can pause generators, it doesn't matter if they run forever
function* generateIndex (initialValue) {
let x = initialValue;
while (true) x = yield x;
/*
Something really cool is happening here!
We can yield on the right side of an assignment.
When we call .next on the iterator, we can pass in a value as a parameter.
That value will then be the value that gets assigned!
*/
}
let i = 0,
it = generateIndex(i);
for (; i < 10; i++) console.log(it.next(i).value);
// 0 1 2 3 4 5 6 7 8 9
/*
Each yield pauses the function and yields a value.
If the yield is in an assignment, it will also wait
to receive a value from outside as well!
*/
Generators + Promises
- What if our generator yielded a promise?
- It may not be obvious at first, but this will give us unprecedented control over asynchronous behavior!
- Because we can pause/restart generators, we can implement a pattern where we "stream" promises one-by-one into our control to deal with their results before getting the next one
Example
/*
What if we have an array of api requests, and we only want
to get the contents of a request that will give us more than
twenty items?
*/
function* generateAsyncRequests (arrayOfRequests) {
for (let request of arrayOfRequests) {
yield fetch(request); // fetch, like our fetch, requests an http resource
}
}
function cycle (iterator) {
let request = iterator.next(); // yields object with promise for the data
if (request.done) return console.log('No requests yielded more than twenty items');
else return request.value
.then(data => {
if (data.length > 20) return data;
else cycle(iterator);
});
}
let urls = ['/api/techno', '/api/electronica', '/api/dance'],
it = generateAsyncRequests(urls);
cycle(it);
prompt.js
/* prompt.js */
/*
we're going to need both the answer and the event to resolve from our prompt
so let's spin up a different version of our prompt function
*/
export function promptForEvent (event, ques) {
return new Promise((resolve, reject) => {
rl.question(ques, answer => answer ?
resolve({ answer: answer, event: event }) : reject('No command given'));
});
}
search.js part a
/* search.js */
import fetch from './fetch';
import prompt, {promptForEvent} from './prompt'; // import our new prompt!
// our generator function
function* generateEvents (events) {
for (let event of events) {
yield promptForEvent(event, `
${event.id}: ${new Date(event.datetime)}
${event.venue.name} ${event.venue.city}, ${event.venue.region}
Do you want to attend this event? > `);
}
}
search.js part b
/* search.js */
// our function for "cycling" through the events
function cycleEvents (it, done) {
let result = it.next();
if (result.done) done();
else result.value
.then(response => {
if (response.answer === 'y') console.log(`
Added the show at ${response.event.venue.name} to the schedule!
`);
cycleEvents(it, done);
})
.catch(err => {
console.error(err);
cycleEvents(it, done);
});
}
search.js part c
/* search.js */
// our search function is now a lot cleaner!
export default function search (done) {
let _bandName;
prompt('Enter an artist to search for > ')
.then(bandName => {
_bandName = bandName;
return fetch(bandName);
})
.then(events => {
events = events.filter(event => event.venue.city === 'New York');
console.log(`\nUpcoming concerts for ${_bandName} in New York`);
let it = generateEvents(events);
cycleEvents(it, done);
})
.catch(error => {
console.error(error);
done();
});
};
You may be wondering...
- Can generators be used as function expressions (i.e. used in callbacks, variables, and as object methods)?
- Can I use apply/call/bind with generators?
- Yes to all of the above!
- Is there an arrow function version of generators
- Nope!
Whew!
Classes
Our app now
- We can show each concert and prompt the user to add it to the schedule or not
- We're missing something...oh yeah! The schedule! Right now, we only have console.logs
- We're done all the hard parts, so now we can plug in a schedule pretty easily! Let's create a Schedule using Object Oriented principles. We'll have a Schedule class, that we'll use to create an object instance of a schedule
Our Schedule
- What data will it hold?
- A list of events that we're going to
- What actions can it perform?
- It can log its contents to the console
- It can add a new concert to itself
- It can save its contents to a file
- It can load that file to re-set its contents
Background
- Unlike many other languages (Java, Ruby, etc), JavaScript does not have class-based concepts built in
- JavaScript uses prototypal inheritance, rather than class-based inheritance
- ES6 introduces the class keyword, similar to other languages to make it more intuitive to "create" classes
Class Syntax
- syntactic sugar for writing constructor functions that helps eliminate some boilerplate
- familiar to developers coming from other languages
- adding new functionality - behind the scenes, the behavior is the same as a constructor function
- replacing prototypal inheritance somehow
What it is:
What it isn't:
Stay Classy
class Dog {
// the constructor function itself goes in a named function called 'constructor'
constructor (name, age, breed) {
this.name = name;
this.age = age;
this.breed = breed;
}
// no need to attach methods to the prototype, like Dog.prototype.bar = function () {}!
bark () {
console.log('Arf!');
}
}
let snoopy = new Dog("Snoopy", 5, "Beagle");
snoopy.bark(); // 'Arf!'
Inheritance
- Inheritance is sort of a pain in JavaScript
- Using ParentClass.call in the constructor, Object.create, re-assigning ChildClass.prototype.constructor...
- Using ES6 class syntax, all of that is gone!
"Super" inheritance
// our parent class
class Game {
constructor (title, numPlayers, timeToPlay) {
this.title = title;
this.numPlayers = numPlayers;
this.timeToPlay = timeToPlay;
}
play () {
console.log("Let's play " + this.title + '!');
}
}
// using 'extends' removes the need to mess with accessing the function prototype
class Avalon extends Game {
// using super is easier than using Game.call()
constructor (title, numPlayers, timeToPlay, rules) {
super(title, numPlayers, timeToPlay);
this.rules = rules;
}
play () {
super.play(); // easily access any methods from the base class using super
console.log("I'm not Merlin!");
}
}
Static Members
- You can also easily declare static members (methods) using the static keyword
Static shock
class Minion extends Characters {
constructor (picture, loyalty) {
super (picture, loyalty);
}
static defaultMinion () {
return new Minion ('minion_1.png', 'evil');
}
}
let defaultMinion = Minion.defaultMinion();
let customMinion = new Minion('minion_2.png, 'also evil');
Creating the schedule class
/* schedule.js */
// create a new file called schedule.js
'use strict';
// we'll use the Node "fs" (filesystem) library to read and write files!
import * as fs from 'fs';
export default class Schedule {
constructor (events) {
this.schedule = events || [];
}
add (event) {
this.schedule.push(event);
console.log('Added to schedule!');
}
Default function parameters
- That pattern we used in the constructor is common in JS for setting "default" parameter values
- Actually, ES6 offers yet another new feature to declare default values in the function expression itself!
With a default!
/* schedule.js */
import * as fs from 'fs';
export default class Schedule {
// niiiiice!
constructor (events=[]) {
this.schedule = events;
}
add (event) {
this.schedule.push(event);
console.log('Added to schedule!');
}
The log method
/* schedule.js */
import * as fs from 'fs';
export default class Schedule {
/* ... */
log () {
if (!this.schedule.length) return console.log('\nNo events currently scheduled\n');
console.log('Your schedule: \n');
for (let event of this.schedule) {
console.log(`
${event.artists[0].name}: ${new Date(event.datetime)}
${event.venue.name} ${event.venue.city}, ${event.venue.region}
`);
}
}
}
Reading and writing
// In node, we use fs.readFile and fs.writeFile to read and write files
// the ./ syntax means relative to the file where the node process is executing
fs.readFile('./some-file.json', 'utf8', function (err, data) {
if (error) throw error;
return JSON.parse(data);
});
fs.writeFile('./some-file.json', JSON.stringify(ourData), 'utf8', function (error) {
if (error) throw error;
console.log('Wrote to file successfully!');
});
/*
These callback-version functions are okay. But our Schedule will be a lot cooler
if it returns Promises!
*/
Saving
/* schedule.js */
// instances of our schedule will save themselves
save () {
return new Promise ((resolve, reject) => {
fs.writeFile('./schedule.json', JSON.stringify(this.schedule), 'utf8', error => {
if (error) reject(error);
resolve(this);
});
});
}
Loading
/* schedule.js */
/*
When we load data for our schedule, we won't have an instance of a schedule yet!
Let's make this a static on the Schedule class, that it can use to create an
instance of itself!
*/
static load () {
return new Promise((resolve, reject) => {
fs.readFile('./schedule.json', 'utf8', (error, data) => {
if (error) reject(error);
data ? resolve(new Schedule(JSON.parse(data))) : resolve(new Schedule());
});
});
}
Hooking up our schedule part a
/* index.js */
/*
We only want to load the schedule on startup, so it's not quite right for our main function.
Let's put all the application start logic in a new function called "bootstrap"
*/
import prompt from './prompt';
import search from './search';
import Schedule from './schedule';
function bootstrap () {
console.log("Welcome to ConcertGoer!");
Schedule.load()
.then(schedule => {
schedule.log();
main(schedule);
})
.catch(error => {
console.log('No schedule.json found - one will be created upon adding an event!');
main(new Schedule());
});
}
Hooking up our schedule part b
/* index.js */
function main (schedule) {
prompt('What would you like to do > ')
.then(command => {
switch (command) {
case 'search':
search(schedule, main); // let's pass our schedule into search so we can use it there
break;
case 'view schedule': // let's give ourselves a new commend
schedule.log();
main(schedule);
break;
default:
console.log('Not a valid command');
main(schedule); // main takes a schedule now - be sure to pass it down
}
})
.catch(err => {
console.log(err);
main(schedule); // same here!
});
}
// kick things off with our bootstrap!
bootstrap();
Search.js
/* search.js */
// account for our new schedule
export default function search (schedule, done) {
let _bandName;
prompt('Enter an artist to search for > ')
.then(bandName => {
_bandName = bandName;
return fetch(bandName);
})
.then(events => {
events = events.filter(event => event.venue.city === 'New York');
console.log(`\nUpcoming concerts for ${_bandName} in New York`);
let it = generateEvents(events);
cycleEvents(it, schedule, done); // we'll pass the schedule along here
})
.catch(error => {
console.error(error);
done(schedule); // don't forget here too!
});
}
Finally putting it together
/* search.js */
// pass in our new schedule!
function cycleEvents (it, schedule, done) {
let result = it.next();
// save our schedule when we're done
if (result.done) schedule.save()
.then(savedSchedule => done(savedSchedule))
.catch(console.error);
// add to our schedule if we say "y" at the prompt!
else result.value
.then(response => {
if (response.answer === 'y') schedule.add(response.event);
cycleEvents(it, schedule, done);
})
.catch(err => {
console.error(err);
cycleEvents(it, schedule, done);
});
}
Let's try it out!
Goodbye world
Saying farewell
/* index.js */
function main (schedule) {
prompt('What would you like to do > ')
.then(command => {
switch (command) {
case 'search':
search(schedule, main);
break;
case 'view schedule':
schedule.log();
main(schedule);
break;
// all good things must come to end...
case 'exit':
console.log('Goodbye!');
process.exit();
break;
Your ES6 journey is only beginning!
- We covered a lot of ground...
- Const and Let
- Arrow Functions
- String interpolation
- Module import/export
- Promises
- Iterators/Iterables
- Generators
- Classes
- Function parameter defaults
There's more to learn!
- Some topics I would have like to get to:
- Destructured assignment
- Symbols
- Spread operator/rest parameter
- Set and Map collections
- Proxy
- Reflect
- Tail call optimization
- ...and more!
ES7!?!
- ES7 (or ES2016) will be much, much smaller
- Going forward, there are plans to release language updates more granularly, rather than in monolithic packages
- We'll get new language features sooner!
Sources & Resources
- MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript
- ES6 Quick Guide: es6-features.org
- Babel: https://babeljs.io/
- YDKJS: https://github.com/getify/You-Dont-Know-JS
- 2ality: http://www.2ality.com/
- bandsintown: https://www.bandsintown.com/api/overview
- Philip Roberts: https://www.youtube.com/watch?v=8aGhZQkoFbQ
- Many, many various blog and stack overflow posts
This presentation
- Slides: https://slides.com/tomkelly-1/es6-deck/
- Starting point: https://github.com/tmkelly28/es6-starting-point
- Ending point: https://github.com/tmkelly28/es6-ending-point
Want to keep hacking?
Try making it so you can remove events from the schedule!
Try making it so that the user can choose which city to search!
Try making it so that events fall off the schedule once their date passes!
Come visit me down in InternetTown...
- Github: https://github.com/tmkelly28
- LinkedIn: https://www.linkedin.com/in/thomas-kelly-867155b8
Thanks for coming!
ES6 - New Features in JavaScript
By Tom Kelly
ES6 - New Features in JavaScript
A dive into some exciting new JavaScript features
- 2,254