Mani Nilchiani
Traditionally in JS, only functions create scope using `var`. ES6 introduces `let` and `const`, block scoped variables.
var a = 2;
(function IIFE(){
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
let i = 55;
for(let i = 0; i < 10; i++) {
console.log(i); // 0 1 2 3 4 5 6 7 8 9
}
console.log(i); // 55
const foo = 'abc';
foo = 'def'; // TypeError
For `const`, the value is not frozen or immutable because of const, just the assignment of it. If the value is complex, such as an object or array, the contents of the value can still be modified:
const a = [1,2,3];
a.push( 4 );
console.log( a ); // [1,2,3,4]
a = 42; // TypeError
ES6 introduces a new `...` operator that's typically referred to as the spread or rest operator, depending on where/how it's used.
In ES5 we would do:
function foo(x,y,z) {
console.log( x, y, z );
}
foo.apply( null, [1,2,3] ); // 1 2 3
In ES6 we can simply:
foo( ...[1,2,3] ); // 1 2 3
But `...` can be used to spread out/expand a value in other contexts as well, such as inside another array declaration.
ES5:
var a = [2,3,4];
var b = [1].concat(a, [5]); // [1,2,3,4,5]
ES6:
var a = [2,3,4];
var b = [ 1, ...a, 5 ];
console.log( b ); // [1,2,3,4,5]
Instead of spreading a value out, the `...` can gather a set of values together into an array.
function foo(x, y, ...z) {
console.log( x, y, z );
}
foo( 1, 2, 3, 4, 5 ); // 1 2 [3,4,5]
Which means that you can gather your `arguments` if they are unnamed
function foo(...args) {
console.log( args );
}
foo( 1, 2, 3, 4, 5); // [1,2,3,4,5]
One of the most common idioms in JavaScript relates to setting a default value for a function parameter.
ES5:
function foo(x,y) {
x = x || 11;
y = y || 31;
console.log( x + y );
}
foo(); // 42
foo( 5, 6 ); // 11
foo(0, 42); // 53 -- YIKES!
ES6:
function foo(x = 11, y = 31) {
console.log( x + y );
}
foo(); // 42
foo( 5, 6 ); // 11
foo( 0, 42 ); // 42 -- WOOT!
Function default values can be more than just simple values like 31; they can be any valid expression, even a function call:
function foo(x = y + 3, z = bar( x )) {
console.log( x, z );
}
Simply put: structured assignments
ES5:
function foo() {
return [1,2,3];
}
var tmp = foo(),
a = tmp[0], b = tmp[1], c = tmp[2];
console.log( a, b, c ); // 1 2 3
ES6:
var [ a, b, c ] = foo();
Same is true about Orrays
ES5:
function bar() {
return {
x: 4,
y: 5,
z: 6
};
}
var tmp = bar(),
x = tmp.x, y = tmp.y, z = tmp.z;
console.log( x, y, z ); // 4 5 6
ES6:
var { x: x, y: y, z: z } = bar();
If the property name being matched is the same as the variable you want to declare, you can actually shorten the syntax:
var { x, y, z } = bar();
console.log( x, y, z ); // 4 5 6
This is great. But when you need to assign a prop name to a different variable name, you can use the verbose syntax:
var { x: bam, y: baz, z: bap } = bar();
console.log( bam, baz, bap ); // 4 5 6
console.log( x, y, z ); // ReferenceError
In fact, the assignment expressions (a, y, etc.) don't actually need to be just variable identifiers. Anything that's a valid assignment expression is allowed. For example:
var o = {};
[o.a, o.b, o.c] = foo();
( { x: o.x, y: o.y, z: o.z } = bar() );
console.log( o.a, o.b, o.c ); // 1 2 3
console.log( o.x, o.y, o.z ); // 4 5 6
If the property name being matched is the same as the variable you want to declare, you can actually shorten the syntax:
var { x, y, z } = bar();
console.log( x, y, z ); // 4 5 6
This is great. But when you need to assign a prop name to a different variable name, you can use the verbose syntax:
var { x: bam, y: baz, z: bap } = bar();
console.log( bam, baz, bap ); // 4 5 6
console.log( x, y, z ); // ReferenceError
Both forms of destructuring can offer a default value option for an assignment, using the = syntax similar to the default function argument values discussed earlier.
var [ a = 3, b = 6, c = 9, d = 12 ] = foo();
var { x = 5, y = 10, z = 15, w = 20 } = bar();
console.log( a, b, c, d ); // 1 2 3 12
console.log( x, y, z, w ); // 4 5 6 20
In the following ES5 snippet, 42 is implicitly assigned to `x`
function foo(x) {
console.log( x );
}
foo( 42 );
Which means that we should be able to destructure that assignment. Which we can!
ES6:
function foo( [ x, y ] ) {
console.log( x, y );
}
foo( [ 1, 2 ] ); // 1 2
foo( [ 1 ] ); // 1 undefined
foo( [] ); // undefined undefined
Object destructuring works too:
function foo( { x, y } ) {
console.log( x, y );
}
foo( { y: 1, x: 2 } ); // 2 1
foo( { y: 42 } ); // undefined 42
foo( {} ); // undefined undefined
ES6 adds a number of important convenience extensions to the humble { .. } object literal.
ES5:
var x = 2, y = 3,
o = {
x: x,
y: y
};
Consice props in ES6:
var x = 2, y = 3,
o = {
x,
y
};
Same is true with methods
ES5:
var o = {
x: function(){
// ..
},
y: function(){
// ..
}
}
Consice methods in ES6:
var o = {
x() {
// ..
},
y() {
// ..
}
}
This has been a non-standard feature of many JS engines, which in ES6 is now standard.
var o1 = {
// ..
};
var o2 = {
__proto__: o1,
// ..
};
ES6 also offers a utility for this:
Object.setPrototypeOf( o2, o1 );
super is typically thought of as being only related to classes. However, due to JS's classless-objects-with-prototypes nature, super is equally effective, and nearly the same in behavior, with plain objects' concise methods.
var o1 = {
foo() {
console.log( "o1:foo" );
}
};
var o2 = {
foo() {
super.foo();
console.log( "o2:foo" );
}
};
Object.setPrototypeOf( o2, o1 );
o2.foo(); // o1:foo o2:foo
ES5:
var name = "Kyle";
var greeting = "Hello " + name + "!";
console.log( greeting ); // "Hello Kyle!"
console.log( typeof greeting ); // "string"
ES6:
var name = "Kyle";
var greeting = `Hello ${name}!`;
console.log( greeting ); // "Hello Kyle!"
console.log( typeof greeting ); // "string"
Backtick delimiter also allows for multi-line text:
ES5:
var text = 'Now is the time for all good men' +
'to come to the aid of their' +
'country!'; // Ugh
ES6:
var text =
`Now is the time for all good men
to come to the aid of their
country!`; // Yay
This makes interpolated expressions happen too!
function upper(s) {
return s.toUpperCase();
}
var who = "reader";
var text =
`A very ${upper( "warm" )} welcome
to all of you ${upper( `${who}s` )}!`;
console.log( text );
// A very WARM welcome
// to all of you READERS!
Let's first illustrate what an arrow function looks like, as compared to normal functions:
function foo(x,y) {
return x + y;
}
// versus
var foo = (x,y) => x + y; // Implicit return
Other usecases:
var f1 = () => 12;
var f2 = x => x * 2;
var f3 = (x,y) => {
var z = x * 2 + y;
y++;
x *= 3;
return (x + y + z) / 2;
};
Arrow functions for functional code:
var a = [1,2,3,4,5];
a = a.map( v => v * 2 );
console.log( a ); // [2,4,6,8,10]
Arrow functions save the scope!
ES5:
var controller = {
makeRequest: function(..){
var self = this;
btn.addEventListener( "click", function(){
// ..
self.makeRequest(..);
}, false );
}
};
ES6:
var controller = {
makeRequest: function(..){
btn.addEventListener( "click", () => {
// ..
this.makeRequest(..);
}, false );
}
};
Lexical this in the arrow function callback in the previous snippet now points to the same value as in the enclosing makeRequest(..) function. In other words, => is a syntactic stand-in for var self = this.
Joining the for and for..in loops from the JavaScript we're all familiar with, ES6 adds a for..of loop, which loops over the set of values produced by an iterator.
var a = ["a","b","c","d","e"];
for (var idx in a) {
console.log( idx );
}
// 0 1 2 3 4
for (var val of a) {
console.log( val );
}
// "a" "b" "c" "d" "e"
in ES5, we would have to do the following:
var a = ["a","b","c","d","e"],
k = Object.keys( a );
for (var val, i = 0; i < k.length; i++) {
val = a[ k[i] ];
console.log( val );
}
// "a" "b" "c" "d" "e"
A new primitive type has been added to JavaScript: the symbol.
// create a symbol
var sym = Symbol( "some optional description" );
typeof sym; // "symbol"
Some things to note:
Using symbols as keys is where symbols become interesting. Because symbols are unique, a symbol can create a unique property inside an object. You never have to worry about conflicting with existing methods, or being accidently overwritten.
let firstName = Symbol();
let person = {
lastName: "Allen",
[firstName]: "Scott",
};
let names = [];
for(var p in person) {
names.push(p);
}
expect(names.length).toBe(1); // Symbol keys are ignored in all iterables
expect(names[0]).toBe("lastName");
"Well Known" Symbols are those that come built-in with ES6 like:
[1,2,3][Symbol.iterator]; // [Function: values]
Iterators are a way of organizing ordered, sequential, pull-based consumption of data.
An iterator is a structured pattern for pulling information from a source in one-at-a-time fashion.
Examples:
Many of the built-in data structures in JavaScript will now expose an iterator implementing this standard. And you can also construct your own iterators adhering to the same standard, for maximal interoperability.
Interface:
Iterator [required]
next() {method}: retrieves next IteratorResult
There are two optional members that some iterators are extended with:
Iterator [optional]
return() {method}: stops iterator and returns IteratorResult
throw() {method}: signals error and returns IteratorResult
The IteratorResult interface is specified as:
IteratorResult
value {property}: current iteration value or final return value
(optional if `undefined`)
done {property}: boolean, indicates completion status
Example Usecase:
var arr = [1,2,3];
var it = arr[Symbol.iterator]();
it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }
it.next(); // { value: undefined, done: true }
var greeting = "hello world";
var it = greeting[Symbol.iterator]();
it.next(); // { value: "h", done: false }
it.next(); // { value: "e", done: false }
Functions that do not run to completion. `yield` is used to incrementally execute the function.
function *foo() {
yield 1;
yield 2;
yield 3;
}
function *bar() {
yield *foo();
}
Early completion:
function *foo() {
yield 1;
yield 2;
yield 3;
}
var it = foo();
it.next(); // { value: 1, done: false }
it.return( 42 ); // { value: 42, done: true }
it.next(); // { value: undefined, done: true }
Async application of generators:
function request(url) {
// `it.next(..)` is the generator's iterator-resume call
makeAjaxCall( url, function(response){
it.next( response );
} );
// Note: nothing returned here!
}
function *main() {
var result1 = yield request( "http://some.url.1" );
var data = JSON.parse( result1 );
var result2 = yield request( "http://some.url.2?id=" + data.id );
var resp = JSON.parse( result2 );
console.log( "The value you asked for: " + resp.value );
}
var it = main();
it.next(); // get it all started
Async application of generators:
function request(url) {
// `it.next(..)` is the generator's iterator-resume call
makeAjaxCall( url, function(response){
it.next( response );
} );
// Note: nothing returned here!
}
function *main() {
var result1 = yield request( "http://some.url.1" );
var data = JSON.parse( result1 );
var result2 = yield request( "http://some.url.2?id=" + data.id );
var resp = JSON.parse( result2 );
console.log( "The value you asked for: " + resp.value );
}
var it = main();
it.next(); // get it all started
1991 called. they wanted their language feature back.
The Old Way:
function Hello(name) {
function greeting() {
console.log( "Hello " + name + "!" );
}
// public API
return {
greeting: greeting
};
}
var me = Hello( "Kyle" );
me.greeting(); // Hello Kyle!
CommonJS: (NodeJS Style)
// salute.js
var MySalute = "Hello";
module.exports = MySalute;
// main.js
var salute = require('./salute.js');
ES6 Modules - Export
export function foo() {
// ..
}
export var awesome = 42;
var bar = [1,2,3];
export { bar };
//or
function foo(..) {
// ..
}
export default foo;
//or
function foo(..) {
// ..
}
export { foo as default };
//or
export default function foo(..) {
// ..
}
ES6 Modules - Import
import { foo, bar, baz } from "foo";
//or
import { foo } from "foo";
//or
import { foo as theFooFunc } from "foo";
// or
import "foo";
Syntactic sugar for Prototypcal Inheritence
ES5:
function Foo(a,b) {
this.x = a;
this.y = b;
}
Foo.prototype.gimmeXY = function() {
return this.x * this.y;
}
ES6:
class Foo {
constructor(a,b) {
this.x = a;
this.y = b;
}
gimmeXY() {
return this.x * this.y;
}
}
Extend and Super
class Bar extends Foo {
constructor(a,b,c) {
super( a, b );
this.z = c;
}
gimmeXYZ() {
return super.gimmeXY() * this.z;
}
}
var b = new Bar( 5, 15, 25 );
b.x; // 5
b.y; // 15
b.z; // 25
b.gimmeXYZ(); // 1875
`static`
class Foo {
static cool() { console.log( "cool" ); }
wow() { console.log( "wow" ); }
}
class Bar extends Foo {
static awesome() {
super.cool();
console.log( "awesome" );
}
neat() {
super.wow();
console.log( "neat" );
}
}
Foo.cool(); // "cool"
Bar.cool(); // "cool"
Bar.awesome(); // "cool // "awesome"
var b = new Bar();
b.neat(); // "wow // "neat"
b.awesome; // undefined
b.cool; // undefined
Promises are not about replacing callbacks. Promises provide a trustable intermediary -- that is, between your calling code and the async code that will perform the task -- to manage callbacks. Promises can be chained together, which can sequence a series of asychronously completing steps. Together with higher-level abstractions like the all(..) method (in classic terms, a "gate") and the race(..) method (in classic terms, a "latch"), promise chains provide a mechanism for async flow control.
Callback Style:
function ajax(url,cb) {
// make request, eventually call `cb(..)`
}
// ..
ajax( "http://some.url.1", function handler(err,contents){
if (err) {
// handle ajax error
}
else {
// handle `contents` success
}
} );
Promise:
ajax( "http://some.url.1" )
.then(
function fulfilled(contents){
return ajax(
"http://some.url.2?v=" + contents
);
},
function rejected(reason){
return ajax(
"http://backup.url.3?err=" + reason
);
}
)
.then( function fulfilled(contents){
// `contents` comes from the subsequent
// `ajax(..)` call, whichever it was
} );
Promise:
function updatePriority(abTest) {
ABTestService
.get({ abTestId: abTest.ab_test_id })
.$promise
.then(function(response) {
return response.item;
})
.then(function(resource) {
return ABTestService.update({ abTestId: resource.abTest.abTestId }, resource);
})
.then(function(response) {
$log.log('Priority Updated ', response);
})
.catch(function(err) {
$log.error(err);
});
}
In JS, we use Objects to create objects-as-maps. The drawback is the inability to use a non-string value as the key.
let map = new Map();
map.set('foo', 123);
map.get('foo');
map.set(NaN, 123);
var buf = new ArrayBuffer( 32 );
buf.byteLength; // 32
buf is now a binary buffer that is 32-bytes long (256-bits), that's pre-initialized to all 0s. A buffer by itself doesn't really allow you any interaction exception for checking its byteLength property.
Several web platform features use or return array buffers, such as FileReader#readAsArrayBuffer(..), XMLHttpRequest#send(..), and ImageData (canvas data).