ECMAScript (ES) is a trademarked scripting-language specification standardized by Ecma International in ECMA-262. It was based on JavaScript, which now tracks ECMAScript. What you know as JavaScript in browsers and Node.js is actually a superset of ECMAScript.
You’ve probably heard about ECMAScript 6 (or ES6) already. It’s the next version of JavaScript, and it has some great new features for managing large and complex codebases. The features have varying degrees of complexity and are useful in both simple scripts and complex applications. In this article, we’ll discuss a hand-picked selection of ES6 features that you can use in your everyday JavaScript coding.
Block Bindings
In most C-based languages, variables (or bindings) are created at the spot where the declaration occurs. In JavaScript, however, this is not the case. Where your variables are actually created depends on how you declare them, and ECMAScript 6 offers options to make controlling scope easier
Only one small problem remains: the front end ecosystem barely supports ES6 modules. No browser natively supports all the new features of ES6. Tools called transpilers, like Babel and Traceur, can precompile ES6 modules into valid ES5 code, which today’s browsers can process; but that ES5 has to be wrapped in an asynchronous syntax and then handled.
In my opinion, switching to ES6 is worthwhile for the syntax changes alone. You may be shaking your head, but I think things such as block scoping, arrow functions, object property and method shorthands, default parameters, and template strings can really help to streamline your code. Taking default function parameters as an example, it might only take an extra line of code to do this in ES5 but every extra line you don’t have to type makes your code more concise and more readable. You can focus on logic over boilerplate.
Var Declarations
Variable declarations using var are treated as if they are at the top of the function (or global scope, if declared outside of a function) regardless of where the actual declaration occur.
Consider the following function definition:
function getValue(condition) { if (condition) { var value = "blue"; // other code return value; } else { // value exists here with a value of undefined return null; } // value exists here with a value of undefined }
If you are unfamiliar with JavaScript, then you might expect that variable value only created if condition satisfied. In fact, the variable value is created regardless. Behind the scenes, the JavaScript engine changes the getValue function to look like this:
function getValue(condition) { var value; if (condition) { value = "blue"; // other code return value; } else { return null; } }
That means the variable value is actually still accessible from within the else clause. If accessed from there, the variable would just have a value of undefined because it hasn’t been initialized.
It often takes new JavaScript developers some time to misunderstanding this unique behavior can end up causing bugs. For this reason, ECMAScript 6 introduces block level scoping options to make the controlling a variable’s lifecycle a little more powerful.
Block-Level Declarations
Block-level declarations are those that declare variables that are inaccessible outside of a given block scope. Block scopes, also called lexical scopes, are created:
- Inside of a function
- Inside of a block (indicated by the { and } characters)
Let Declarations
The let declaration syntax is the same as the syntax for var. You can basically replace var with let to declare a variable, but limit the variable’s scope to only the current code block
function getValue(condition) { if (condition) { let value = "blue"; // other code return value; } else { // value doesn't exist here return null; } // value doesn't exist here }
Since the variable value is declared using let instead of var, the variable value is no longer accessible once execution flows out of the if block. If condition evaluates to false, then value is never declared or initialized.
No Redeclaration
If an identifier has already been defined in a scope, then using the identifier in a let declaration inside that scope causes an error to be thrown. For example:
var count = 30; // Syntax error let count = 40;
On the other hand, no error is thrown if a let declaration creates a new variable with the same name as a variable in its containing scope, as demonstrated in the following code:
var count = 30; // Does not throw an error if (condition) { let count = 40; }
This let declaration doesn’t throw an error because it creates a new variable called count within the if statement, instead of creating count in the surrounding block. Inside the if block, this new variable shadows the global count, preventing access to it until execution leaves the block.
Constant Declarations
Variables declared using const are considered constants, meaning their values cannot be changed once set. For this reason, every const variable must be initialized on declaration, as shown in this example:
const MY_CONSTANT = 1; MY_CONSTANT = 2 // Error const SOME_CONST; // Error
Since the variable value is declared using let instead of var, the variable value is no longer accessible once execution flows out of the if block. If condition evaluates to false, then value is never declared or initialized.
No Redeclaration
If an identifier has already been defined in a scope, then using the identifier in a let declaration inside that scope causes an error to be thrown. For example:
var count = 30; // Syntax error let count = 40;
Note that you can still change object properties or array members:
const MY_OBJECT = {some: 1}; MY_OBJECT.some = 'body'; // Cool
Constants, like let declarations, are block-level declarations. That means constants are no longer accessible once execution flows out of the block in which they were declared, as demonstrated in this example:
if (condition) { const MY_CONSTANT = 1; } // MY_CONSTANT isn't accessible here
In another similarity to let, a const declaration throws an error when made with an identifier for an already-defined variable in the same scope. It doesn’t matter if that variable was declared using var (for global or function scope) or let (for block scope). For example, consider this code:
var message = "Hello!"; let age = 25; // Each of these would throw an error. const message = "Goodbye!"; const age = 30;
Declaring Objects with Const
A const declaration prevents modification of the binding and not of the value itself. That means const declarations for objects do not prevent modification of those objects. For example:
const person = { name: "Nicholas" }; // works person.name = "Greg"; // throws an error person = { name: "Greg" };
A variable declared with either let or const cannot be accessed until after the declaration. Attempting to do so results in a reference error, even when using normally safe operations such as the typeof operation in this example:
if (condition) { console.log(typeof value); // ReferenceError! let value = "blue"; }
This is true anytime you attempt to use a variable declared with let or const before it’s been defined. As the previous example demonstrated, this even applies to the normally safe typeof operator. You can, however, use typeof on a variable outside of the block where that variable is declared, though it may not give the results you’re after. Consider this code:
console.log(typeof value); // "undefined" if (condition) { let value = "blue"; }
The typeof operation executes because it occurs outside of the block in which value is declared. That means there is no value binding, and typeof simply returns “undefined”.
Block Binding in Loops
Perhaps one area where developers most want block level scoping of variables is within for loops, where the throwaway counter variable is meant to be used only inside the loop. For instance, it’s not uncommon to see code like this in JavaScript:
for (var i = 0; i < 10; i++) { process(items[i]); } // i is still accessible here console.log(i); // 10
In JavaScript, however, the variable i is still accessible after the loop is completed because the var declaration gets hoisted. Using let instead, as in the following code, should give the intended behavior:
for (let i = 0; i < 10; i++) { process(items[i]); } // i is not accessible here - throws an error console.log(i);
In this example, the variable i only exists within the for loop. Once the loop is complete, the variable is no longer accessible elsewhere.
Functions in Loops
The characteristics of var have long made creating functions inside of loops problematic, because the loop variables are accessible from outside the scope of the loop. Consider the following code:
var funcs = []; for (var i = 0; i < 10; i++) { funcs.push(function() { console.log(i); }); } funcs.forEach(function(func) { func(); // outputs the number "10" ten times });
You might ordinarily expect this code to print the numbers 0 to 9, but it outputs the number 10 ten times in a row. That’s because i is shared across each iteration of the loop, meaning the functions created inside the loop all hold a reference to the same variable. The variable i has a value of 10 once the loop completes, and so when console.log(i) is called, that value prints each time.
A let declaration simplifies loops by effectively mimicking what the IIFE does in the previous example. On each iteration, the loop creates a new variable and initializes it to the value of the variable with the same name from the previous iteration. That means you can omit the IIFE altogether and get the results you expect, like this:
var funcs = []; for (let i = 0; i < 10; i++) { funcs.push(function() { console.log(i); }); } funcs.forEach(function(func) { func(); // outputs 0, then 1, then 2, up to 9 })
The let declaration creates a new variable i each time through the loop, so each function created inside the loop gets its own copy of i. Each copy of i has the value it was assigned at the beginning of the loop iteration in which it was created.
Global Block Bindings
When var is used in the global scope, it creates a new global variable, which is a property on the global object (window in browsers). That means you can accidentally overwrite an existing global using var, such as:
// in a browser var RegExp = "Hello!"; console.log(window.RegExp); // "Hello!" var ncz = "Hi!"; console.log(window.ncz); // "Hi!"
Even though the RegExp global is defined on window, it is not safe from being overwritten by a var declaration.
If you instead use let or const in the global scope, a new binding is created in the global scope but no property is added to the global object. That also means you cannot overwrite a global variable using let or const, you can only shadow it. Here’s an example:
// in a browser let RegExp = "Hello!"; console.log(RegExp); // "Hello!" console.log(window.RegExp === RegExp); // false const ncz = "Hi!"; console.log(ncz); // "Hi!" console.log("ncz" in window); // false
Here, a new let declaration for RegExp creates a binding that shadows the global RegExp. That means window.RegExp and RegExp are not the same, so there is no disruption to the global scope.