A JavaScript Primer with ES6
Fat Arrow Functions
They provide syntactical sugar and make the this
keyword resolve lexically.
const a = () => '5'
const b = num => num+1;
Above fat arrow examples show a.) implicit return and b.) no need of paranthesis when there is only a single parameter to be passed
class Foo {
constructor(name) {
this.name = name;
}
speak(){
console.log(`I am ${this.name}`);
}
}
var f = new Foo('sandeep');
f.speak(); // I am sandeep
let abc = f.speak;
abc(); // Cannot read property 'name' of undefined
Above happens because in this context this
is undefined.
To read more about Fat Array Functions go here: http://sandeep45.github.io/javascript/es6/functions/2016/02/08/fat-arrow-functions.html
Function Arguments
Default values can be assigned in the function declaration. Its basically checking if the values are undefined, and if that is the case it uses the value you passed in.
const printName = (name = 'Sandeep') => console.log(`I am ${name}`)
printName('Foo') // I am Foo
printName() // I am Sandeep
printName(undefined) // I am Sandeep
printName(void(0)) // I am Sandeep
Arguments can be deconstructed right in the function declaration. By adding three dots ...
before the name of the variable, we have made it an array which will catch all remaining parameters
const names = (mother, father, ...restOfFamily) => {
console.log(mother, father);
console.log(restOfFamily);
console.log(restOfFamily.length);
}
names("foo", "moo", "bar", "jones", "adam");
// foo, moo
// [bar, jones, "adam"]
// 3
A similar deconstructing can also be done when the argument is an object/Hash. By specifying keys of the hash we want, we can put them in a variable, and then optionally put the rest in a object/hash variable.
const names = ({mother, father, ...restOfFamily}) => {
console.log(mother, father);
console.log(restOfFamily);
console.log(restOfFamily.length);
};
names({
mother: 'foo',
father: 'moo',
brother: 'bar',
sister: 'jones',
uncle: 'adam'
});
// foo, moo
// {brother: 'bar', sister: 'jones', uncle: 'adam'}
// 3
We can also __deconstruct` similarly when the argument is an array. We can just specify variables at indexes we want and optionally put the rest in an array variable.
const names = ([mother, father, ...restOfFamily]) => {
console.log(mother, father);
console.log(restOfFamily);
console.log(restOfFamily.length);
};
names([
'foo',
'moo',
'bar',
'jones',
'adam'
]);
// foo, moo
// 'bar', 'jones','adam'}
// 3
You can also mix deconstructing of an object along with default values:
const a = ({x=100,y=200}) => {
console.log(x,y)
}
a({
x: 1,
z: 2,
});
// 1 200
Deconstructing
Selectively take what you want from an object by key and from an array by index.
var {name} = {name: 'Sandeep', age: 34}
console.log(name); // Sandeep
var {name:myFirstName} = {name: 'Sandeep', age: 34}
console.log(myFirstName); // Sandeep
var [name] = {Sandeep, 34}
console.log(name); // Sandeep
var [name,,id] = {Sandeep, 34, a1234}
console.log(name); // Sandeep
console.log(id); // a12345
You can read more on deconstructing here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters
For-Of Loop
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for…of
The for-of
loop is use it to iterate over an objects iterable items in random order. This is different from a for-in
loop which is used to iterate over an objects enumerable properties in their insertion/creation order.
const items = [1, 5, 10];
for(let i of items) {
console.log(i); // prints 1, then 5 and then 10
}
console.log(i); // i not defined as we used let vs var
This works like shown above, because the Array object has an iterator
function defined which is returning the items of the array as iterable items
. String, Map etc. also have their own version of iterator
functions.
Array.prototype.mynameis = 'sandeep';
let items = [1, 5, 10];
for(let i in items) {
console.log(i); // 0, 1, 2, mynameis
}
for(let i in items) {
console.log(items[i]); // 1, 5, 10, 'sandeep'
}
Now yes, we could use hasOwnProperty
to check and ignore properties which dont belong to items
and then lookup the values of those properties on items, to get the same answer, but that misses the point here, which is that for-of
iterates over iteratable items
which one may even customly defined by defining the iterator generator function.
Array.prototype.mynameis = 'sandeep';
let items = [1, 5, 10];
for(let i in items) {
if( items.hasOwnProperty(i) ) {
console.log(items[i]); // 1, 5, 10
}
}
let, const & var
Use let
to have a variable scoped to the block instead of the function, which is what var
does.
Since let
is scoped to the block, we dont have to deal with the issue of variables getting hoised to the top of the function. In the example below, we are declaring i variable, its hoised to the top and when the inner function runs on nextTick
they all see its last value and it prints the number 10, ten times.
for(var i=0;i<10;i++){
setTimeout(() => {
console.log(i); // 10 10 10 10 10 10 10 10 10 10
}, 0)
}
On the other hand, in the example below, we use let
and there is no hoisting and each function has its j and they dont see any common j
hoisted to the top. Therefore it prints the numnbers from 0 to 9.
for(let j=0;j<10;j++){
setTimeout(() => {
console.log(j); // 0 1 2 3 4 5 6 7 8 9
}, 0)
}
To learn more about let
: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let```
const
is also block scoped like let
and unlike var
which is scoped to the function. Once created, a const
can not be redefined and/or changed. Note, it doesn’t make the object it points to read-only. It merely prevent its reassignment.
Object property Definition
ES6 makes it a bit nice to define properties on objects as shown in the example below:
var info = {first: 'sandeep', last: 'arneja'};
var a = 1, b =2, c = 3;
var foo = () => console.log('hi');
var obj = {
a, b, c, foo,
name: 'sandeep',
bar() {
console.log("i am bar")
},
jones: () => console.log('i am jones'),
...info
};
console.log(obj);
/*
{ a: 1,
b: 2,
c: 3,
foo: [Function: foo],
name: 'sandeep',
bar: [Function: bar],
jones: [Function: jones],
first: 'sandeep',
last: 'arneja' }
*/
import & Export
skipped as not supported natively in NodeJS
NodeJS has its own module.exports
concept which works great!
// helper.js
const add = (...args) => args.reduce( (accumulator, currentValue) => accumulator + currentValue );
module.exports = add;
// index.js
const helper = require('./helper');
console.log(helper(1,2,3)); // 6
In the case above add
is the default
export, in other words the function is the only thing exported.
When there are more than 1 item to return its better to return an object, like shown below:
// helper.js
const add = (...args) => args.reduce( (accumulator, currentValue) => accumulator + currentValue );
module.exports = {add};
// index.js
const helper = require('./helper');
console.log(helper.add(1,2,3)); // 6
Promises
A promise
allows to return an object instead of asking to pass in a success and failure callback. This makes the code more linear and easier to read rather than having a right falling tree.
Without promises, if something was happening asynchronously, and we wished to run some code after it, we would pass in what we want to happen afterwards. These are known as callbacks and then we would trust that the async code will call our callback when the time is right.
Promises flip this setup on its head. Instead we expect the async function to return us a promise. We will then using the then
keyword setup what we want to run after the async function has finished. See below how its done by using promises.
const iAmAsyncFunction = () => {
return new Promise( (resolve, reject) => {
setTimeout( () => {
console.log('after 1 second I will run and return data back');
resolve(500)
}, 1000)
} );
}
iAmAsyncFunction().then(data => console.log('i got', data), undefined)
The function passed to the constructor of the Promise
is called the executioner
and it runs immediately and is passed in the resolve
and reject
methods. When these functions are called, their corresponding methods are called in the then
method which has been called on the promise
object.
Then
takes 2 functions as parameters. 1st one is called when the promise its setup on is resolved and then 2nd one is called when the promise is rejected.
Understanding Chaining
Promises can be chained using then
. This happens because each then
return an implict promise which is resolved with the value returned by then
. See example below:
const doSomething = () => {
var p = new Promise( (resolve,reject) => {
setTimeout(() => {
console.log('doing something async which takes 2 seconds');
resolve(100);
}, 2000)
});
return p;
};
const moreDoing = data => {
console.log("more doing immediately with: ", data);
return 5;
}
const doSomethingMore = data => {
console.log("doing something more immediately with: ", data);
return 500;
}
doSomething().then(moreDoing).then(doSomethingMore);
In the code above, first the async function runs, after it is done each of the setup functions runs after the other. The reason then
works on the return of then
is because each then
is returning a promise. This promise is implicitly being resolved with the value returned by the function its passed in. In our case that is moreDoing
which returns 5 and therefor that then
is returning a resolved promise with 5. Its like the then
function is calling Promise.resolve(5)
and returning that.
Now the function passed to then, may also doing async things and return a promise of its own. In the case, the next chained then
just works off the explicitly returned promise by the function passed in to then
. This works because Promse.resolve(aPromise)
is the same as aPromise
. Lets see an example:
const doSomething = () => {
var p = new Promise( (resolve,reject) => {
setTimeout(() => {
console.log('doing something async which takes 2 seconds');
resolve(100);
}, 2000)
});
return p;
};
const moreDoing = data => {
var p = new Promise( (resolve,reject) => {
setTimeout(() => {
console.log('moreDoing async which takes 2 seconds');
resolve(500);
}, 2000)
});
return p;
}
const doSomethingMore = data => {
console.log("doing something more immediately with: ", data);
return 5000;
}
doSomething().then(moreDoing).then(doSomethingMore);
Here the moreDoing
explicitly returns a promise and then the next then is built on top of that promise. This means doSomething
does not fire till moreDoing
finished and resolved the promise it has returned.
Understanding implicit return in .then
Now lets explore how this implicitly returned promise from then
works.
First lets take the case when we return a promise explicitly.
var p = new Promise((resolve, reject) => setTimeout(resolve, 2000))
p.then(() => console.log("its done"));
var p = new Promise((resolve, reject) => setTimeout(resolve, 2000))
Promise.resolve(p).then(() => console.log("its done"));
Both the snippets of code above, do the exact same thing, even though in the 2nd one I called Promise.resolve
on the promise
p
.
Now lets take the case when we return just a number and expect the implict promise to handle things.
var x = 5;
Promise.resolve(x).then(() => console.log("its done"));
The above code also works and runs immediately, therefore proving our point.
Understanding .catch
When promises are chained, they will look for their handler - success or failure and skip the chained items which dont defined their kind of handler. This is why adding a catch
all the way at the bottom works. Its working because the previous then
didnt define the failure handler.
Promise.reject(1).
then( () => {console.log('in then 1')}, undefined).
then( () => {console.log('in then 1')}, undefined).
catch( () => 'got the error' )
catch(fn)
is shorthand for then(undefined, fn)
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises
Making an old-school non promise function, work with promises
We can wrap the old-school callback
based function in a new function. In this new function we will call the old function and pass in a callback which will call resolve
.
// npm install ajax-request --save
var request = require('ajax-request');
request('https://www.google.com', function(err, res, body) {
console.log('got status code', res.statusCode, ' and body length', body.length);
});
// got status code 200 and body length 46014
var myRequest = url => new Promise( (resolve, reject) => {
request(url, (err, res, body) => resolve({res, body}) )
});
myRequest('https://www.google.com').then( ({res, body}) => {
console.log('got status code', res.statusCode, ' and body length', body.length);
});
// got status code 200 and body length 46950
Async/Await ECMA 2017
Read my detailed article here: http://sandeep45.github.io/redux/react/react-router/2019/02/11/aysnc_await.js.html
Generator
Generators are used to build generator functions. The generator function returns a value everytime next
is called on it. It keeps returning the next yield
and never the same one twice and stops once return
is called.
function* even_numbers(){
var num = 0;
while(true){
if(num > 10) {
return num;
}
else {
yield num;
}
num = num+2
}
}
const nums = even_numbers();
console.log(nums);
console.log("manually: ", nums.next());
console.log("manually: ", nums.next());
for(let i of nums){
console.log("from for-of loop", i);
}
/*
Object [Generator] {}
manually: { value: 0, done: false }
manually: { value: 2, done: false }
from for-of loop 4
from for-of loop 6
from for-of loop 8
from for-of loop 10
*/
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator
Keyed Collection - Map & Sets
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Keyed_collections
Map
Javascript already has objects, which we have always been using as hashes. We can easily add properties to this object and access them like a dictionary. But now we have Map’s. Map’s allow:
- keys of any type, not just string like an object.
- iterating over the results in their insertion order by the use of an iterator. This means that
new Map()[Symbol.iterator]
is present and defined. So you can callnext
and/or usefor-of
loop. - accessing the size of the hash.
var hash = new Map();
hash.set('name', 'sandeep');
hash.set(1,2);
hash.set(2,"arneja");
console.log(hash.size);
console.log(hash.get(2));
console.log(hash.keys());
console.log(hash.values());
console.log(hash[Symbol.iterator]);
const myIterator = hash[Symbol.iterator]();
console.log(myIterator.next());
console.log(myIterator.next());
console.log('--');
for(let i of hash){
console.log(i);
}
/*
3
arneja
[Map Iterator] { 'name', 1, 2 }
[Map Iterator] { 'sandeep', 2, 'arneja' }
[Function: entries]
{ value: [ 'name', 'sandeep' ], done: false }
{ value: [ 1, 2 ], done: false }
--
[ 'name', 'sandeep' ]
[ 1, 2 ]
[ 2, 'arneja' ]
*/
An object can be used to do all these things but without the nice helper things set
provides, but if you truly need an object, which has methods, which can operator on other properties of that object and access the this
keyword, then you should still use an object.
var hash = {}
hash['name'] = 'sandeep';
hash['change'] = function() {
this.name = 'arneja'
}
console.log(hash.name);
hash.change();
console.log(hash.name);
/*
sandeep
arneja
*/
WeakMap
Same like a set
, just that the key's
must be objects and will be garbage collected if they dont have any other reference to them. Also there is no way of getting list of keys, since key’s without reference should not exist, but may exist depedning on how the GC is feeling and therefore is non-deterministic.
It use is to store information about a key, without having memory leak, meaning that information should only be present as long as that key exists. This cant be dont with an array as an array would hold reference to they key, even after the key is no long reference by anyone else and is therefore of no use.
Use cases
https://stackoverflow.com/questions/29413222/what-are-the-actual-uses-of-es6-weakmap
If you want to add data about an object, but cant write in to that object, then store thata data in the WeakMap with the obect as the key. If you store it in a Map, or using 2 arrays, you will introduce a memory leak as your map or arrays will hold the value for every, even after that object ceases to exist. WeakMap’s solve this problem as the key ceases to exist in the weakmap as it allow garbage collection of its keys.
Set & WeakSet
This is the same as a regular set data structue. It only has values, all values are unique and can be iterated over in insertion order.
The same can be accomplished by using an array, but then you have to deal with smaller details like preventing duplicates, size etc.
A WeakSet, is the same, just that they keys can only be objects and maybe garbage collected when the key ceases to have any other reference.
Classes
Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
My detailed article on classes
here: http://sandeep45.github.io/es6/classes/javascript/2016/02/04/classes.html
and for this
go here: http://sandeep45.github.io/javascript/this/es6/fat-arrow/2016/02/03/this-keyword-in-javascript-part2.html
Body of a class runs in strict mode, so be aware. Body has space for 1 and only 1 constructor
method which has access to super
and can be used for object initialization.
super
is used to call the same method from its super class.
extends
is used for subclassing.
static
puts methods on the class
instead of the prototype
.
ECMA2016 has ability to write bounded instance methods, but ES6 doesn’t.
User Object.setPrototypeOf()
to have one object extend from another non-constructible object.
value of this
will be undefined when methods are not called directly from the object. this is because its running in strict mode.
class Person {
constructor(x,y){
this.x = x;
this.y = y;
}
sum() {
if(this === undefined){
console.log("unknown")
}else{
console.log(this.x+this.y);
}
}
}
const p = new Person(1,2);
p.sum();
const sum = p.sum;
sum();
/*
3
unknown
*/
Also not that when a method is defined like above, its being put on the prototype chain
console.log(Person.prototype.sum); // [Function: sum]
A Word on scope of this
for methods
By default its scope-by-flow
, so the value of this
depends on the context from where its called
class Calculator {
constructor(x,y){
this.x = x;
this.y = y;
}
sum() {
return this.x+this.y;
}
}
console.log(new Calculator(1,2).sum()); // 3
To prove that this
is scope by flow
, lets change how sum is called.
var me = {
sum: new Calculator(1,2).sum,
x: 5,
y: 6
}
console.log(me.sum()); // 11
To solve this issue in ES7, change the function definition to use fat-arrow (=>
) syntax and now this will only be resolved lexically.
In ES6, this can be solved by binding the function to this
in the constrcutor, so that no matter where its called from, the value of this
is what we want it to be.
class Calculator {
constructor(x,y){
this.x = x;
this.y = y;
this.sum = this.sum.bind(this); // money shoT!
}
sum() {
return this.x+this.y;
}
}
var me = {
sum: new Calculator(1,2).sum,
x: 5,
y: 6
}
console.log(me.sum()); // 3 ! Boom !