- Notifications
You must be signed in to change notification settings - Fork0
ES2015 [ES6] cheatsheet containing tips, tricks, best practices and code snippets
albertolarah/es6-cheatsheet
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
A cheatsheet containing ES2015 [ES6] tips, tricks, best practices and codesnippet examples for your day to day workflow. Contributions are welcome!
- var versus let / const
- Replacing IIFEs with Blocks
- Arrow Functions
- Strings
- Destructuring
- Modules
- Parameters
- Classes
- Symbols
- Maps
- WeakMaps
- Promises
- Generators
- Async Await
Besides
var
, we now have access to two new identifiers for storing values—let
andconst
. Unlikevar
,let
andconst
statements are not hoistedto the top of their enclosing scope.
An example of usingvar
:
varsnack='Meow Mix';functiongetFood(food){if(food){varsnack='Friskies';returnsnack;}returnsnack;}getFood(false);// undefined
However, observe what happens when we replacevar
usinglet
:
letsnack='Meow Mix';functiongetFood(food){if(food){letsnack='Friskies';returnsnack;}returnsnack;}getFood(false);// 'Meow Mix'
This change in behavior highlights that we need to be careful when refactoringlegacy code which usesvar
. Blindly replacing instances ofvar
withlet
may lead to unexpected behavior.
Note:
let
andconst
are block scoped. Therefore, referencingblock-scoped identifiers before they are defined will produceaReferenceError
.
console.log(x);letx='hi';// ReferenceError: x is not defined
Best Practice: Leave
var
declarations inside of legacy code to denotethat it needs to be carefully refactored. When working on a new codebase, uselet
for variables that will change their value over time, andconst
forvariables which cannot be reassigned.
A common use ofImmediately Invoked Function Expressions is to enclosevalues within its scope. In ES6, we now have the ability to create block-basedscopes and therefore are not limited purely to function-based scope.
(function(){varfood='Meow Mix';}());console.log(food);// Reference Error
Using ES6 Blocks:
{letfood='Meow Mix';}console.log(food);// Reference Error
Often times we have nested functions in which we would like to preserve thecontext ofthis
from its lexical scope. An example is shown below:
functionPerson(name){this.name=name;}Person.prototype.prefixName=function(arr){returnarr.map(function(character){returnthis.name+character;// Cannot read property 'name' of undefined});};
One common solution to this problem is to store the context ofthis
usinga variable:
functionPerson(name){this.name=name;}Person.prototype.prefixName=function(arr){varthat=this;// Store the context of thisreturnarr.map(function(character){returnthat.name+character;});};
We can also pass in the proper context ofthis
:
functionPerson(name){this.name=name;}Person.prototype.prefixName=function(arr){returnarr.map(function(character){returnthis.name+character;},this);};
As well as bind the context:
functionPerson(name){this.name=name;}Person.prototype.prefixName=function(arr){returnarr.map(function(character){returnthis.name+character;}.bind(this));};
UsingArrow Functions, the lexical value ofthis
isn't shadowed and wecan re-write the above as shown:
functionPerson(name){this.name=name;}Person.prototype.prefixName=function(arr){returnarr.map(character=>this.name+character);};
Best Practice: UseArrow Functions whenever you need to preserve thelexical value of
this
.
Arrow Functions are also more concise when used in function expressions whichsimply return a value:
varsquares=arr.map(function(x){returnx*x});// Function Expression
constarr=[1,2,3,4,5];constsquares=arr.map(x=>x*x);// Arrow Function for terser implementation
Best Practice: UseArrow Functions in place of function expressionswhen possible.
With ES6, the standard library has grown immensely. Along with these changesare new methods which can be used on strings, such as.includes()
and.repeat()
.
varstring='food';varsubstring='foo';console.log(string.indexOf(substring)>-1);
Instead of checking for a return value> -1
to denote string containment,we can simply use.includes()
which will return a boolean:
conststring='food';constsubstring='foo';console.log(string.includes(substring));// true
functionrepeat(string,count){varstrings=[];while(strings.length<count){strings.push(string);}returnstrings.join('');}
In ES6, we now have access to a terser implementation:
// String.repeat(numberOfRepetitions)'meow'.repeat(3);// 'meowmeowmeow'
UsingTemplate Literals, we can now construct strings that have specialcharacters in them without needing to escape them explicitly.
vartext="This string contains \"double quotes\" which are escaped.";
lettext=`This string contains "double quotes" which are escaped.`;
Template Literals also support interpolation, which makes the task ofconcatenating strings and values:
varname='Tiger';varage=13;console.log('My cat is named '+name+' and is '+age+' years old.');
Much simpler:
constname='Tiger';constage=13;console.log(`My cat is named${name} and is${age} years old.`);
In ES5, we handled new lines as follows:
vartext=('cat\n'+'dog\n'+'nickelodeon');
Or:
vartext=['cat','dog','nickelodeon'].join('\n');
Template Literals will preserve new lines for us without having toexplicitly place them in:
lettext=(`catdognickelodeon`);
Template Literals can accept expressions, as well:
lettoday=newDate();lettext=`The time and date is${today.toLocaleString()}`;
Destructuring allows us to extract values from arrays and objects (even deeplynested) and store them in variables with a more convenient syntax.
vararr=[1,2,3,4];vara=arr[0];varb=arr[1];varc=arr[2];vard=arr[3];
let[a,b,c,d]=[1,2,3,4];console.log(a);// 1console.log(b);// 2
varluke={occupation:'jedi',father:'anakin'};varoccupation=luke.occupation;// 'jedi'varfather=luke.father;// 'anakin'
letluke={occupation:'jedi',father:'anakin'};let{occupation, father}=luke;console.log(occupation);// 'jedi'console.log(father);// 'anakin'
Prior to ES6, we used libraries such asBrowserifyto create modules on the client-side, andrequireinNode.js. With ES6, we can now directly use modules of all types(AMD and CommonJS).
module.exports=1;module.exports={foo:'bar'};module.exports=['foo','bar'];module.exports=functionbar(){};
With ES6, we have various flavors of exporting. We can performNamed Exports:
exportletname='David';exportletage=25;
As well asexporting a list of objects:
functionsumTwo(a,b){returna+b;}functionsumThree(a,b,c){returna+b+c;}export{sumTwo,sumThree};
We can also export functions, objects and values (etc.) simply by using theexport
keyword:
exportfunctionsumTwo(a,b){returna+b;}exportfunctionsumThree(a,b,c){returna+b+c;}
And lastly, we canexport default bindings:
functionsumTwo(a,b){returna+b;}functionsumThree(a,b,c){returna+b+c;}letapi={ sumTwo, sumThree};exportdefaultapi;
Best Practices: Always use the
export default
method atthe end ofthe module. It makes it clear what is being exported, and saves time by havingto figure out what name a value was exported as. More so, the common practicein CommonJS modules is to export a single value or object. By sticking to thisparadigm, we make our code easily readable and allow ourselves to interpolatebetween CommonJS and ES6 modules.
ES6 provides us with various flavors of importing. We can import an entire file:
import'underscore';
It is important to note that simplyimporting an entire file will executeall code at the top level of that file.
Similar to Python, we have named imports:
import{sumTwo,sumThree}from'math/addition';
We can also rename the named imports:
import{sumTwoasaddTwoNumbers,sumThreeassumThreeNumbers}from'math/addition';
In addition, we canimport all the things (also called namespace import):
import*asutilfrom'math/addition';
Lastly, we can import a list of values from a module:
import*asadditionUtilfrom'math/addition';const{ sumTwo, sumThree}=additionUtil;
When importing the default object we can choose which functions to import:
importReactfrom'react';const{ Component, PropTypes}=React;
This can also be simplified further, using:
importReact,{Component,PropTypes}from'react';
Note: Values that are exported arebindings, not references.Therefore, changing the binding of a variable in one module will affect thevalue within the exported module. Avoid changing the public interface of theseexported values.
In ES5, we had varying ways to handle functions which neededdefault values,indefinite arguments, andnamed parameters. With ES6, we can accomplishall of this and more using more concise syntax.
functionaddTwoNumbers(x,y){x=x||0;y=y||0;returnx+y;}
In ES6, we can simply supply default values for parameters in a function:
functionaddTwoNumbers(x=0,y=0){returnx+y;}
addTwoNumbers(2,4);// 6addTwoNumbers(2);// 2addTwoNumbers();// 0
In ES5, we handled an indefinite number of arguments like so:
functionlogArguments(){for(vari=0;i<arguments.length;i++){console.log(arguments[i]);}}
Using therest operator, we can pass in an indefinite amount of arguments:
functionlogArguments(...args){for(letargofargs){console.log(arg);}}
One of the patterns in ES5 to handle named parameters was to use theoptionsobject pattern, adopted from jQuery.
functioninitializeCanvas(options){varheight=options.height||600;varwidth=options.width||400;varlineStroke=options.lineStroke||'black';}
We can achieve the same functionality using destructuring as a formal parameterto a function:
functioninitializeCanvas({ height=600, width=400, lineStroke='black'}){// Use variables height, width, lineStroke here}
If we want to make the entire value optional, we can do so by destructuring anempty object:
functioninitializeCanvas({ height=600, width=400, lineStroke='black'}={}){// ...}
In ES5, we could find the max of values in an array by using theapply
method onMath.max
like this:
Math.max.apply(null,[-1,100,9001,-32]);// 9001
In ES6, we can now use the spread operator to pass an array of values to be used asparameters to a function:
Math.max(...[-1,100,9001,-32]);// 9001
We can concat array literals easily with this intuitive syntax:
letcities=['San Francisco','Los Angeles'];letplaces=['Miami', ...cities,'Chicago'];// ['Miami', 'San Francisco', 'Los Angeles', 'Chicago']
Prior to ES6, we implemented Classes by creating a constructor function andadding properties by extending the prototype:
functionPerson(name,age,gender){this.name=name;this.age=age;this.gender=gender;}Person.prototype.incrementAge=function(){returnthis.age+=1;};
And created extended classes by the following:
functionPersonal(name,age,gender,occupation,hobby){Person.call(this,name,age,gender);this.occupation=occupation;this.hobby=hobby;}Personal.prototype=Object.create(Person.prototype);Personal.prototype.constructor=Personal;Personal.prototype.incrementAge=function(){Person.prototype.incrementAge.call(this);this.age+=20;console.log(this.age);};
ES6 provides much needed syntactic sugar for doing this under the hood. We cancreate Classes directly:
classPerson{constructor(name,age,gender){this.name=name;this.age=age;this.gender=gender;}incrementAge(){this.age+=1;}}
And extend them using theextends
keyword:
classPersonalextendsPerson{constructor(name,age,gender,occupation,hobby){super(name,age,gender);this.occupation=occupation;this.hobby=hobby;}incrementAge(){super.incrementAge();this.age+=20;console.log(this.age);}}
Best Practice: While the syntax for creating classes in ES6 obscures howimplementation and prototypes work under the hood, it is a good feature forbeginners and allows us to write cleaner code.
Symbols have existed prior to ES6, but now we have a public interface to usingthem directly. Symbols are immutable and unique and can be used as keys in any hash.
CallingSymbol()
orSymbol(description)
will create a unique symbol that cannot be looked upglobally. A Use case forSymbol()
is to patch objects or namespaces from third parties with your ownlogic, but be confident that you won't collide with updates to that library. For example,if you wanted to add a methodrefreshComponent
to theReact.Component
class, and be certain thatyou didn't trample a method they add in a later update:
constrefreshComponent=Symbol();React.Component.prototype[refreshComponent]=()=>{// do something}
Symbol.for(key)
will create a Symbol that is still immutable and unique, but can be looked up globally.Two identical calls toSymbol.for(key)
will return the same Symbol instance. NOTE: This is not true forSymbol(description)
:
Symbol('foo')===Symbol('foo')// falseSymbol.for('foo')===Symbol('foo')// falseSymbol.for('foo')===Symbol.for('foo')// true
A common use case for Symbols, and in particular withSymbol.for(key)
is for interoperability. This can beachieved by having your code look for a Symbol member on object arguments from third parties that contain someknown interface. For example:
functionreader(obj){constspecialRead=Symbol.for('specialRead');if(obj[specialRead]){constreader=obj[specialRead]();// do something with reader}else{thrownewTypeError('object cannot be read');}}
And then in another library:
constspecialRead=Symbol.for('specialRead');classSomeReadableType{[specialRead](){constreader=createSomeReaderFrom(this);returnreader;}}
A notable example of Symbol use for interoperability is
Symbol.iterable
which exists on all iterable and iteratortypes in ES6: Arrays, strings, generators, etc. When called as a method it returns an object with an Iteratorinterface.
Maps is a much needed data structure in JavaScript. Prior to ES6, we createdhash maps through objects:
varmap=newObject();map[key1]='value1';map[key2]='value2';
However, this does not protect us from accidentally overriding functions withspecific property names:
>getOwnProperty({hasOwnProperty:'Hah, overwritten'},'Pwned');>TypeError:Property'hasOwnProperty'isnotafunction
ActualMaps allow us toset
,get
andsearch
for values (and much more).
letmap=newMap();>map.set('name','david');>map.get('name');// david>map.has('name');// true
The most amazing part of Maps is that we are no longer limited to just usingstrings. We can now use any type as a key, and it will not be type-cast toa string.
letmap=newMap([['name','david'],[true,'false'],[1,'one'],[{},'object'],[function(){},'function']]);for(letkeyofmap.keys()){console.log(typeofkey);// > string, boolean, number, object, function}
Note: Using non-primitive values such as functions or objects won't workwhen testing equality using methods such as
map.get()
. As such, stick toprimitive values such as Strings, Booleans and Numbers.
We can also iterate over maps using.entries()
:
for(let[key,value]ofmap.entries()){console.log(key,value);}
In order to store private data versions < ES6, we had various ways of doing this.One such method was using naming conventions:
classPerson{constructor(age){this._age=age;}_incrementAge(){this._age+=1;}}
But naming conventions can cause confusion in a codebase and are not alwaysgoing to be upheld. Instead, we can use WeakMaps to store our values:
let_age=newWeakMap();classPerson{constructor(age){_age.set(this,age);}incrementAge(){letage=_age.get(this)+1;_age.set(this,age);if(age>50){console.log('Midlife crisis');}}}
The cool thing about using WeakMaps to store our private data is that theirkeys do not give away the property names, which can be seen by usingReflect.ownKeys()
:
>constperson=newPerson(50);>person.incrementAge();// 'Midlife crisis'>Reflect.ownKeys(person);// []
A more practical example of using WeakMaps is to store data which is associatedto a DOM element without having to pollute the DOM itself:
letmap=newWeakMap();letel=document.getElementById('someElement');// Store a weak reference to the element with a keymap.set(el,'reference');// Access the value of the elementletvalue=map.get(el);// 'reference'// Remove the referenceel.parentNode.removeChild(el);el=null;value=map.get(el);// undefined
As shown above, once the object is is destroyed by the garbage collector,the WeakMap will automatically remove the key-value pair which was identifiedby that object.
Note: To further illustrate the usefulness of this example, consider howjQuery stores a cache of objects corresponding to DOM elements which havereferences. Using WeakMaps, jQuery can automatically free up any memory thatwas associated with a particular DOM element once it has been removed from thedocument. In general, WeakMaps are very useful for any library that wraps DOMelements.
Promises allow us to turn our horizontal code (callback hell):
func1(function(value1){func2(value1,function(value2){func3(value2,function(value3){func4(value3,function(value4){func5(value4,function(value5){// Do something with value 5});});});});});
Into vertical code:
func1(value1).then(func2).then(func3).then(func4).then(func5,value5=>{// Do something with value 5});
Prior to ES6, we usedbluebird orQ. Now we have Promises natively:
newPromise((resolve,reject)=>reject(newError('Failed to fulfill Promise'))).catch(reason=>console.log(reason));
Where we have two handlers,resolve (a function called when the Promise isfulfilled) andreject (a function called when the Promise isrejected).
Benefits of Promises: Error Handling using a bunch of nested callbackscan get chaotic. Using Promises, we have a clear path to bubbling errors upand handling them appropriately. Moreover, the value of a Promise after it hasbeen resolved/rejected is immutable - it will never change.
Here is a practical example of using Promises:
varfetchJSON=function(url){returnnewPromise((resolve,reject)=>{$.getJSON(url).done((json)=>resolve(json)).fail((xhr,status,err)=>reject(status+err.message));});};
We can alsoparallelize Promises to handle an array of asynchronousoperations by usingPromise.all()
:
varurls=['http://www.api.com/items/1234','http://www.api.com/items/4567'];varurlPromises=urls.map(fetchJSON);Promise.all(urlPromises).then(function(results){results.forEach(function(data){});}).catch(function(err){console.log('Failed: ',err);});
Similar to howPromises allow us to avoidcallback hell, Generators allow us to flatten our code - giving ourasynchronous code a synchronous feel. Generators are essentially functions which we canpause their executionand subsequently return the value of an expression.
A simple example of using generators is shown below:
function*sillyGenerator(){yield1;yield2;yield3;yield4;}vargenerator=sillyGenerator();>console.log(generator.next());// { value: 1, done: false }>console.log(generator.next());// { value: 2, done: false }>console.log(generator.next());// { value: 3, done: false }>console.log(generator.next());// { value: 4, done: false }
Wherenextwill allow us to push our generator forward and evaluate a new expression. While the above example is extremelycontrived, we can utilize Generators to write asynchronous code in a synchronous manner:
// Hiding asynchronousity with Generatorsfunctionrequest(url){getJSON(url,function(response){generator.next(response);});}
And here we write a generator function that will return our data:
function*getData(){varentry1=yieldrequest('http://some_api/item1');vardata1=JSON.parse(entry1);varentry2=yieldrequest('http://some_api/item2');vardata2=JSON.parse(entry2);}
By the power ofyield
, we are guaranteed thatentry1
will have the data needed to be parsed and storedindata1
.
While generators allow us to write asynchronous code in a synchronous manner, there is no clearand easy path for error propagation. As such, as we can augment our generator with Promises:
functionrequest(url){returnnewPromise((resolve,reject)=>{getJSON(url,resolve);});}
And we write a function which will step through our generator usingnext
which in turn will utilize ourrequest
method above to yield a Promise:
functioniterateGenerator(gen){vargenerator=gen();(functioniterate(val){varret=generator.next();if(!ret.done){ret.value.then(iterate);}})();}
By augmenting our Generator with Promises, we have a clear way of propagating errors through the use of ourPromise.catch
andreject
. To use our newly augmented Generator, it is as simple as before:
iterateGenerator(function*getData(){varentry1=yieldrequest('http://some_api/item1');vardata1=JSON.parse(entry1);varentry2=yieldrequest('http://some_api/item2');vardata2=JSON.parse(entry2);});
We were able to reuse our implementation to use our Generator as before, which shows their power. While Generatorsand Promises allow us to write asynchronous code in a synchronous manner while retaining the ability to propagateerrors in a nice way, we can actually begin to utilize a simpler construction that provides the same benefits:async-await.
While this is actually an upcoming ES2016 feature,async await
allows us to perform the same thing we accomplishedusing Generators and Promises with less effort:
varrequest=require('request');functiongetJSON(url){returnnewPromise(function(resolve,reject){request(url,function(error,response,body){resolve(body);});});}asyncfunctionmain(){vardata=awaitgetJSON();console.log(data);// NOT undefined!}main();
Under the hood, it performs similarly to Generators. I highly recommend using them over Generators + Promises. A great resourcefor getting up and running with ES7 and Babel can be foundhere.
About
ES2015 [ES6] cheatsheet containing tips, tricks, best practices and code snippets
Resources
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Releases
Packages0
Languages
- JavaScript100.0%