Table of Contents (Hide)

JavaScript

ECMAScript 6 (ES6)

This article describes the new features introduced in ECMAScript version 6 (or ES6, ES2015, ES6Harmony), formerly known as "ECMA-262 ECMAScript Language Specification 2015".

All the examples are tested under Node.js (see "Node.js How To" on how to setup and run JavaScripts).

For brevity, cleaner-looking and more meaningful codes, I omit the ending semicolons; use single quote for string; avoid foo-bar, i, j, k; use const instead of let and var; keep the sentences short and brief; ...

The var, let and const Variable Declarations

Prior to ES6, you could only use the keyword var to declare a variable. The var-variables fall into two scopes:

  1. Global Scope: var-variables declared outside functions have global scope. They can be accessed from any part of the script.
  2. Local-Function Scope: var-variables declared inside a function definition have local-function scope. They are accessible within the defining function only. A local-function variable having the same name as a global variable shadows the global variable inside the function.

The var-variable is peculiar! There is no local block-scope variables.

ES6 introduces local block-scope let and const declarations.

(Prior to ES6) var-Declarations are Hoisted to the Top of the File or Function

In most of the C-based languages, variables are created upon their declaration and exist within the block that they are declared. This is known as block-scope or lexical-scope. Recall that a block is a group of statements surrounded by curly braces {}.

JavaScript's var-declaration is peculiar! The var-variables are not block-scope but hoisted outside the defining blocks. var-declarations outside functions are hoisted to the top of the script; var-declarations inside a function definition are hoisted to the top of the function.

Consider the following code, you will be surprise that var-variable is accessible outside the defining block.

function testVarHoisting(boolean) {
   if (boolean) {
      console.log('msg in true-block before var-declaration is: ' + msg)
      var msg = 'hello'
         // var-declaration will be hoisted to the top of the function,
         // but NOT the initialization
      console.log('msg in true-block after var-declaration is: ' + msg)
   } else {
      console.log('msg in false-block is: ' + msg)
   }
   console.log('msg outside the block is: ' + msg)
}

testVarHoisting(true)
//msg in true-block before var-declaration is: undefined
//msg in true-block after var-declaration is: hello
//msg outside the block is: hello

testVarHoisting(false)
//msg in false-block is: undefined
//msg outside the block is: undefined

The var-declaration is actually hoisted to the top of the function, but the initialization remains at the same spot. In other words, JavaScript interprets the above codes as:

function testVarHoisting(boolean) {
   var msg   // var-declaration is hoisted to the top of the function
   if (boolean) {
      console.log('msg in true-block before var-declaration is: ' + msg)
      msg = 'hello'   // initialization remains at the same spot
      console.log('msg in true-block after var-declaration is: ' + msg)
   } else {
      console.log('msg in false-block is: ' + msg)
   }
   console.log('msg outside the block is: ' + msg)
}

Similarly, var-declaration inside the loop is also hoisted to the top of the file (or function). For example,

for (var number = 0; number < 3; ++number) {
      // var-declaration will hoisted to the top of the file/fucntion,
      // but NOT the initialization.
   console.log('number is: ' + number)
}
console.log('number outside the loop is: ' + number)
//number is: 0
//number is: 1
//number is: 2
//number outside the loop is: 3

Again, var-declaration is hoisted to global scope (top of the file) and interpret as follows:

var number   // var-declaration is hoisted to the top of the file/function
for (number = 0; number < 3; ++number) {   // initialization remains at the same spot
   console.log('number is: ' + number)
}
console.log('number outside the block is: ' + number)

The let Block-Level Declarations

ES6 introduces a new keyword let to declare block-scope variables. For examples,

function testLet(boolean) {
   if (boolean) {
      let msg = 'hello'     // let-variable has block-scope
      console.log('msg is: ' + msg)
   } else {
      console.log('msg is: ' + msg)
      //ReferenceError: msg is not defined
   }
   console.log('msg outside the block is: ' + msg)
   //ReferenceError: msg is not defined
}
for (let number = 0; number < 3; ++number) {   // let-variable has block-scope
   console.log('number is: ' + number)
}
console.log('number outside the loop is: ' + number)
//ReferenceError: number is not defined

The let-declarations are not hoisted to the top of the enclosing block. Hence, it is recommended to write the let-declarations at the top of the block so that the variables are available for the entire block.

Re-Declaring a Variable

If you re-declare a var-vaiable in the same scope, there is no syntax error, but the variable is overwritten, e.g.,

var num = 111
console.log(num)
//111

var num = 222   // Override the current value
console.log(num)
//222

var num   // Keep the current value
console.log(num)
//222

If you declare a global var-variable of the same name inside a function, the global variable is shadowed inside the function, e.g.,

var num = 111
function myFun() {
   var num = 222   // Shadow the global variable
   console.log('num inside the function is: ' + num)
}

myFun()
//num inside the function is: 222
console.log('num outside the function is: ' + num)
//num outside the function is: 111

Unlike var-variable, you cannot re-declare a let-variable in the same scope, e.g.,

let num = 111
let num = 222
//SyntaxError: Identifier 'num' has already been declared

You also cannot declare a let-variable having the same name as var-variable in the same scope, e.g.,

var num = 1
let num = 2   // let-declaration in the same scope is not permitted
//SyntaxError: Identifier 'num' has already been declared

However, you can use let to define a variable in a block, which shadows the global var-variable or let-variable of the same name. For example,

var num1 = 111
let num2 = 222

if (true) {
   let num1 = 333
   console.log('num1 inside the block is: ' + num1)
   //num1 inside the block is: 333
   let num2 = 444
   console.log('num2 inside the block is: ' + num2)
   //num2 inside the block is: 444
}
console.log('num1 outside the block is: ' + num1)
//num1 outside the block is: 111
console.log('num2 outside the block is: ' + num2)
//num2 outside the block is: 222

The const (constants) Block-level Declarations

The const (constant) declaration is the same as let-declaration, except that its value cannot be changed.

Constants must be initialized on declaration. For example,

const num
//SyntaxError: Missing initializer in const declaration

const num = 1
num = 2
//TypeError: Assignment to constant variable

A const-declaration prevents modification of the binding (or reference), not the value. In other words, const-declarations for arrays/objects permit modification of their elements. For example,

const arr = [1, 2, 3]
arr[0] = 4
console.log(arr)
//[ 4, 2, 3 ]
arr = [4, 5, 6]
//TypeError: Assignment to constant variable

const obj = {a: 1}
obj.a = 2
console.log(obj)
//{ a: 2 }
obj = {b: 2}
//TypeError: Assignment to constant variable

let and const in for(;;), for..in and for..of

You need to use let to declare the index for the traditional for(;;) loop, NOT const, as the index changes over iterations. For example,

let fruits = ['apple', 'banana']
for (let i = 0; i < fruits.length; ++i) {
   console.log(fruits[i])
}
//apple
//banana

for (const i = 0; i < fruits.length; ++i) {
   console.log(fruits[i])
}
//apple
//TypeError: Assignment to constant variable.

However, the for..of and for..in loop, const declaration is recommended. For example,

let fruits = ['apple', 'banana']
for (const fruit of fruits) {
   console.log(fruit);
}
//apple
//banana

for (const idx in fruits) {
   console.log(fruits[idx]);
}
//apple
//banana

A closer look reveals that for..of and for..in loops create a new block scope with each iteration. That is, each new element or index is actually a new variable within a new scope and our constant is never reassigned.

As an illustration, you can see that changing the index in the for..in loop has no effect in the next iteration. That is, a new block-scope variable is created for each iteration.

let fruits = ['apple', 'banana', 'orange']
for (let idx in fruits) {  // use let to change the index for illustration
   console.log(idx, fruits[idx]);
   idx *= 2;  // change the index has no effect on the next iteration
}
//0 apple
//1 banana
//2 orange

Function Declaration in Loop with let-declaration

Consider the following function declaration inside a loop with var-declaration:

var funArray = []
for (var i = 0; i < 3; i++) {  // one copy hoisted to the top and shared
   funArray.push(function () {
      console.log('i is: ' + i);  // reference the same global i
   })
}

funArray.forEach(function (f) {
   f();  // reference the same global i, which has value of 3.
})
//i is: 3
//i is: 3
//i is: 3

The result is probably not desirable. This is because the same i is shared across all iterations of the loop. All the functions created inside the loop hold the same reference.

Let change the var-declaration to let-declaration:

var funArray = []
for (let i = 0; i < 3; i++) {   // local block-scope copies
   funArray.push(function () {
      console.log('i is: ' + i);  // each function has its own block-scope i
   })
}

funArray.forEach(function (f) {
   f();  // reference its own copy
})
//i is: 0
//i is: 1
//i is: 3

For let-declaration inside a loop, a new variable of the same name is created on each iteration with value from the previous iteration. In the above code, each function gets its copy of variable i with the appropriate value.

Best Practices

If you are programming in ES6, use const/let instead of var for proper block scope to prevent accidental erros. Use const to prevent unexpected changes. Use let if the value needs to be changed. Use var only for backward compatibility.

Function Syntax Enhancements

Prior to ES6, JavaScript functions support variable number of parameters. Furthermore, you can pass fewer or more arguments than formally specified. The arguments are kept in an object called arguments. For example,

function foo(p1, p2) {   // defined with 2 parameters
   console.log(p1, p2)
   console.log(arguments)
}

foo(1, 2)  // matching number of arguments
//1 2
//[Arguments] { '0': 1, '1': 2 }
foo()      // no arguments
//undefined undefined
//[Arguments] {}
foo(1)     // fewer arguments
//1 undefined
//[Arguments] { '0': 1 }
foo(1, 2, 3)   // more arguments
//1 2
//[Arguments] { '0': 1, '1': 2, '2': 3 }

ES6 introduces enhancements to functions.

Functions with Default Parameter Values

Prior to ES6, you can use the following pattern to provide default parameter values to function:

function greets(name, callback) {
   name = name || 'everybody'
   callback = callback || function(n) { console.log(`hello, ${n}`) }
   callback(name)
}

greets()
//hello, everybody
greets('peter')
//hello, peter
greets('peter', console.log)
//peter

Sometimes, you may need to check the type of the argument to accept arguments like number 0 or an empty string, which evaluated to false :

name = (typeof name === 'undefined') ? 'Everybody' : name
callback = (typeof callback === 'undefined') ? function(n) { console.log(`hello, ${n}`) } : callback

In ES6, you can simply write out the default parameter values in the form of param = defaultValue, as follows:

function greets(name = 'Everybody', callback = function(n) { console.log(`hello, ${n}`) }) {
   callback(name)
}

Another example:

const myFun = function (p1, p2 = 99, p3 = 'hello') {
   console.log(p1, p2, p3)
   console.log(arguments)
}

myFun(1)
//1 99 hello
//[Arguments] { '0': 1 }

myFun(1, 2)
//1 2 hello
//[Arguments] { '0': 1, '1': 2 }

myFun(1, 2, 3)
//1 2 3
//[Arguments] { '0': 1, '1': 2, '2': 3 }

myFun()
//undefined 99 hello
//[Arguments] {}

myFun(1, 2, 3, 4)
//1 2 3
//[Arguments] { '0': 1, '1': 2, '2': 3, '3': 4 }

The "Rest" Function Parameter (...)

ES6 supports packing of the rest of function arguments (after the named arguments), denoted by three dots (...), into an array, for example,

function foo(p1, p2, ...more) {
   console.log(p1, p2)
   console.log(more)
   console.log(arguments)
}

foo(1, 2, 3, 4, 5)
//1 2
//[ 3, 4, 5 ]
//[Arguments] { '0': 1, '1': 2, '2': 3, '3': 4, '4': 5 }
foo()
//undefined undefined
//[]
//[Arguments] {}
foo(1)
//1 undefined
//[]
//[Arguments] { '0': 1 }
foo(1, 2)
//1 2
//[]
//[Arguments] { '0': 1, '1': 2 }
foo(1, 2, 3)
//1 2
//[ 3 ]
//[Arguments] { '0': 1, '1': 2, '2': 3 }

The rest parameters must be last function parameter.

In some situations, rest parameter could provide more convenience than arguments object.

The "Spread" Operator (...)

Spread operator has the same 3-dot notation (...) as rest parameter. While the rest parameter packs multiple function arguments into an array, the spread operator splits (or spreads, or de-structure) an array into separate arguments to pass into a function. For example,

let numbers = [3, 2, 5, 1, 4]
console.log(Math.max(numbers))   // does not accept array
//NaN
console.log(Math.max(...numbers))   // same as: Math.max(3, 2, 5, 1, 4)
//5
console.log(Math.max(...numbers, 9))   // same as: Math.max(3, 2, 5, 1, 4, 9)
//9

The spread operator can also be used to combine the contents of arrays. For example,

let arr1 = [1, 2, 3],
    arr2 = ['a', 'b']

let arr3 = [...arr1, ...arr2]
console.log(arr3)
//[ 1, 2, 3, 'a', 'b' ]

let arr4 = ['a', ...arr1, 'b']
console.log(arr4)
//[ 'a', 1, 2, 3, 'b' ]

Spread operator can also be used on the LHS of the assignment:

let arr = [1, 2, 3, 4, 5]

let [i1, i2] = arr
console.log(i1, i2)
//1 2

let [ , , i3, ...rest] = arr
console.log(i3)
//3
console.log(rest)
//[ 4, 5 ]

You can clone an array via spread operator, as follows:

let arr1 = [1, 2, 3, 4, 5]
let arr2 = [...arr1]   // clone
arr1[0] = 6
console.log(arr1)
//[ 6, 2, 3, 4, 5 ]
console.log(arr2)
//[ 1, 2, 3, 4, 5 ]

You can extract the first and last item of an array:

let arr = [1, 2, 3, 4, 5]
let [first] = arr
let [last] = [...arr].reverse()   // reverse() is mutable!
console.log(first)
//1
console.log(last)
//5
console.log(arr)
//[ 1, 2, 3, 4, 5 ]

Spread operator is also applicable for object de-structuring (to be discussed later).

"Arrow Function" Notation

ES6 introduces a new syntax (notation) for defining function, called arrow function, in the form of (parameters) => {body}. The function keyword is removed. This allows you to write out the entire function in one line.

const increment = value => value + 1
// same as:
// const increment = function(value) { return value + 1 }
console.log(increment(1))
//2

const sum = (value1, value2) => value1 + value2
// same as:
// const sum = function(value1, value2) { return value1 + value2 }
console.log(sum(1, 2))
//3

For functions with one parameter, you can omit (). For functions with one-statement body, you can omit {} and the return keyword.

const max = (value1, value2) => {
   console.log(value1, value2)
   return value1 > value2 ? value1 : value2
}
console.log(max(3, 4))
//3 4
//4

For functions with no parameter, you need to write an empty ().

const hello = () => console.log('hello, world')
hello()
//hello, world

const doNothing = () => {};  // with empty braces

To return an object, wrap it with parentheses (this is because the braces create a block).

const getObject = (name, age) => ( {name: name, age: age} )
console.log(getObject('Peter', 21))
//{ name: 'Peter', age: 21 }

The array methods that accept callback functions, such as sort(), map(), and reduce(), can be benefit from simpler arrow function notation. For example,

const values = [1, 2, 3, 4]
let results = values.map(value => value * value)
// same as:
// let results = values.map(function(value) {value * value})
console.log(results)
//[ 1, 4, 9, 16 ]

You use also use the arrow function syntax on call(), apply(), and bind(), e.g.,

const sum = (value1, value2) => value1 + value2
console.log(sum.call(null, 1, 2));
//3
console.log(sum.apply(null, [1, 2]));  // apply() works on array
//3
let boundSum = sum.bind(null, 1, 2);
console.log(boundSum());
//3

Object/Array Enhancements

Object Literal Enhancement

ES6 introduces simpler object literal for creating objects. For example,

let name = 'Peter',
    age = 21,
    height = 180
    
let person = { name, age, height }  // object literal - keynames to match variable names
// Same as:
// let person = { name: name, age: age, height: height }
console.log(person)
//{ name: 'Peter', age: 21, height: 180 }

You can also use the object literal in function:

// const createPerson = (name, age) => (
//    {
//       name: name,
//       age: age
//    }
// )
// Can be written as:
const createPerson = (name, age) => ( { name, age } )   // keynames to match parameters
console.log(createPerson('Peter', 21))
//{ name: 'Peter', age: 21 }
//const createPerson = (name) => (
//   {
//      name: name, 
//      greets: function() {
//         console.log(`hello, ${name}`)
//      }
//   }
//)
// Can be written as:
const createPerson = (name) => (
   {
      name,          // property initializer shorthand
      greets() {     // concise method shorthand
         console.log(`hello, ${name}`)
      }
   }
)
createPerson('Peter').greets()
//hello, Peter

[TODO] More

Object De-Structuring Assignment

Prior to ES6, fetching data from objects into local variables requires a lot of duplicate code. For example,

let person = {
   name: 'Peter',
   age: 21,
   gender: 'm',
   height: 180
}

let name = person.name,
    age = person.age,
    gender = person.gender,
    height = person.height
    
console.log(name, age, gender, height)
//Peter 21 m 180

ES6 introduces object destructuring syntax to de-structure an object and assign into variables:

let person = {
   name: 'Peter',
   age: 21,
   gender: 'm',
   height: 180
}

let { name, age, gender, height } = person   // de-structure an object, assign to variables
    
console.log(name, age, gender, height)
//Peter 21 m 180

Take note that:

  • The variable names shall match the property (key) names.
  • You do not need to de-structure the entire object.
  • Any variable names that do not matched the property names are set to undefined.
let { name, age } = person  // Declare and initialize
console.log(name, age)
//Peter 21

person.age++
let weight                         // Declare
({ name, age, weight } = person)   // Assignment needs to be surrounded by ()
console.log(name, age, weight)
//Peter 22 undefined

You can use de-structure to declare and initialize new variables via let, const or var. You can also use it in assignment with existing variables by enclosing the statement with parentheses (this is because the braces create a block statement).

You can also set the default values, which will be used if the property is missing, e.g.,

let { name, age = 18, weight = 80 } = person   // provide default values
    
console.log(name, age, weight)
//Peter 21 80

You can use variable names different from the property names, as follows:

let { name: theName, age: theAge } = person   // change the variable name
    
console.log(theName, theAge)
//Peter 21
//console.log(name, age)
//ReferenceError: name is not defined

You can apply de-structuring to nested objects as follows:

let person = {
   name: 'Peter',
   age: 21,
   gender: 'm',
   height: 180,
   likes: {
      sports: 'football',
      book: 'JavaScript for dummies'
   }
}

let { name, likes: { sports } } = person  // nested objects
    
console.log(name, sports)
//Peter football

If the property names are constructed dynamically using expressions, you need to surround them by square bracket [], e.g.,

let person = {
   name: 'Peter',
   age: 21
}
let theName = 'name'

let {[theName]:x, age} = person

console.log(x, age)
//Peter 21

Array De-structuring Assignment

Array destructuring syntax is similar to object destructuring. You need to provide the variable names (because there are no keys in array).

let coffees = ['cappuccino', 'espresso', 'latte', 'mocha']

let [coffee1, coffee2] = coffees
console.log(coffee1, coffee2)
//cappuccino espresso

let [ , , coffee3] = coffees
console.log(coffee3)
//latte

let [coffee4, ...coffee5] = coffees   // rest, must be the last element
console.log(coffee4)
//cappuccino
console.log(coffee5)
//[ 'espresso', 'latte', 'mocha' ]

Another example:

let [v1, v2, v3] = [1, 2, 3]
console.log(v1, v2, v3)
//1 2 3

let [v4, , v6] = [4, 5, 6]   // omit elements
console.log(v4, v6)
//4 6

let [v7, v8] = [7]   // more variables than elements
console.log(v7, v8)
//7 undefined

let [v9, ...v10] = [9, '10a', '10b', '10c']   // rest parameter
console.log(v9, v10)
//9 [ '10a', '10b', '10c' ]

let [v11, v12 = 'a', v13 = 'b'] = [11, 12]   // with default values
console.log(v11, v12, v13)
//11 12 b

let [v14, [v15a, v15b]] = [14, ['15a', '15b']]   // nested arrays
console.log(v14, v15a, v15b)
//14 15a 15b

Using Object Destructor in Function Parameter

You can use object de-structor in function parameter to extract selected data from an object argument. For example,

function foo(greeting, {name}) {  // name is to be de-structured from an object argument
   console.log(`${greeting}, ${name}`)
}

foo('hello', {name: 'Peter', age: 21})
//hello, Peter

Functional Programming (Filter-Map-Reduce Pattern)

Functional Programming is used more and more these days (especially in data analytics). A popular pattern is to filter, map (transform) and reduce an array (via parallel computing). Functional programming is a part of a larger programming paradigm called declarative programming. In declarative programming (against imperative programming), we describe what should happen instead of how it should happen (e.g., via low-level loops and if-else statements).

Functioning programming employs these concepts: immutability, purity, data transformation, higher-order functions, and recursion.

Functions are first-class object (or first-class members) in JavaScript. A function can take another function as its argument. A function can also return another function. In other words, a function can do whatever a variable can do. They are also known as higher-order functions.

Immutability

In functional programming, data shall never change. Instead, we shall clone the data and work on the cloned copy if changes are needed.

Pure Functions

A pure function is a function that takes at least one argument and returns a value that is computed based on its arguments. They do not have side effects, use global variables, or modify the application state. The arguments are immutable. One important benefit of pure functions is they are testable.

Data Transformation and Filter-Map-Reduce Pattern

JavaScript provides these functions: Array.filter(), Array.map() and Array.reduce() to transform data.

The inArray.filter(predicate)->outArray is an immutable function that takes one argument, called predicate, and produces a new array from inArray. The predicate(inValue)->true|false is a immutable one-argument function that returns a boolean value of true or false, based on inValue. This predicate is applied for every item of inArray, and the item is added to the output array only if the result is true. For example,

let inArr = [5, 2, 6, 1, 8]
let outArr = inArr.filter(item => item > 5)

console.log(outArr)
//[ 6, 8 ]
console.log(inArr)     // immutable / pure function
//[ 5, 2, 6, 1, 8 ]

The inArray.map(mapFunc)->outArray is an immutable function that take one argument, a mapping function, and produces a new array from inArray. The mapFunc(inValue)->outValue is an immutable one-argument function that maps inValue to outValue. The mapping function is applied to every item of inArray, and the resultant value is added to the output array. For example,

let inArr = [5, 2, 6, 1, 8]
let outArr = inArr.map(item => ++item)

console.log(outArr)
//[ 6, 3, 7, 2, 9 ]
console.log(inArr)     // immutable / pure function
//[ 5, 2, 6, 1, 8 ]

The inArray.reduce(reduceFunc)->resultValue is an immutable functions that take one argument, a reduce (or aggregate) function, and produce one value from inArray. The reduceFunc(callback, initResult)->result is a two-argument function: a callback function and an initial value. The callback(item, result) is applied to every item of the inArray to update the result. For example,

let inArr = [5, 2, 6, 1, 8]
let mySum = (value1, value2) => value1 + value2
let resultValue = inArr.reduce(mySum, 0)

console.log(resultValue)
//22
console.log(inArr)     // immutable / pure function
//[ 5, 2, 6, 1, 8 ]

An alternative called inArray.reduceRight(), which works like reduce(), but starts reducing from the end of the array.

We often chain up these function in a pattern called filter-map-reduce. For example,

let inArr = [5, 2, 6, 1, 8]
let mySum = (value1, value2) => value1 + value2
let resultValue = inArr
                  .filter(item => item > 5)
                  .map(item => ++item)
                  .reduce(mySum, 0)

console.log(resultValue)
//16
console.log(inArr)     // Immutable
//[ 5, 2, 6, 1, 8 ]

The filter(), map(), reduce() functions can be applied to any types, including primitives, arrays, objects, and functions. It can also take values of one type and produces result in another type.

Higher-Order Functions

Higher-order functions are functions that can manipulate other functions. They can take functions as arguments, or return a function, or both. Higher-order functions are essential in functional programming.

JavaScript's functions are higher-order functions. The filter(), map(), reduce() are higher-order functions because they take function as their argument. Let write a simple higher-order function that takes functions as argument and returns a function:

// If result is true, run success(); else fail()
let foo = (result, success, fail) => result ? success() : fail()

let sayYes = () => console.log('yes')
let sayNo = () => console.log('no')

foo(true, sayYes, sayNo)
//yes
foo(false, sayYes, sayNo)
//no

[TODO] more on curried function

Recursion

Recursion is a technique that involves creating functions that call themselves. For example,

[TODO]

Althought functional programming advocates the use of recursions over loops, not all JavaScript engine are optimized for a large amount of recursion.

Recursion works well with asynchronous processes. Functions can re-call themselves when they are ready. Recursion also works well for searching tree-like data structure, e.g., iterating through the HTML DOM to reach a leaf node.

Composition

Functional programs compose of many small pure functions that focus on specific tasks. These smaller functions may run in series or parallel.

The most common composition technique is chaining. In JavaScript, functions can be chained together using dot notation to act on the return value of the previous function. For example, func1(p1).func2(p2).func3(p3).

You can also pipe the output of one function into another function, which is harder to keep track. For example, func3(func2(func1(param)).

A recommended approach is to create a larger higher-order function, using the smaller functions as its arguments. For example, compose(func1, func2, func3, ...). The steps are clear marked out.

[TODO] Example

Promises and Asynchronous Programming

JavaScript, created for client-side web programming, need to handle asynchronous user interaction, such as mouse-clicks and key-presses to reload contents (or part of the document).

Promise is a way for handling asynchronous programming, similar to future and deferred in other languages. Like event-handlers and callbacks, a promise specifies some code to be executed later. But promise needs to indicate if the code succeed or fail.

JavaScript Event Handling Model

JavaScript maintains a single-thread event-handling loop. Whenever an event (such as mouse-click or key-press) is triggered, the corresponding event-handler is added to the back of the event-handling queue. For example,

let btnHello = document.getElementById('btnHello')
btnHello.onclick = function(event) {   // button onclick event-handler
   console.log('hello, world')
}

The event-handler will not be executed until the button is click. When the button is clicked, the event-handler is added to the back of the event-handling queue.

Promises

A promise is a placeholder for the result of an asynchronous operation. For example,

// The asynchronous function readFile will return a promise at some point in the future
let promise = readFile('test.txt')

A promise starts in the pending state, which indicate the asynchronous operation has not completed yet. When the asynchronous operation completes, the promise is settled and enter either fulfilled (succeed) or rejected (fail) state. An internal property [[PromiseState]] keep track of the state of "pending", "fulfilled" or "rejected". A Promise.then() method is present on all promises, which takes two arguments: callback function for fulfilled and callback function for rejected. For example,

let promise = readFile('test.txt')

promise.then(function(contents) {
   console.log(contents)  // fulfilled
}, function(err) {
   console.log(err)       // rejected
})

Both the callback functions are optional. You can decide to handle either one, both, or none. For example,

// Fulfilled only
promise.then(function(contents) {
   console.log(contents)
})

// Rejected only
promise.then(null, function(err) {
   console.log(err)
})

You can also use Promise.catch() method for rejected-only handlers. For example,

promise.catch(function(err) {
   console.log(err)   // rejected
})
Creating Promises

A promise can be crated using the Promise constructor, which accepts a single function argument called executor(). The executor in turn takes two function arguments: resolve() and reject().

function readFile(filename) {
   return new Promise(function(resolve, reject) { ... })
}

[TODO] more

Classes

Prior to ES6, JavaScript had no classes. However, you can create class-like structure by creating a constructor function, and add methods to the constructor function object using prototype. For example,

// Define a constructor function for the instances
function Person(name, age) {
   this.name = name
   this.age = age
}

// Add method to the constructor's prototype, to be shared by all instances of Person objects 
Person.prototype.desc = function() {
   return 'Person{name:' + this.name + ', age:' + this.age + '}'
}

// Create a new instance of Person object
let peter = new Person('Peter', 21)  
console.log(peter.desc())
//Person{name:Peter, age:21}
console.log(peter)
//Person { name: 'Peter', age: 21 }
console.log(peter instanceof Person)
//true
console.log(peter instanceof Object)
//true
console.log(typeof Person)
//function
console.log(typeof Person.prototype.desc)
//function

ES6 introduces class declaration:

class Person {
   constructor(name, age) {
      this.name = name
      this.age = age
   }
   desc() {
      return `Person{name:${this.name}, age:${this.age}}`
   }
}

let peter = new Person('Peter', 21)
console.log(peter.desc())
//Person{name:Peter, age:21}
console.log(peter)
//Person { name: 'Peter', age: 21 }
console.log(peter instanceof Person)
//true
console.log(peter instanceof Object)
//true
console.log(typeof Person)
//function
console.log(typeof Person.prototype.desc)
//function

ES6 introduces class keyword to declare a class. The constructor function is defined inside the class. Methods can also be added inside the class. Clearly, ES6 class declaration is just a sugar coat over the pre-ES6 implementation.

Similar to function, classes have two forms: declaration (as above) and expression (as below).

let Person = class {   // no identifier after keyword class
   constructor(name, age) {
      this.name = name
      this.age = age
   }
   desc() {
      return `Person{name:${this.name}, age:${this.age}}`
   }
}

let peter = new Person('Peter', 21)
console.log(peter.desc())
//Person{name:Peter, age:21}

[TODO] getter and setter

Classes are First-Class Citizens

JavaScript classes, like functions, are first-class citizens (or first-class members). In other words, they can be assigned to variables, passed into a function as arguments, and returned from a function. For example,

// function which takes a class definition and return an instance
function createInstance(classDef) {
   return new classDef()
}

// A class definition
class Hello {
   sayHello() { console.log('hello, world') }
}

// Invoke function to create an instance
let obj = createInstance(Hello)

obj.sayHello()
//hello, world

You can create subclass via the extends keyword:

// Superclass
class Person {
   constructor(name, age) {
      this.name = name
      this.age = age
   }
   desc() {
      return `Person{name:${this.name}, age:${this.age}}`
   }
}

// Subclass
class Student extends Person {
   constructor(name, age, school) {
      super(name, age)
      this.school = school
   }
   desc() {
      return `Student{${super.desc()}, school:${this.school}}`
   }
}

let paul = new Student('Paul', 18, 'ABCU')
console.log(paul.desc())
//Student{Person{name:Paul, age:18}, school:ABCU}
console.log(paul)
//Student { name: 'Paul', age: 18, school: 'ABCU' }

[TODO] More

Modules

ES6 introduces support for modules, which are pieces of reusable code that can be imported into other JavaScript files.

JavaScript modules are stored in separate files, one file per module. You can export one or many JavaScript objects from a module. For example,

export const print(message) => console.log(`${new Date()}: ${message}`)
export const log(message, dateTime) => console.log(`${dateTime.toString()}: ${message}`)

You can export any JavaScript types: primitives, objects, arrays and functions.

If only one item is exported from a module, you can use keyword export default instead, for example,

const logMsg = (message, dateTime) => console.log(`${dateTime.toString()}: ${message}`)
export default logMsg

Module can be used by other JavaScript files using the import statement.

import { print, log } from 'module1'
import logMsg from 'module2'