ES6(a.k.a ES2015) has been around for some time. Although it is fully backward compatible with previous version of JavaScript - ES5 which has been around for decades, there's quite some new syntax and features introduced in ES6 that is exciting on one hand, but can easily get wrong on the other hand. This posts is my study notes on ES6.
let for block scoping
let
defines block scope variables, which can be accessed in a nested scope just like var
. The following code outputs 12
:
'use strict';
function updateProductId() {
productId =12;
}
let productId =null;
updateProductId();
console.log(productId);
Unlike var
, let
is block scoped.
In the following example, every time the for loop runs, a new i
is declared. Thurs each function gets its own copy of i
.
The code outputs 0
:
'use strict';
let updateFunctions =[];
for (let i =0; i <2; i++){
updateFunctions.push(function() {return i; });
}
console.log(updateFunctions[0]());
Whereas in the following example, var i
is function scoped and hoisted. Each function gets the same i
. So the code outputs 2
'use strict';
let updateFunctions =[];
for (var i =0; i <2; i++){
updateFunctions.push(function() {return i; });
}
console.log(updateFunctions[0]());
An arrow function is not a function
It is not like a function
in a few ways. The key difference to note is that this
in an arrow function is
captured at function creation, not function invocation(runtime), and this
can't be changed at runtime.
Unlike traditional function
, an arrow function is not 'bindable'.
The following code outputs 123
rather than 456
:
var o1 = {
number: 123,
getArrowFunc: function() {
return () => console.log(this.number);
}
};
var o2 = {
number: 456
};
o1.getArrowFunc().bind(o2)();
Unlike function
, an arrow function is not 'callable'.
Same as above, the following code outputs 123
rather than 456
:
var o1 = {
number: 123,
getArrowFunc: function() {
return () => console.log(this.number);
}
};
var o2 = {
number: 456
};
o1.getArrowFunc().call(o2);
Unlike function
, an arrow function doesn't has a prototype
.
The code below outputs false
:
'use strict';
var getPrice =() =>5.99;
console.log(getPrice.hasOwnProperty("prototype"));
Default Parameters
Default parameters have access to any variables in the scope.
In the following example, price
and baseTax
can be accessed when defining another default parameter price
:
'use strict';
var baseTax=0.07;
var getTotal=function(price, tax=price *baseTax) {
console.log(price +tax);
};
Default parameter doesn't count as the actual parameter passed in.
The following code outputs 1
instead of 2
:
'use strict';
var getTotal = function(price, tax=0.07){
console.log(arguments.length);
};
getTotal(5.00);
Default parameters have orders and are not hoisted.
The following example causes a SyntaxError
because adjustment
is used before it is defined:
'use strict';
var getTotal = function(price = adjustment, adjustment = 1.00){
console.log(price + adjustment);
};
getTotal();
However the following similar example is valid and outputs 6
. This is because JavaScript
is a dynamic language that doesn't do compile time check, and at run time, the lookup for adjustment
is skipped when the actual parameter 5
is passed in and assigned to price
.
'use strict';
var getTotal = function(price = adjustment, adjustment = 1.00){
console.log(price + adjustment);
};
getTotal(5);
The Rest
Parameter
The concept is similar to Java's [Arbitrary Number of Arguments](http://docs.oracle.com/javase/tutorial/java/javaOO/arguments.html#varargs).
The 'Rest' parameter is always an array.
The following code outputs []
:
'use strict';
var showCategories = function(productId, ...categories) {
console.log(categories);
};
showCategories(123);
Unlike default parameters, the 'Rest' parameter doesn't count as a parameter defined on the function signature.
The following code outputs 1
:
'use strict';
var showCategories = function(productId, ...categories) {};
console.log(showCategories.length)
Just like default parameters, the 'Rest' parameter doesn't count as a actual parameter passed in.
The following code outputs the number of actual parameters passed in, which is 3
'use strict';
var showCategories = function(productId, ...categories) {
console.log(arguments.length);
};
showCategories(123, 'c1', 'c2')
The 'Spread' operator
...
is introduced in ES6 as the Spread
operator.
...
works not only with array, but with strings too.
The ...
breaks the string into a series individual characters, so the output is the maximum character 4
:
'use strict';
var maxCode = Math.max(..."43210");
console.log(maxCode);
Object Literal Extensions
An object literal is a list of zero or more pairs of property names and associated values of an object, enclosed in curly braces {}
. In ES6, object
literals are extended with new features.
Like arrow functions, this
in a shorthand function refer to the context the function being called.
For example, the following code outputs 5.99
rather than 7.99
:
'use strict';
var price = 5.99;
var productView = {
price: 7.99,
calculateValue() {
return this.price;
}
};
console.log(productView.calculateValue());
So we need to keep in mind that the shorthand functions are not exactly the same thing as the traditional functions. The same goes for arrow functions.
Template Literals
Expressions are allowed in ${}
'use strict';
let person = 'personA';
let personAName = 'Ryan';
console.log(~Person name: ${person + 'Name'}~); // Output: Person name: personAName
ES6 Modules
Original reference to what is imported become unavailable once alias is used.
import {name as name1} from 'module1.js';
console.log(~${name}, ${name1} are loaded.~); // name is no longer available. It may or may not cause error depending on the transpiler used and runtime environments.
import
statements get hoisted
and executed first before any other code.
Suppose the following program starts from index.js
which loads module1.js
:
// index.js
console.log(~start using name: ${name}~);
import {name} from 'module1.js';
console.log('end');
// module1.js
export let name = "ES6 Template";
console.log('module1 is loaded.');
// output:
module1 is loaded.
start using name: ES6 Template
end
Named imports are read-only.
We can't reassign a new value to named imports, but we can change properties.
// module1.js
exports let name = 'original name';
exports let obj = { name: name};
// index.js - causing error: name is read-only...
import {name} from 'module1.js';
console.log(~old name : ${name}~);
name = 'new name';
console.log(~new name : ${name}~);
// updated index.js - works. outputs new name.
import {obj} from 'module1.js';
console.log(~old name : ${obj.name}~);
obj.name = 'new name';
console.log(~new name : ${obj.name}~);
ES6 modules export/import bindings/references rather than values.
In the example below, both primitive types: age
and name
get updated. So they are not
being imported as local variables:
// module1.js
export let age = 10;
export let name = 'Good Name';
export function makeChange() {
age += 1; // change int primitive type
name += ' Updated'; // change string primitive type
}
// index.js
import {age, name, makeChange} from './module1.js'
console.log(~originals: ${age}, ${name}~);
makeChange();
console.log(~updated: ${age}, ${name}~);
// output:
originals: 10, Good Name
updated: 11, Good Name Updated
Class
Unlike object literals, a class definition is 'sort of' like a function definition.
Because of that, ',' causes SyntaxError
when running the following code:
class Task {
constructor() {
console.log('constructing Task');
},
showId() {
console.log('99');
}
}
let task = new Task();
But unlike a function, variables are not allowed to be declared inside a class. The following code also causes SyntaxError
:
class Task {
var a = 1;
constructor() {
console.log('constructing Task');
}
showId() {
console.log('99');
}
}
let task = new Task();
Just like functions, class definitions can be used in expressions.
Please notice though, once class definition is assigned to a variable, the original reference is not available outside of the class definition.
let newClass1 = class Task1 {
constructor() {
console.log('constructing Task');
}
};
new newClass1(); // works fine
new Task1(); // ReferenceError
let newClass2 = function Task2(){
console.log('constructing Task2');
};
new newClass2(); // works fine
new Task2(); // ReferenceError
Unlike functions, class definitions are not 'callable':
class Task {
constructor() {
console.log('constructing Task');
}
};
Task.call( {} ); // TypeError
Task.call(); //TypeError
Task.call(window); //TypeError
Unlike functions, class definitions are NOT polluting the global variable.
function Project1() { };
class Project2 {};
console.log(window.Project1 === Project1); // true
console.log(window.Project2 === Project2); // false
extends and super
The most basic form:
class Project {
constructor(name) {
console.log('constructing Project: ' + name);
}
}
class SoftwareProject extends Project {
constructor(name) {
console.log('constructing SoftwareProject: ' + name);
super(name);
}
}
new SoftwareProject('Mazatlan');
Default constructors for classes
When constructor is missing from base class, a default 'non-parameterized' one is created:
class Project {
}
class SoftwareProject extends Project {
constructor(name) {
console.log('constructing SoftwareProject: ' + name);
super(name);
}
}
new SoftwareProject('Mazatlan');
When constructor is missing from subclass, a default 'parameterized' one is created to make sure parameters can be passed onto super constructor.
// default constructor for subclass
constructor(...args) {
super(...args);
}
class Project {
constructor(name) {
console.log('constructing Project: ' + name);
}
}
class SoftwareProject extends Project {
}
new SoftwareProject('Mazatlan');
While it is ok not to have an explicit call to super constructor super()
in base class, it is not ok to omit super()
call in a subclass.
So long as there's a constructor in the subclass, a default constructor with super() call won't be created.
class Project {
constructor() {
// no super(), ok.
}
}
class SoftwareProject extends Project {
constructor() {
// no super(), NOT ok. ReferenceError.
}
}
new SoftwareProject('Mazatlan');
this
ordering
In a subclass, you must call super()
before you can use this:
class Project {
constructor() {
}
}
class SoftwareProject extends Project {
constructor(name) {
this.name = name; // causes error
super();
this.name = name; // works.
}
}
new SoftwareProject('Mazatlan');
static function are to be called by class, not the instances.
class Project {
static getName() {
return 'Project Name';
}
}
let p = new Project();
//Project.getName(); // TypeError
p.getName(); // works fine
new.target
is always pointing the initial class
class Project {
constructor() {
console.log(new.target);
}
}
class SoftwareProject extends Project {
constructor(name) {
super();
this.name = name;
}
}
new SoftwareProject();
/* output:
function class SoftwareProject extends Project {
constructor(name) {
super();
this.name = name;
}
}
*/
new.target
can be used to access static functions defined in the original class:
class Project {
constructor() {
console.log(new.target.getCompany());
}
}
class SoftwareProject extends Project {
static getCompany() {
return 'A good company';
}
}
new SoftwareProject();
Symbols
Symbol()
are creating new unique ones, even with same parameters.
let s1 = Symbol('name1');
let s2 = Symbol('name1');
console.log(s1 === s2); //false
Symbol.for()
on the other hand, only creates a new one when no existing one can be found
in the symbol registry.
let s1 = Symbol.for('name1');
let s2 = Symbol.for('name1');
console.log(s1 === s2); //true
Object properties created by Symbol are special ones that needs to be accessed in a special way:
let article = {
title: 'A title',
[Symbol.for('article')]: 'An article'
};
console.log( Object.getOwnPropertyNames(article) ); // ['title']
console.log( Object.getOwnPropertySymbols(article) ); // [Symbol(article)]