ES2015

A better length method for unicode text

1
2
3
4
function codePointLen(text) {
let result = text.match(/[\s\S]/gu);
return result ? result.length : 0;
}

Default param

A trivial version

1
2
3
4
function getID(name, defaultValue) {
defaultValue = defaultValue || 1000;
// ...
}

The problem is defaultValue would be ignored with falsy value like 0. As a result, a more decent version before ES2015 is

1
2
3
4
function getID(name, defaultValue) {
defaultValue = (typeof defaultValue !== "undefined") ? defaultValue : 0;
// ...
}

In ES2015, it can be simplified as

1
2
3
function getID(name, defaultValue=1000) {
// ...
}

Apply, call and array destructuring

1
2
3
4
5
6
7
8
9
10
let values = [4,3,1,2];

// Because Math.max doesn't accept array. Please remember that "call" accepts multiple params and do the same function invocation as "apply"
console.log(Math.max.apply(Math, values));

// The first param can be anything since max is a pure function (instead of a method)
console.log(Math.max.apply(null, values));

// But it's easier with array destructuring
console.log(Math.max(...values));

How to check whether a function is called with “new”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Person(name) {
if (this instanceof Person) {
this.name = name;
} else {
throw new Error("Call Person with new, please");
}
}

let p1 = new Person("tom");

// It can be checked by Person and throw the error
// let p2 = Person("jerry");

// No error with this call
let p2 = Person.call(p1, "jerry");

console.log(p1.name); //jerry
console.log(p2); //undefined

ES2015 fixes the issue with:

1
2
3
4
5
6
7
8
function Person(name) {
// "new.target" is not supported in safari (on iPhone, iPad and Mac)
if (typeof new.target !== "undefined") {
this.name = name;
} else {
throw new Error("Call Person with new, please");
}
}

Note that if we can modify the above comparison to create an abstract class

1
2
if (new.target === Person) 
throw new Error("Abstract class Person can't be initilized directly");

Differences between arrow function and normal one

  • No this, super and new.target bindings; No prototype and arguments
  • Can’t be called with new (since arrow function doesn’t have [[Construct]], only [[Call]])
  • Can’t change this, which remains the same throughout the life cycle of the function

Tail call optimization

In theory, the optimization is enabled when the tail call has no dependency to the current stack and there is no further work to do after the call, plus the call must return as the function value. So this won’t be optimized:

1
2
3
4
5
6
7
function fact(n) {
if (n <= 1) {
return 1;
} else {
return n * fact(n-1);
}
}

In fact, I notice it is quite efficient (in safari on Mac). fact(170), the maximum param making the result less than infinity, is as fast as below, which is regarded as optimized:

1
2
3
4
5
6
7
function fact(n, p=1) {
if (n <= 1) {
return 1*p;
} else {
return fact(n-1, n*p);
}
}

Here is another example:

1
2
3
4
5
6
7
8
9
10
11
12
13
function fibo(n, i=2, p1=1, p2=1) {
if (n < 0) {
throw new Error("param n can't be less than zero");
} else if (n < 2) {
return 1;
} else {
if (n === i) {
return p1 + p2;
} else {
return fibo(n, i+1, p1+p2, p1);
}
}
}

Property Initializer

No need in ES2015 to repeat the value assignment for each property. The eigine will look for the variable in the surrounding context with the same name and do the job.

1
2
3
4
5
6
function createPerson(name, age) {
return {
name,
age
};
}

Two ways to clone an array

1
2
3
let list = ["dog", "cat", "Sam"];
let another = list.concat();
let second = [...list];

Make the last param optional to provide multiple configuration

1
2
3
4
5
function config(id, value, options = {}) {
let {core:{name}, time=new Date(Date.now()), user="Unknown User"} = options;
return {name,time,user};
}
let options = config(1, "abc", {core:{name:"abc"}, user:"Sam"});

If we destructure options inside param list, then we have to refer to the param as arguments[2]. What’s more, it seems to me confusing to have both default value for parameter and inside object destructuring. As a result, I perfer this style.
Also notice the support of nested structure (core:{name}).

Create array-like object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let myList = {
items: [],
*[Symbol.iterator]() {
for (let i of this.items) {
yield items[i];
}
}
};

myList.items.push(1);
myList.items.push(2);
myList.items.push(3);

for (let item of myList.items) {
console.log(item);
}

Don’t forget the comma at the end of items
Be careful about the compatible issue on different browsers

Loop through a Map

The default iterator of Map returns [aKey, aValue] for each key. So it is possible to write for-loop like this:

1
2
3
for (let [aKey, aValue] of aMap) {
console.log(`${aKey} has the value ${aValue} `)
}

for…in vs for…of

The former is to loop properties, while the later is for iterator. for…of is also a solution to unicode text.
Something more about in operator

1
2
3
4
5
6
7
8
9
// Bad
if (typeof obj['aProperty'] !== 'undefined') {
//...
}

// Good
if ('aProperty' in obj) { // or if (obj.hasOwnProperty('aProperty')) {
//...
}

Spread Operator

1
2
3
4
let smallNumbers = [1,2,3],
middleNumbers = [10,20,30],
largeNumbers = [100,200,300],
allNumbers = [...smallNumbers,...middleNumbers,...largeNumbers];

Generator

Understand the input parameter of next()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function *gen() {
// XXX yield statement evaluates first before the variable assignment
let v1 = yield 1;
let v2 = yield 2 + v1;
let v3 = yield 3 + v2;
yield v3;
}

let values = gen();

// the main thread (MT) jumps into gen and jump out with value 1
// The first time of next() is special because the input "10" is assigned nowhere.
console.log(values.next(10).value);

// MT jumps into gen again at the last place when gen jumped out.
// So 20 is assigned to v1 and then MT jumps out of gen with value 2+v1=22
console.log(values.next(20).value);

// MT throws an error, which cancels the whole gen.
// So v3 has NO value and there is no chance to do the last two yields
console.log(values.throw(new Error("WTF")).value);

Return in an iterator (generator)

Both spread operator and for…of can work with iterator.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function *gen() {
yield 1;
yield 2;
return 55;
yield 3;
}

// case 1
let g = gen();
for (let i of g) {
console.log(i);
}
// 1,2

// case 2
let g2 = gen();
console.log([...g2]); // 1,2

// case 3
let g3 = gen();
console.log(g3.next().value); // 1
console.log(g3.next().value); // 2
console.log(g3.next().value); // 55 (done is set because of return)
console.log(g3.next().value); // undefined

In the first two cases, they stop as soon as they see the done is set. The return value is ignored as a result.

Singleton Pattern with class expression

1
2
3
4
5
let person = new Class {
constructor(name) {
this.name = name;
}
}("Sam");

Mixin with class extend

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let Foo = {
doFoo() {}
};

let Bar = {
doBar() {}
};

function mixin(...mixins) {
let base = function() {};
Object.assign(base.prototype, ...mixins);
return base;
}

class Alien extends mixin(Foo, Bar) {
}

let anAlien = new Alien();
anAlien.doFoo();
anAlien.doBar();

The point is a class can extend with an expression (mixin() in this case)