An introduction to ES6 Part 2: Block Scoping

By: James Allardice

Tags:

  • es6
  • javascript

This is part 2 of a 10-part series of articles introducting you to some of the features of the upcoming ES6 specification. It may be helpful to read the first part if you missed it.

Let Declarations

In ES5 (the current major revision of the ECMAScript specification, available in all modern environments) variables can only be scoped to the variable environment of an execution context. In practice, this means variables (declared with a var statement) are accessible throughout the execution context in which they appear:

function example(x) {
    console.log(y); // undefined (not a ReferenceError)
    console.log(z); // ReferenceError: z is not defined
    if (x) {
        var y = 5; // This declaration is hoisted
    }
}

This is due to a concept commonly known as “hoisting”. When a function is invoked the declaration binding instantiation algorithm dictates that a binding is created in the variable environment for each variable and function declaration within its code. If you would like to learn more about hoisting in JavaScript I recommend reading Ben Cherry’s article on the subject.

In ES6 variable and function declarations continue to behave in this manner (which is a good thing for backwards compatability). However, the introduction of the let keyword gives us some more flexibility:

function example(x) {
    console.log(y); // ReferenceError: y is not defined
    if (x) {
        let y = 5;
    }
}

Variables declared with a let statement are created as bindings on the lexical environment, rather than the variable environment, of the current execution context. A change to the specification of block statements in ES6 means that each block has its own lexical environment. In the above example, a new lexical environment is created when the block (the body of the if statement) is evaluated. When the let statement is evaluated a binding is added to this lexical environment and is innaccessible from the outer lexical environment (that of the function declaration itself).

We can see how new lexical environments for blocks are created in detail in the ES6 draft specification (ES6 §13.1.2):

Block : { StatementList }

  1. Let oldEnv be the running execution context’s LexicalEnvironment.
  2. Let blockEnv be the result of calling NewDeclarativeEnvironment passing oldEnv as the argument.
  3. Perform Block Declaration Instantiation using StatementList and blockEnv.
  4. Set the running execution context’s LexicalEnvironment to blockEnv.

It’s worth noting that let declarations are not hoisted in the same way as var declarations. Attempting to reference an identifier created as part of a let declaration before the declaration itself has been evaluated will result in an error:

console.log(x); // ReferenceError: x is not defined
let x = 10;

You can use let declarations anywhere you can use a var declaration. Of particular interest is the usage in loop initialisers. When using a var statement as a loop initialiser the declaration will be hoisted as discussed above and the variable will be accessible throughout the execution context in which it appears. This can be a common cause of confusion for beginners, especially those coming from a language that does implement block scoping. With the addition of the let declaration we can ensure our loop counter is only accessible within its block:

for (let i = 0; i < 10; i++) {
    console.log(i); // Prints 0 to 9
}
console.log(i); // ReferenceError: i is not defined

Const Declarations

ES6 introduces another type of declaration, const (ES6 § 13.2.1). It has the same block scoped binding semantics as let but its value is a read-only constant. They must have an initialiser, unlike let and var declarations:

var x; // x === undefined
const z; // SyntaxError: const declarations must have an initializer

const y = 10; // y === 10

y = 20; // SyntaxError: Assignment to constant variable

Note that the const keyword has been supported by numerous engines for some time now, but with slightly different semantics to the ES6 spec. One of the main differences is that constants are currently declared in function scope, rather than block scope. Attempting to assign a new value to a constant fails, but does not produce an error in all engines.

Function Declarations in Blocks

In ES5 function declarations were not allowed to appear in blocks. Various implementations allowed this anyway, leading to code that behaves differently across those implementations (ES5 §12):

Several widely used implementations of ECMAScript are known to support the use of FunctionDeclaration as a Statement. However there are significant and irreconcilable variations among the implementations in the semantics applied to such FunctionDeclarations.

ES6 proposes to explictly allow the use of function declarations within blocks, again following much the same semantics as let and const declarations:

if (x) {
    function fn() {
        // Do stuff
    }
    someObj.method = fn;
}
console.log(fn); // ReferenceError: fn is not defined

However, there does appear to have been some debate around whether or not it will be possible to implement this so I’m unclear as to whether this will make it into the final spec or not. If you’re interested in discussions around these difficulties the Mozilla and WebKit issues are good places to look.

Come back soon for the next part, where we’ll have a look at destructuring, probably one of the most talked-about ES6 features. Follow us on twitter to be notified when it’s posted, and don’t forget we’re hiring


About the Author

James Allardice

James Allardice is a Senior JavaScript Developer at Venntro. He is passionate about clean, maintainable JavaScript and is regularly on the lookout for ways to improve the JavaScript development process. He's currently interested in JSHint, Grunt, AngularJS and ECMAScript6.