On Github BouvetNord / ecmascript-workshop-slides
Browser support for ECMAScript 6 features is not extensive yet and varies alot.
This is why we need to convert it to the previous version of JavaScript, called ECMAScript 5, which runs fine in any modern browser.
This conversion is often referred to as transpilation, and Babel is a popular solution to do that
Some of the features can be used almost for "free", such as modules, arrow functions, and classes withouth to much overhead (syntatical sugar).
Additions to Array, String and Math objects and prototypes (such as Array.from()) require so-called polyfills (provide missing code).
For a full overview of features that are supported by both transpilers and browers, see ECMAScript 6 compability table
Variables are so much more fun in ES6 - not! But they are a lot nicer(les:more predictible) to hang out with than the old var chap of ES5!
- ES6 brings two new ways to declare variables: let and const - These basically replace, the ES5 way of declaring variables, using var - However, var is still around
Here is an example where the let declared variable tmp, only exists within the block starting in line A:
function switch(x, y) { if (x > y) { // (A) let tmp = x; x = y; y = tmp; } console.log(tmp===x); // ReferenceError: tmp is not defined return [x, y]; }
const is a read-only, initialize at once constant type. Here are a few examples of correct usage and wrong usage leading to SyntaxError and TypeError:
const foo; // SyntaxError: missing = in const declaration const bar = 123; bar = 456; // TypeError: `bar` is read-only
- Promises are a pattern, helping out with one particular kind of asynchronous programming: "a function (or method) that returns its result asynchronously." - To implement such a function, you return a Promise, an object that is a placeholder for the result. - The caller of the function registers callbacks with the Promise to be notified once the result has been computed. - The function sends the result via the Promise. The de-facto standard for JavaScript Promises is called Promises/A+ [1]. The ECMAScript 6 Promise API follows that standard.
A gentle example, to give you a feeling of what working with Promises will be like. With Node.js-style callbacks, reading a file asynchronously looks like this:
fs.readFile('config.json', function (error, text) { if (error) { console.error('Error while reading config file'); } else { try { let obj = JSON.parse(text); console.log(JSON.stringify(obj, null, 4)); } catch (e) { console.error('Invalid JSON in file'); } } });
With Promises, the same functionality is implemented like this:
readFilePromisified('config.json') .then(function (text) { // (A) let obj = JSON.parse(text); console.log(JSON.stringify(obj, null, 4)); }) .catch(function (reason) { // (B) // File read error or JSON SyntaxError console.error('An error occurred', reason); });
There are still callbacks, but they are provided via methods that are invoked on the result (then() and catch()). The error callback in line B is convenient in two ways: First, it’s a single style of handling errors (versus if (error) and try-catch in the previous example). Second, you can handle the errors of both readFilePromisified() and the callback in line A from a single location.
No more global namespace pollution.
There is exactly one module per file and one file per module.
By default anything you declare in a file is not available outside that file (awesome).
A module can export multiple things by prefixing its declarations with the keyword export:
export var myVar1 = ...; export let myVar2 = ...; export const MY_CONST = ...; export function myFunc() {} export class MyClass {}
These exports are distinguished by their names and are called named exports.
import { myFunc, myVar1 } from 'lib'; myFunc();
You can also list everything you want to export at the end of the module (which is once again similar in style to the revealing module pattern):
const MY_CONST = ...; function myFunc() {} export { MY_CONST, myFunc };
You can also import the complete module using a wildcard:
import * as lib from 'lib'; lib.myFunc();
If you want to export a single value from the module then you can use default export.
For example, a class:
export default class {}
And import like this (omit the curly braces):
import MyClass from 'MyClass'; var instance = new MyClass();
Module can have both named exports and a default export.
In such cases, to import a module's default export, you have to omit the curly braces in the import statement:
import myDefault, { foo, bar } from 'lib';
Only use export default, and to do that at the end of your module files. Like this:
var api = { foo: 'bar', baz: 'ponyfoo' } export default api
Easy then to import the whole thing like this:
import api from 'api'; api.foo; // bar
In `src/00-modules/named.js´ create and export two named functions:
In `src/00-modules/default.js` import the two functions created in the previous task, then export them again, but as a single default export.
Simpler and clearer syntax to create objects and to deal with inheritance
Classes aren't really 'classes', instead they are functions which are in turn objects
It’s important to understand that while JavaScript is an object-oriented language, it is prototype-based and does not implement a traditional class system.
Almost everything in JavaScript are objects (including functions and arrays). Except for primitives:
An object is a collection of properties, and a property is an association between a name (or key) and a value. A property's value can be a function, in which case the property is known as a method.
// The easiest way to create an object is like this: let myObject = { 'key1': "Awesome", 'key2': "Object" } // Or: var myObject = Object.create(new Object()); myObject.key1 = "Awesome"; myObject.key2 = "Object"; // Or: function MyObject(value1, value2) { this.key1 = value1; this.key2 = value2; } var myObject = new MyObject("Awesome", "Object");
Which of these alternatives are suited to create multiple instances of same type?
// Get a property var value = myObject.key1; var value = myObject["key1"]; // Set a property myObject.property = "Value"; myObject["property with space in it"] = "Value"; // Call a method myObject.toString(); // To find out if a property exists on an object 'key1' in myObject; // true
function MyClass (id) { var privateField = "My secret"; // Instance property this.id = id; } // Instance method MyClass.prototype.myMethod = function () {}; var instance1 = new MyClass(1); var instance2 = new MyClass(2); instance1.myMethod(); instance1.id; // 1 instance2.myMethod(); instance2.id; // 2 instance1 instanceof MyClass; // true
Prototype methods are shared across all instances of the class. One method, many instances!
MyClass is a constructor function, and combined with the new keyword, work together to create new objects in much in the same way classes do in other OO languages.
When you create an object using the new keyword, it creates a new object, passes it in as this to the constructor function.
The own properties are properties that were defined on the instance, while the inherited properties were inherited from the Function’s Prototype object.
instance1.hasOwnProperty("id"); // true instance1.hasOwnProperty("myMethod"); // false
When you define a function within JavaScript, it comes with a few pre-defined properties and one of these is the illusive prototype.
(1) Initially an empty object. (2) You can add methods and properties on a function’s prototype property to make those methods and properties available to instances of that function. All class instances, past and future, will be affected.
Objects inherit from other objects.
Every object in JavaScript has a special related object called the prototype.
vehicle.__proto__ = machine // machine is the prototype of vehicle car.__proto__ = vehicle // vehicle is the prototype of car
This is a prototype chain: car -> vehicle -> machine
When looking up a property, Javascript will try to find the property in the object itself. If it does not find it then it tries in it's prototype, and so on.
A function’s prototype is used as the object to be assigned as the prototype for new objects’ created using this function as a constructor function.
instance.__proto__ = Function.prototype
An object’s prototype is the object from which it is inheriting properties.
function MySubClass(id) { MyClass.call(this, id); } // Inherit from the parent class MySubClass.prototype = Object.create(MyClass.prototype); MySubClass.prototype.constructor = MySubClass; // Child class method MySubClass.prototype.myMethod = function() { MyClass.prototype.method.call(this); }
Easy to read?
class MyClass { constructor (id) { this.id = id } myMethod () {} static myStaticMethod() {} } let instance = new MyClass(1); instance instanceof MyClass; // true typeof MyClass // 'function' (old-school constructor function) MyClass.myStaticMethod(); darth.hasOwnProperty("myMethod") MyClass.prototype.myMethod(); // prototype-based
The class keyword is syntactical sugar, JavaScript remaining prototype-based!
This is the prototype chain: MyClass -> Object
class MySubClass extends MyClass { constructor (id) { super(id) } }
This is the prototype chain: MySubClass -> MyClass -> Object
Create a class equal to the ECMAScript 5 code:
function Point(x,y) { this.x = x; this.y = y; } // Static method Point.distance = function (a, b) { var dx = a.x - b.x; var dy = a.y - b.y; return Math.sqrt(dx*dx + dy*dy); }; Point.prototype.toString = function () { return this.x + ' ' + this.y; }; module.exports = Point;
Extend the Point class from previous task so that it behaves equal to the ECMAScript 5 code below:
// Child class constructor var ColorPoint = function (x, y, color) { Point.call(this, x, y); this.color = color || 'red'; }; // Inherit from the parent class ColorPoint.prototype = Object.create(Point.prototype); ColorPoint.prototype.constructor = ColorPoint; // Child class method ColorPoint.prototype.getColor = function () { return this.color; }; module.exports = ColorPoint;
ES6 has a new loop — for-of. It works with iterables. Let’s look at his signature:
for (let item of ITERABLE) { CODE BLOCK }
It’s similar to for-in loop, which can be used to iterate through object properties.
Iterable is an object which has [Symbol.iterator]() method inside.
The [Symbol.iterator] method must return an iterator object, which is actually responsible for the iteration logic.
An iterator is an object with a next method that returns { done, value } tuples.
We finally can use for-of for looping over the elements:
const arr = [1, 2, 3, 4, 5]; for (let item of arr) { console.log(item); // 1 // 2 // 3 // 4 // 5 }
Symbol is in turn an unique and immutable data type which can be used as an identifier for object properties — no equivalent in ES5.
// Symbol let s1 = Symbol('abc'); let s2 = Symbol('abc'); console.log(s1 !== s2); // true console.log(typeof s1); // 'symbol' let obj = {}; obj[s1] = 'abc'; console.log(obj); // Object { Symbol(abc): 'abc' }
You can use symbols to create unique identifiers for object properties.
Symbols are not visible in `for...in` iterations and object.keys()
The Symbol.iterator well-known symbol specifies the default iterator for an object.
class Classroom { constructor() { this.students = ["Tim", "Joy", "Sue"]; } }
Option 1: Return a reference to the array, in which case the caller might change the array by adding or removing items.
Option 2: Another option is to make a copy of the array. Then, the original student array remains safe. However, copy operations can be expensive.
Option 3: Make the classroom iterable by adding a [Symbol.iterator]() method.
function PointCloud(list) { this.collection = list; } PointCloud.prototype[Symbol.iterator] = function() { var index = 0; var array = this.collection; return { next: () => { var result = { value: undefined, done: true }; if (index < array.length) { result.value = array[index++]; result.done = false; } return result; } }; }; module.exports = PointCloud;
() => {}
A quick overview of the syntax:
// Basic syntax: (param1, param2, paramN) => { statements } (param1, param2, paramN) => expression // equivalent to: => { return expression; } // Parentheses are optional when there's only one argument: (singleParam) => { statements } singleParam => { statements } // A function with no arguments requires parentheses: () => { statements }
Another example:
function(x) { return x+1;} x => x+1;
Here a2 is using traditional anonymous functions while a3 is using the arrow function syntax.
var a = ["Hydrogen", "Helium", "Lithium", "Beryllium"]; var a2 = a.map(function(s){ return s.length }); var a3 = a.map( s => s.length ); //a2 = [8, 6, 7, 10] //a3 = [8, 6, 7, 10]
function foo(a) { var b = a * 2; function bar(c) { console.log( a, b, c ); } bar(b * 3); } foo( 2 ); // 2 4 12
function person(firstName, lastName, age, eyeColor) { this.firstName = firstName; this.lastName = lastName; this.changeName = function (name) { this.lastName = name; }; }
function Person() { // The Person() constructor defines `this` as an instance of itself. this.age = 0; setInterval(function growUp() { // In nonstrict mode, the growUp() function defines `this` // as the global object, which is different from the `this` // defined by the Person() constructor. this.age++; }, 1000); } var p = new Person();
That can be solved like this:
function Person() { var self = this; // Some choose `that` instead of `self`. // Choose one and be consistent. self.age = 0; setInterval(function growUp() { // The callback refers to the `self` variable of which // the value is the expected object. self.age++; }, 1000); } var p = new Person()
With arrow functions and lexical binding of this, the problem can be solved like this:
function Person(){ this.age = 0; setInterval(() => { this.age++; // |this| properly refers to the person object }, 1000); }
let {a,b} = obj;
let foo = ["one", "two", "three"]; // without destructuring let one = foo[0]; let two = foo[1]; let three = foo[2]; // with destructuring let [one, two, three] = foo;
let o = {p: 42, q: true}; let {p, q} = o;
It is also possible to give the new variable a new name by using:
let {p:age, q}
var metadata = { title: "Scratchpad", translations: [ { locale: "de", localization_tags: [ ], last_edit: "2014-04-14T08:43:37", url: "/de/docs/Tools/Scratchpad", title: "JavaScript-Umgebung" } ], url: "/en-US/docs/Tools/Scratchpad" }; let { title: englishTitle, translations: [{ title: localeTitle }] } = metadata;
[a, b] = [b, a];
var [a, , b] = f();
for (var {name: n, family: { father: f } } of people) {...}
var help = function* helpiterable() { yield 100; } function* myiterable() { while(true) { yield 1; yield 2; for(var i=0;i<10;i++) { yield i*2; } yield* [1,2,3]; yield* help(); yield; // yield undefined; } return 0; // if return is not added, then it is implicit as return undefined; }
function* myobserver() { console.log(yield); } var miobserver = myobserver(); miobserver.next(); // initialize miobserver.next("test"); // writes "test" to console
function* mycoroutine() { yield 1; // A console.log(yield); // B console.log(yield); // C } var micoroutine = mycoroutine(); var i = micoroutine.next(); // initialize // A, and pauses at A micoroutine.next(); // goes to B, and pauses, waits for data micoroutine.next("test"); // writes "test" to console and continues until next yield, pauses // B->C micoroutine.next("test2"); // writes "test" to console goes to invisible return // B->C console.log(micoroutine.next()); // returns "undefined" from return undefined; exits generator
function* producer() { console.log('1'); yield; // (A) console.log('2'); // (B) } let myProducer = producer(); // nothing happens, generator object is returned myProducer.next(); // will pause on yield on (A) myProducer.next(); // will continue, and execute (B). Generator is done.
`Unleash the power of backticks`
// The good oldfashioned way var a = 5; var b = 10; console.log('The sum of ' + a + ' and ' + b + ' is ' + (a + b)); // With template strings console.log(`The sum of ${a} and ${b} is ${a + b}`);
// The good oldfashioned way console.log('Thank you Mario!\n' + 'But our princess is in another castle.'); // With template strings console.log(`Thank you Mario! But our princess is in another castle.`);
var a = 5; var b = 10; function tag(strings, value1, value2) { console.log(strings[0]); // 'Hello ' console.log(strings[1]); // ' world ' console.log(value1); // 15 console.log(value2); // 50 return 'Foo!'; } tag`Hello ${ a + b } world ${ a * b }`; // 'Foo!'
In `src/04-template-strings/01-variable-substitution.js´ change the log() method to use embedded expressions instead of concatenated strings.
In `src/04-template-strings/02-multiline-strings.js´, remove all string concatenations and '\n' characters, and use a multi-line string with embedded expressions instead.
New data structures
var capitals = { 'Norway': 'Oslo', 'Sweden': 'Stockholm' }; capitals['Denmark'] = 'Copenhagen'; console.log(capitals['Sweden']); // 'Stockholm'
// Iteration is a bit awkward Object.keys(capitals).forEach(key => { console.log('The capital of ' + key + ' is ' + capitals[key]); }); // The easiest way to get the size is var size = Object.keys(capitals).length; // Keys can only be represented as strings capitals[1] = 'London'; // 1 is converted to '1' capitals[true] = 'Berlin'; // true is converted to 'true'
// Initialize the map var map = new Map(); // Add entry with key 'Norway' and value 'Oslo' map.set('Norway', 'Oslo'); // Keys can be any data type, object, or even a function var keyString = "a string", keyObj = {}, keyFunc = function () {}; map.set(keyString, 'value'); map.set(keyObj, 'value'); map.set(keyFunc, 'value');
// Iteration is a bit cleaner for (let [country, capital] of capitals) { console.log('The capital of ' + country + ' is ' + capital); } // Getting the size is easy var size = capitals.size;
// Initialize the set var set = new Set(); // Add value set.add('Norway'); // No duplicate values set.add('Norway'); // has no effect // Values can be any data type, object, or a function var obj = {}, func = function () {}; set.add(obj); set.add(func);
// Simple iteration for (let value of set) { console.log(value); } // Get size var size = set.size; // Check if value exists var exists = set.has('Norway'); // Delete set.delete('Norway');
let map = new WeakMap(); let apple = { id: 1, name: 'Apple' }; map.set(apple, 3); map.get(apple); // 3 // Will not be kept in the map, as no one else is holding a // reference to the key map.set({ id: 2, name: 'Orange' }, 5); // Will be held in the map until the element with id 'example' // is removed from DOM map.set(document.getElementById('example'), { clickCount: 4 });
// Simple tagging of DOM elements that have been touched/edited. // When elements are removed from DOM, the garbage collector // will automatically remove them from the Set. var touchedElements = new WeakSet(); function addTouched(domElement) { touchedElements.add(domElement); } function hasBeenTouched(domElement) { return touchedElements.has(domElement); }
In `src/07-maps-and-sets/01-map.js´, rewrite carMap to be a Map, and modify the functions so that they work with the Map data structure.
In `src/07-maps-and-sets/02-set.js´, change getUniqueActors() to use a Set in order to return a unique array of actors.