Arguably the most horrifying part of the ECMAScript specification concerns the question:
"What happens if I declare a function inside a block in sloppy mode?"
To start with an intentionally overwhelming example, would you believe that the following prints 1
? 😳
var a = -1;
(function () {
const printOuter = () => console.log(a);
{
a = 1;
function a() {}
a = 2;
printOuter();
}
})();
Indeed, it's a bit much to dive right into the deep end, so let's start at the beginning and work our way up to it.
Before ES2015
var
s, function
s, and blocks have all been around since ES1, but blocks were originally only meant for turning a list of statements into a statement itself, and not usable as a scope for declarations. Since blocks were needed for if
branches, while
bodies, and such, it was no extra burden to allow standalone blocks too (whether this benefitted developers or not).
Now, it's always been possible to declare a var
from within a block, but it'd still be hoisted to the top of the immediate function (or script). Hence these two snippets have the same observable behavior:
console.log(a); // undefined
{
var a = 3;
}
console.log(a); // 3
var a;
console.log(a); // undefined
a = 3;
console.log(a); // 3
On the other hand, even as late as ES5, function declarations in blocks were not part of the spec at all!
The Post-var
World
One of ES2015's biggest innovations was introducing lexical let
and const
(and class
!) declarations, which do in fact make use of blocks as scopes.
Block-scoped variables exhibit their own sort of hoisting-like behavior: from the beginning of the block until their point of initialization, they are said to be in the "temporal dead zone" (TDZ), meaning that the following is mercifully an error:
var a;
{
a = 3; // ReferenceError!
let a;
}
ES2015 also introduced function declarations in blocks. And, being a new addition to the spec, it was naturally decided that they too should be scoped to the block!
Note though, that there is no TDZ for function declarations, since it's important to be able to call functions that are declared later on. Effectively, the initialization gets raised to the top as well:
{
// let a = function () {};
a();
function a() {} // nothing happens here
}
Web Reality
But alas, the web is not so simple. JS engines are free to extend the language in various ways (except where forbidden), and function declarations in blocks were quite desirable, even before ES2015 and its fancy block scoping. So engines did implement this feature, and without spec text to keep them aligned, they implemented it in mutually incompatible ways.
And so, while the main body of the spec accurately describes the behavior of this feature in strict mode, a section called Annex B.3.3 was added to describe the "intersection semantics" of these divergent extensions—i.e., the (un)happy path that browser-hosted engines would need to support in order to achieve web compatibility in sloppy mode.
This essentially amounts to treating a function declaration in a block as a let
and a var
at the same time. 🤯
That is, we have the following effects in sloppy mode:
// var a;
...
{
// let a = function () {};
...
function a() {} // (var) a = (let) a; <-- not actually writable in surface code
}
The Deep End
And with that, you're equipped to understand the following grand example (courtesy of fellow TC39er Kevin Gibbons):
var a = -1;
(function () {
// var a;
const printOuter = () => console.log('outer:', a);
{
// let a = function () {};
const printInner = () => console.log('inner:', a);
printOuter(); // outer: undefined
printInner(); // inner: function a(){}
a = 1;
printOuter(); // outer: undefined
printInner(); // inner: 1
function a() {} // (var) a = (let) a;
printOuter(); // outer: 1
printInner(); // inner: 1
a = 2;
printOuter(); // outer: 1
printInner(); // inner: 2
}
})();
console.log('outermost:', a); // outermost: -1
Consider it a cautionary tale to always use strict mode (for which you'll get outer: -1
across the board). 😉
Top comments (3)
Thanks for the easy to understand explanation. I wanted to understand this behavior (because of a example someone shared).
Reading the spec for this annexure (specially without the historical context) would have been a very involving task. Please share more summaries from interesting parts of the spec, like this. This are quite invaluable for understanding the details.
Thanks a lot.
Thanks for your posting,
Interesting.