Important takeaways from Kyle Simpson’s YDKJS book series — Scope & Closures
Chapter 1 - What is Scope?
The inclusion of variables into our program begets the most interesting questions we will now address: where do those variables live? In other words, where are they stored? And, most important, how does our program find them when it needs them? These questions speak to the need for a well-defined set of rules for storing variables in some location, and for finding those variables at a later time. We’ll call that set of rules: scope. But, where and how do these scope rules get set?
Compiler Theory
JavaScript falls under the general category of “dynamic” or “interpreted” languages, it is in fact a compiled language. It is not compiled well in advance, as are many traditionally compiled languages, nor are the results of compilation portable among various distributed systems. But, nevertheless, the JavaScript engine performs many of the same steps, albeit in more sophisticated ways than we may commonly be aware of any traditional language compiler.
In a traditional compiled-language process, a chunk of source code, your program, will undergo typically three steps before it is executed, roughly called “compilation”:
- Tokenizing/Lexing: Breaking up a string of characters into meaningful (to the language) chunks, called tokens.
- Parsing: Taking a stream (array) of tokens and turning it into a tree of nested elements, which collectively represent the grammatical structure of the program. This tree is called an “AST” (abstract syntax tree) (astexplorer.net).
- Code-Generation: The process of taking an AST and turning it into executable code. This part varies greatly depending on the language, the platform it’s targeting, and so on.
The JavaScript engine is vastly more complex than just those three steps, as are most other language compilers. For instance, in the process of parsing and code-generation, there are certain steps to optimize the performance of the execution, including collapsing redundant elements, etc.
The JS compiler will take the program var a = 2; and compile it first, and then be ready to execute it, usually right away.
- Engine: Responsible for start-to-finish compilation and execution of our JavaScript program.
- Compiler: One of Engine’s friends; handles all the dirty work of parsing and code-generation (see the previous section).
- Scope: Another friend of Engine; collects and maintains a look-up list of all the declared identifiers (variables), and enforces a strict set of rules as to how these are accessible to currently executing code.
Scope lookups
When Engine executes the code that Compiler produced for step 2, it has to look up the variable a
to see if it has been declared, and this look-up is consulting Scope. But the type of look-up the Engine performs affects the outcome of the look-up. In our case, it is said that the Engine would be performing an LHS lookup for the variable a
. The other type of look-up is called RHS.
function foo(a) {
console.log( a ); // 2
}
foo( 2 );
Let’s imagine the above exchange (which processes this code snippet) as a conversation. The conversation would go a little something like this:
Engine: Hey Scope, I have an RHS reference for
foo
. Ever heard of it?
Scope: Why yes, I have. The compiler declared it just a second ago. It’s a function. Here you go.
Engine: Great, thanks! OK, I’m executingfoo
.
Engine: Hey, Scope, I’ve got an LHS reference fora
, ever heard of it?
Scope: Why yes, I have. Compiler declared it as a formal parameter tofoo
just recently. Here you go.
Engine: Helpful as always, Scope. Thanks again. Now, time to assign2
toa
.
Engine: Hey, Scope, sorry to bother you again. I need an RHS lookup for theconsole
. Ever heard of it?
Scope: No problem, Engine, this is what I do all day. Yes, I’ve got theconsole
. It’s built-in. Here ya go.
Engine: Perfect. Looking up log(..). OK, great, it’s a function.
Engine: Yo, Scope. Can you help me out with an RHS reference toa
. I think I remember it, but just want to double-check.
Scope: You’re right, Engine. The same variable hasn’t changed. Here ya go.
Engine: Cool. Passing the value ofa
, which is 2, into log(..).
In summary: In the execution phase, the Engine performs two types of variable lookups - LHS and RHS. LHS lookups occur when values are being assigned to variables, as in the var a = 2; example. RHS lookups occur when retrieving the values of variables. Eg when you console.log(a) and, the Engine needs to look up the value of a.
Q: Check your understanding so far. Make sure to play the part of Engine and have a “conversation” with Scope:
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );
- Identify all the LHS look-ups (there are 3!).
- Identify all the RHS look-ups (there are 4!).
Note: Just as a block or function is nested inside another block or function, scopes are nested inside other scopes. So, if a variable cannot be found in the immediate scope, the Engine consults the next outer containing scope, continuing until is found or until the outermost (a.k.a., global) scope has been reached.
- Unfulfilled RHS references result in ReferenceErrors being thrown.
- Unfulfilled LHS references result in an automatic, implicitly created global of that name (if not in Strict Mode), or a ReferenceError (if in Strict Mode).
Chapter 2 - What is Lexical Scope?
There are two main scope models used in different programming languages — lexical and dynamic. Lexical scope is what’s used in JavaScript. Lexical scoping (sometimes known as static scoping) is a convention used in many programming languages that sets the scope (range of functionality) of a variable so that it may only be called (referenced) from within the block of code in which it is defined. The scope is determined when the code is compiled.
- Bubble 1 encompasses the global scope and has just one identifier in it:
foo
. - Bubble 2 encompasses the scope of
foo
, which includes the three identifiers:a
,bar
, andb
. - Bubble 3 encompasses the scope of
bar
, and it includes just one identifier:c
.
Scope bubbles are defined by where the blocks of scope are written, which one is nested inside the other, etc.
Scope look-up stops once it finds the first match. The same identifier name can be specified at multiple layers of nested scope, which is called “shadowing” (the inner identifier “shadows” the outer identifier). Regardless of shadowing, scope look-up always starts at the innermost scope being executed at the time, works its way outward/upward until the first match, and stops.
The lexical scope look-up process only applies to first-class identifiers, such as the a, b, and c. If you had a reference to foo.bar.baz
in a piece of code, the lexical scope look-up would apply to finding the foo identifier, but once it locates that variable, object property-access rules take over to resolve the bar and baz properties, respectively.
Ways to cheat Lexical scope?
cheating lexical scope leads to poorer performance
eval
: The eval(..) function in JavaScript takes a string as an argument and treats the contents of the string as if it had actually been authored code at that point in the program. It can modify existing lexical scope (at runtime) by evaluating a string of “code” that has one or more declarations in it.
The string "var b = 3;" is treated, at the point of the eval(..) call, as code that was there all along. Because that code happens to declare a new variablefunction foo(str, a) { eval( str ); // cheating! console.log( a, b ); } var b = 2; foo( "var b = 3;", 1 ); // 1, 3
b
, it modifies the existing lexical scope offoo(..)
. In fact, as mentioned earlier, this code actually creates variableb
inside offoo(..)
that shadows theb
that was declared in the outer (global) scope. When the console.log(..) call occurs, it finds botha
andb
in the scope offoo(..)
, and never finds the outerb
. Thus, we print out “1, 3” instead of “1, 2” as would have normally been the case.
with
: The other frowned-upon (and now deprecated!) feature in JavaScript that cheats lexical scope is the with the keyword. It essentially creates a whole new lexical scope (again, at run-time) by treating an object reference as a scope and that object’s properties as scoped identifiers.
The JavaScript engine has a number of performance optimizations that it performs during the compilation phase. Some of these boil down to being able to essentially statically analyze the code as it lexes, and pre‐determine where all the variable and function declarations are so that it takes less effort to resolve identifiers during execution. But if the engine finds anfunction foo(obj) { with (obj) { a = 2; } } var o1 = { a: 3 }; var o2 = { b: 3 }; foo( o1 ); console.log( o1.a ); // 2 foo( o2 ); console.log( o2.a ); // undefined console.log( a ); // 2—Oops, leaked global!
eval(..)
or within the code, it essentially has to assume that all its awareness of identifier location may be invalid, because it cannot know at the lexing time exactly what code you may pass toeval(..)
to modify the lexical scope or the contents of the object you may pass to with to create a new lexical scope to be consulted. In other words, in the pessimistic sense, most of those optimizations it would make are pointless if eval(..) or with are present, so it simply doesn’t perform the optimizations at all.
Chapter 3 – Function v/s Block Scope?
JavaScript has function-based scope. That is, each function you declare creates a bubble for itself, but no other structures create their own scope bubbles. As we’ll see in just a little bit, this is not quite true.
Let’s explore function scope and its implications.
function foo(a) {
var b = 2; // some code
function bar()
{
// ...
} // more code
var c = 3;
}
- In this snippet, the scope bubble for foo(..) includes identifiers a, b, c, and bar. It doesn’t matter where in the scope a declaration appears, the variable or function belongs to the containing scope bubble, regardless.
- bar(..) has its own scope bubble. So does the global scope, which has just one identifier attached to it: foo.
- Because a, b, c, and bar all belong to the scope bubble of foo(..), they are not accessible outside of foo(..). That is, the following code would all result in ReferenceError errors, as the identifiers are not available to the global scope:
bar(); // fails console.log( a, b, c ); // all 3 fail
var outer = function () {
var hideThingsInsideMe = function () {
// a variable hidden from the global scope by outer's scope bubble
var hiddenThing = "tortise"; // a variable hidden from the scope of outer and beyond by hideThingsInsideMe's scope bubble
var anotherHiddenThing = function () {
// a variable hidden from the scope of outer and beyond by hideThingsInsideMe's scope bubble
consoe.log("turtle");
};
};
console.log(hiddenThing); // ReferenceError: hiddenThing is not defined
console.log(anotherHiddenThing); // ReferenceError: anotherHiddenThing is not defined
console.log(hideThingsInsideMe); // hideThingsInsideMe is defined here
};
console.log(hideThingsInsideMe); // ReferenceError: hideThingsInsideMe is not defined
outer();
Note: We can “hide” variables and functions by enclosing them in the scope of a function.
But why hide?
- They tend to arise from the software design principle Principle of Least Privilege, also sometimes called Least Authority or Least Exposure. This principle states that in the design of software, such as the API for a module/object, you should expose only what is minimally necessary, and “hide” everything else.
- Another benefit of “hiding” variables and functions inside the scope is to avoid unintended collision between two different identifiers with the same name but different intended usages
Another option for collision avoidance is the more modern module approach, using any of the various dependency managers.
Functions as Scopes
We’ve seen that we can take any snippet of code and wrap a function around it, and that effectively “hides” any enclosed variable or function declarations from the outside scope inside that function’s inner scope.
var a = 2;
function foo() { // <-- insert this
var a = 3;
console.log(a); // 3
} // <-- and this
foo(); // <-- and this
console.log(a); // 2
The above snippet arises few problems:
- It declared a named-function foo(), which means that the identifier name foo itself “pollutes” the enclosing scope (global, in this case).
- We have to explicitly call the function by name (foo()) so that the wrapped code actually executes.
Any solution to the above problem? IIFE
var a = 2;
(function foo() { // <-- insert this
var a = 3;
console.log(a); // 3
})(); // <-- and this
console.log(a); // 2
Notice that the wrapping function statement starts with (function… as opposed to just function…. While this may seem like a minor detail, it’s actually a major change. Instead of treating the function Function declaration vs expression
The easiest way to distinguish declaration vs. expression is the position of the word function in the statement (not just a line, but a distinct statement). If function is the very first thing in the statement, then it’s a function declaration. Otherwise, it’s a function expression.
Anonymous function expression:
setTimeout(function () {
console.log("I waited 1 second!");
}, 1000);
It has no name identifier on it. Function expressions can be anonymous, but function declarations cannot omit the name—that would be illegal JS grammar.
Drawbacks of Anonymous function expressions
- Anonymous functions have no useful name to display in stack traces, which can make debugging more difficult.
- Without a name, if the function needs to refer to itself, for recursion, etc., the deprecated arguments. callee reference is unfortunately required. Another example of needing self-reference is when an event handler function wants to unbind itself after it fires.
- Anonymous functions omit a name, which is often helpful in providing more readable/understandable code. A descriptive name helps self-document the code in question.
Inline function expressions are powerful and useful—the question of anonymous versus named doesn’t detract from that. Providing a name for your function expression quite effectively addresses all these drawbacks, but has no tangible downsides. The best practice is to always name your function expressions:
setTimeout(function timeoutHandler() { // <-- Look, I have a name!
console.log("I waited 1 second!");
}, 1000);
IIFE - immediately invoked function expression.
Of course, IIFEs don’t need names, necessarily—the most common form of IIFE is to use an anonymous function expression. While certainly less common, naming an IIFE has all the aforementioned benefits over anonymous function expressions, so it’s a good practice to adopt. There are 2 ways to write:
(function() {
console.log('hi')
})();
----------------
(function() {
console.log('hey')
}());
// It’s purely a stylistic choice which you prefer.
Block Scope
While functions are the most common unit of scope, there are other ways to define scope. Look at the below snippet:
var foo = true;
if (foo) {
var bar = foo * 2;
bar = something(bar);
console.log(bar);
}
We are using a bar
variable only in the context of the if statement, so it makes a kind of sense that we would declare it inside the if
block. However, where we declare variables is not relevant when using var
, because they will always belong to the enclosing scope. This snippet is essentially fake block-scoping, for stylistic reasons, and relying on self-enforcement not to accidentally use bar
in another place in that scope.
There is a way to create a Block scope which won’t pollute the global scope: let
& const
. catch also creates block scope in try catch statement.
// var
var happyDays = function (day) {
if (day === 'sunny') {
var a = 'apples'; //a is function-scoped
}
console.log(a); // apples
}
happyDays('sunny')
// let
var happyDays = function (day) {
if (day === 'sunny') {
let b = 'bananas'; //b is block-scoped
}
console.log(b); //ReferenceError: b is not defined
}
happyDays('sunny')
// const
var happyDays = function (day) {
if (day === 'sunny') {
const c = 'cherries'; //c is block-scoped
}
console.log(c); //ReferenceError: c is not defined
}
happyDays('sunny')
// catch
try {
var d = 'doggies'; //d is function-scoped
throw 'my exception';
}
catch (err) {
console.log('error:', err); //err is block-scoped
}
console.log(d);
console.log(err);
// error: my exception
// doggies
// Uncaught ReferenceError: err is not defined
However, declarations made with let will not hoist to the entire scope of the block they appear in. Such declarations will not observably “exist” in the block until the declaration statement.
{
console.log(bar); // Reference Error!
let bar = 2;
} // more on it in next chapter
Another reason block-scoping is useful relates to closures and garbage collection to reclaim memory.
Chapter 4 – Hoisting?
There’s a temptation to think that all of the code you see in a JavaScript program is interpreted line-by-line, top-down in order, as the program executes. While that is substantially true, there’s one part of that assumption that can lead to incorrect thinking about your program.
Look at the below code,
a = 2;
var a;
console.log( a );
What do you expect to be printed in the console.log(..) statement? Many developers would expect undefined
since the var a statement comes after the a = 2
, and it would seem natural to assume that the variable is redefined and thus assigned the default undefined. However, the output will be 2.
Consider another piece of code:
console.log( a );
var a = 2;
You might be tempted to assume that, since the previous snippet exhibited some less-than-top-down looking behavior, perhaps in this snippet, 2 will also be printed. Others may think that since the a
variable is used before it is declared, this must result in a ReferenceError
being thrown. Unfortunately, both guesses are incorrect. undefined
is the output
So, what’s going on here? It would appear we have a chicken-and-theegg question. Which comes first, the declaration (“egg”), or the assignment (“chicken”)?
Recall that the engine actually will compile your JavaScript code before it interprets it. Part of the compilation phase was to find and associate all declarations with their appropriate scopes.
So, the best way to think about things is that all declarations, both variables, and functions, are processed first before any part of your code is executed.
Note: Only the declarations themselves are hoisted, while any assignments or other executable logic are left in place. If hoisting were to re-arrange the executable logic of our code, that could wreak havoc.
Both function declarations and variable declarations are hoisted. But a subtle detail (that can show up in code with multiple “duplicate” declarations) is that functions are hoisted first, and then variables. Consider the below snippet:
console.log(x) // function x () {...}
x(); // gday world
var x = 'hello world'
function x () {
console.log('gday world');
}
console.log(x) // hello world
x(); // TypeError: x is not a function
/*
The compiler will hoist the function declaration first
It will then try to hoist the variable declartion, but ignore it, as x has already been declared
It will then begin executing our code, at which point the x that gets logged to the console is our function
It will execute our function, causing 'gday world' to log to the console
It will assign hello world to the value of x
It will skip over our function declaration, which was already hoisted and dealt with, there's nothing to assign or execute here
It will then console log x and the value will be hello world
It will try to execute x, but get a TypeError
*/
Chapter 5 – Scope Closure?
Closure is all around you in JavaScript, you just have to recognize and embrace it.
Closures happen as a result of writing code that relies on the lexical scope. They just happen. You do not even really have to intentionally create closures to take advantage of them. Closures are created and used for you all over your code. What you are missing is the proper mental context to recognize, embrace, and leverage closures for your own will. The enlightenment moment should be: oh, closures are already occurring all over my code, I can finally see them now. Understanding closures is like when Neo sees the Matrix for the first time.
Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope.
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2 -- Whoa, closure was just observed, man.
The function bar()
has lexical scope access to the inner scope of foo()
. But then, we take bar()
, the function itself, and pass it as a value. In this case, we return the function object itself that bar
references. After we execute foo()
, we assign the value it returned (our inner bar() function) to a variable called baz
, and then we actually invoke baz()
, which of course is invoking our inner function bar()
, just by a different identifier reference. bar()
is executed, for sure. But in this case, it’s executed outside of its declared lexical scope. After foo()
is executed, normally we would expect that the entirety of the inner scope of foo()
would go away, because we know that the engine employs a garbage collector that comes along and frees up memory once it’s no longer in use. Since it would appear that the contents of foo()
are no longer in use, it would seem natural that they should be considered gone. But the “magic” of closures does not let this happen. That inner scope is in fact still in use and thus does not go away. Who’s using it? The function bar()
itself. By virtue of where it was declared, bar()
has a lexical scope closure over that inner scope of foo()
, which keeps that scope alive for bar()
to reference at any later time. bar()
still has a reference to that scope, and that reference is called closure
. So, a few microseconds later, when the variable baz
is invoked (invoking the inner function we initially labeled bar), it duly has access to author-time lexical scope, so it can access the variable a
just as we’d expect. The function is being invoked well outside of its author-time lexical scope. Closure lets the function continue to access the lexical scope it was defined in at author time.
Whatever facility we use to transport an inner function outside of its lexical scope, it will maintain a scope reference to where it was originally declared, and wherever we execute him, that closure will be exercised.
Eg:
var fn;
function foo() {
var a = 2;
function baz() {
console.log(a);
}
fn = baz; // assign baz to global variable
}
function bar() {
fn(); // look ma, I saw closure!
}
foo();
bar(); // 2
Real working example we see every day:
function wait(message) {
setTimeout( function timer(){
console.log( message );
}, 1000 );
}
wait( "Hello, closure!" );
We take an inner function (named timer) and pass it to setTime out(..). But timer has a scope closure over the scope of wait(..), indeed keeping and using a reference to the variable message.
Essentially whenever and wherever you treat functions (that access their own respective lexical scopes) as first-class values and pass them around, you are likely to see those functions exercising closure. Be the timers, event handlers, Ajax requests, cross window messaging, web workers, or any of the other asynchronous (or synchronous!) tasks, when you pass in a callback function, get ready to sling some closure around!
Lets see some more example:
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
// Ouput 6, five times..
Huh?
First, let’s explain where 6 comes from. The terminating condition of the loop is when i is not <=5. The first time that’s the case is when i is 6. So, the output is reflecting the final value of the i
after the loop terminates. This actually seems obvious at second glance. The timeout function callbacks are all running well after the completion of the loop. In fact, as timers go, even if it was setTimeout(.., 0) on each iteration, all those function callbacks would still run strictly after the completion of the loop, and thus print 6 each time.
What’s missing is that we are trying to imply that each iteration of the loop “captures” its own copy of i
, at the time of the iteration. But, the way scope works, all five of those functions, though they are defined separately in each loop iteration, are closed over the same shared global scope, which has, in fact, only one i
in it.
How to fix this?
We learned in Chapter 3 that the IIFE creates scope by declaring a function and immediately executing it.
for (var i = 1; i <= 5; i++) {
(function () {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
})();
}
Does that work? No… But why?
We now obviously have more lexical scope. Each timeout function callback is indeed closing over its own per-iteration scope created respectively by each IIFE. It’s not enough to have a scope to close over if that scope is empty. Look closely. Our IIFE is just an empty do-nothing scope. It needs something in it to be useful to us. It needs its own variable, with a copy of the i
value at each iteration.
for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
// Eureka, It works!
We used IIFE to create new scope per iteration. So, we actually needed a per-iteration block scope and we have heard something about let
.
for (var i = 1; i <= 5; i++) {
let j = i; // yay, block-scope for closure!
setTimeout(function timer() {
console.log(j);
}, j * 1000);
}
// OR,
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
Modules
There are other code patterns that leverage the power of closure but that do not on the surface appear to be about callbacks. Let’s examine the most powerful of them: the module.
function foo() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join(" ! "));
}
}
As this code stands right now, there’s no observable closure going on. We simply have some private data variables something and another, and a couple of inner functions doSomething() and doAnother(), which both have a lexical scope (and thus closure!) over the inner scope of foo().
But now consider:
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join(" ! "));
}
return {
doSomething: doSomething,
doAnother: doAnother
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
This is the pattern in JavaScript we call module. The most common way of implementing the module pattern is often called revealing module, and it’s the variation we present here. Let’s examine some things about this code.
- First, CoolModule() is just a function, but it has to be invoked for there to be a module instance created. Without the execution of the outer function, the creation of the inner scope and the closures would not occur.
- Second, the CoolModule() function returns an object, denoted by the object-literal syntax { key: value, … }. The object we return has references on it to our inner functions, but not to our inner data variables. We keep those hidden and private. It’s appropriate to think of this object return value as essentially a public API for our module. This object return value is ultimately assigned to the outer variable foo, and then we can access those property methods on the API, like foo.doSomething().
- The doSomething() and doAnother() functions have closure over the inner scope of the module instance (arrived at by actually invoking CoolModule()). When we transport those functions outside of the lexical scope, by way of property references on the object we return, we have now set up a condition by which closure can be observed and exercised.
Requirements for the module pattern to be exercised:
- There must be an outer enclosing function, and it must be invoked at least once (each time creates a new module instance).
- The enclosing function must return back at least one inner function, so that this inner function has closure over the private scope, and can access and/or modify that private state.
Converting a Module into IIFE, so that there is no need to create a new module instance every time:
var foo = (function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join(" ! "));
}
return {
doSomething: doSomething,
doAnother: doAnother
};
})();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
- Modules are just functions, so they can receive parameters:
ES6 adds first-class syntax support for the concept of modules. When loaded via the module system, ES6 treats a file as a separate module. Each module can both import other modules or specific API members, as well as export their own public API members. ES6 modules do not have an “inline” format, they must be defined in separate files (one per module). The browsers/engines have a default “module loader” (which is overridable, but that’s well beyond our discussion here), which synchronously loads a module file when it’s imported.
Consider:
// bar.js
function hello(who) {
return "Let me introduce: " + who;
}
export hello;
// foo.js
// import only `hello()` from the "bar" module
import hello from "bar";
var hungry = "hippo";
function awesome() {
console.log(
hello(hungry).toUpperCase()
);
}
export awesome;
// baz.js
// import the entire "foo" and "bar" modules
module foo from "foo";
module bar from "bar";
console.log(
bar.hello("rhino")
); // Let me introduce: rhino
foo.awesome(); // LET ME INTRODUCE: HIPPO
Appendix A – Dynamic Scope
Lexical scope is the set of rules about how the engine can look up a variable and where it will find it. The key characteristic of lexical scope is that it is defined at author's time when the code is written (assuming you don’t cheat with eval() or with). Dynamic scope seems to imply, and for good reason, that there’s a model whereby scope can be determined dynamically at runtime, rather than statically at author time.
function foo() {
console.log(a); // 2
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();
Lexical scope holds that the RHS reference to a
in foo() will be resolved to the global variable a, which will result in value 2 being output.
Dynamic scope, by contrast, doesn’t concern itself with how and where functions and scopes are declared, but rather where they are called from. In other words, the scope chain is based on the call stack, not the nesting of scopes in code.
So, if JavaScript had dynamic scope, when foo() is executed, theoretically the code below would instead result in 3 as the output.
function foo() {
console.log(a); // 3 (not 2!)
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();
How can this be? Because when foo()
cannot resolve the variable reference for a
, instead of stepping up the nested (lexical) scope chain, it walks up the call stack, to find where foo()
was called from. Since foo()
was called from bar()
, it checks the variables in scope for bar()
, and finds an a there with value 3. Strange? You’re probably thinking so, at the moment. But that’s just because you’ve probably only ever worked on (or at least deeply considered) code that is lexically scoped. So dynamic scoping seems foreign. If you had only ever written code in a dynamically scoped language, it would seem natural, and lexical scope would be the oddball. To be clear, JavaScript does not, in fact, have dynamic scope. It has lexical scope. Plain and simple. But this mechanism is kind of like dynamic scope.
Appendix B – Polyfilling Block Scope
What if we wanted to use block scope in pre-ES6 environments?
{
let a = 2;
console.log(a); // 2
}
console.log( a ); // ReferenceError
Basically alternative for pre-es6?
try { throw 2 } catch (a) {
console.log(a); // 2
}
console.log(a); // ReferenceError
// Ugly code
Note: Google maintains a project called Traceur, which is exactly tasked with transpiling ES6 features into pre-ES6 (mostly ES5, but not all!) for general usage.
That’s all folks for today, Very soon I will be writing another blog for the next book in the series.
Want an even shorter version of this? Check out below:
Open to feedback and comments if I missed any concept which was covered in this book.
🙏 Credit: