Besides var, we now have access to two new identifiers for storing values - let and const. Unlike var, let and const statements are not hoisted to the top of their enclosing scope.
An example of using var:
var snack = 'Meow Mix';
function getFood(food) {
if (food) {
var snack = 'Friskies';
return snack;
}
return snack;
}
getFood(false); // undefined</pre> </div>
However, observe what happens when we replace var using let:
let snack = 'Meow Mix';
function getFood(food) {
if (food) {
let snack = 'Friskies';
return snack;
}
return snack;
}
getFood(false); // 'Meow Mix'</pre> </div>
This change in behavior highlights that we need to be careful when refactoring legacy code which uses var. Blindly replacing instances of var with let may lead to unexpected behavior.
Best Practice: Leave var declarations inside of legacy code to denote that it needs to be carefully refactored. When working on a new codebase, use let for variables that will change their value over time, and const for variables that will be immutable over time.
A common use of Immediately Invoked Function Expressions is to enclose values within its scope. In ES6, we now have the ability to create block-based scopes and therefore are not limited purely to function-based scope.
One common solution to this problem is to store the context of this using a variable:
function Person(name) {
this.name = name;
}
Person.prototype.prefixName = function (arr) {
var that = this; // Store the context of this
return arr.map(function (character) {
return that.name + character;
});
};</pre> </div>
Using Arrow Functions, the lexical value of this isn't shadowed and we can re-write the above as shown:
With ES6, the standard library has grown immensely. Along with these changes are new methods which can be used on strings, such as .includes() and .repeat().
.includes( )
var string = 'food';
var substring = '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:
Prior to ES6, we used libraries such as Browserify to create modules on the client-side, and require in Node.js. With ES6, we can now directly use modules of all types (AMD and CommonJS).
Exporting in CommonJS
module.exports = 1
module.exports = { foo: 'bar' }
module.exports = ['foo', 'bar']
module.exports = function bar () {}
Exporting in ES6
With ES6, we have various flavors of exporting. We can perform Named Exports:
export let name = 'David';
export let age = 25;??
As well as exporting a list of objects:
function sumTwo(a, b) {
return a + b;
}
function sumThree(a, b, c) {
return a + b + c;
}
export { sumTwo, sumThree };</pre> </div>
We can also export a value simply by using the export keyword:
export function sumTwo(a, b) {
return a + b;
}
export function sumThree(a, b, c) {
return a + b + c;
}</pre> </div>
And lastly, we can export default bindings:
function sumTwo(a, b) {
return a + b;
}
function sumThree(a, b, c) {
return a + b + c;
}
let api = {
sumTwo,
sumThree
}
export default api</pre> </div>
Best Practices: Always use the export default method at the end of the module. It makes it clear what is being exported, and saves time by having to figure out what name a value was exported as. More so, the common practice in CommonJS modules is to export a single value or object. By sticking to this paradigm, we make our code easily readable and allow ourselves to interpolate between CommonJS and ES6 modules.
Importing in ES6
ES6 provides us with various flavors of importing. We can import an entire file:
import `underscore`
It is important to note that simply importing an entire file will execute all 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 {
sumTwo as addTwoNumbers,
sumThree as sumThreeNumbers
} from 'math/addition'
In addition, we can import all the things (also called namespace import):
import * as util from 'math/addition'
Lastly, we can use destructuring to import a list of values from a file:
import * as additionUtil from 'math/addtion';
const { sumTwo, sumThree } = additionUtil;
or when we are importing the default object but we want to grab some of the functions on the object:
Note: Values that are exported are bindings, not references. Therefore, changing the binding of a variable in one module will affect the value within the exported module. Avoid changing the public interface of these exported values.
In ES5, we had varying ways to handle functions which needed default values, indefinite arguments, and named parameters. With ES6, we can accomplish all of this and more using more concise syntax.
Default Parameters
function addTwoNumbers(x, y) {
x = x || 0;
y = y || 0;
return x + y;
}
In ES6, we can simply supply default values for parameters in a function:
function addTwoNumbers(x=0, y=0) {
return x + y;
}
Best Practice: While the syntax for creating classes in ES6 obscures how implementation and prototypes work under the hood, it is a good feature for beginners and allows us to write cleaner code.
Symbols have existed prior to ES6, but now we have a public interface to using them directly. One such example is to create unique property keys which will never collide:
Maps is a much needed data structure in JavaScript. Prior to ES6, we created hash maps through objects:
var map = new Object();
map[key1] = 'value1';
map[key2] = 'value2';
However, this does not protect us from accidentally overriding functions with specific property names:
> getOwnProperty({ hasOwnProperty: 'Hah, overwritten'}, 'Pwned');
> TypeError: Property 'hasOwnProperty' is not a function
Actual Maps allow us to set, get and search for values (and much more).
let map = new Map();
> 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 using strings. We can now use any type as a key, and it will not be type-casted to a string.
let map = new Map([
['name', 'david'],
[true, 'false'],
[1, 'one'],
[{}, 'object'],
[function () {}, 'function']
]);
for (let key of map.keys()) {
console.log(typeof key);
// > string, boolean, number, object, function
};</pre> </div>
We can also iterate over maps using .entries( ):
for (let [key, value] of map.entries()) {
console.log(key, value);
}
In order to store private data in < ES5, we had various ways of doing this. One such method was using naming conventions:
class Person {
constructor(age) {
this._age = age;
}
_incrementAge() {
this._age += 1;
}
}</pre> </div>
But naming conventions can cause confusion in a codebase and are not always going to be upheld. Instead, we can use WeakMaps to store our values:
let _age = new WeakMap();
class Person {
constructor(age) {
_age.set(this, age);
}
incrementAge() {
let age = _age.get(this) + 1;
_age.set(this, age);
if(age > 50) {
console.log('Midlife crisis');
}
}
}</pre> </div>
The cool thing about using WeakMaps to store our private data is that their keys do not give away the property names, which can be seen by using Reflect.ownKeys():
> const person = new Person(50);
> person.incrementAge(); // 'Midlife crisis'
> Reflect.ownKeys(person); // []
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 used bluebird or Q. Now we have Promises natively:
new Promise((resolve, reject) =>
reject(new Error('Failed to fulfill Promise')))
.catch(reason => console.log(reason));
Where we have two handlers, resolve (a function called when the Promise is fulfilled) and rejected (a function called when the Promise is rejected).
Benefits of Promises: Error Handling using a bunch of nested callbacks can get chaotic. Using Promises, we have a clear path to bubbling errors up and handling them appropriately. Moreover, the value of a Promise after it has been resolved/rejected is immutable - it will never change.