1. Principle of Least Privilege
“hide” variables and functions by enclosing them in the scope of a function.
Why would “hiding” variables and functions be a useful technique?
There’s a variety of reasons motivating this scope-based hiding. They tend to arise from the software design principle Principle of Least Privilege1, 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.
This principle extends to the choice of which scope to contain variables and functions. If all variables and functions were in the global scope, they would of course be accessible to any nested scope. But this would violate the “Least…” principle in that you are (likely) exposing many variables or functions that you should otherwise keep private, as prop‐
er use of the code would discourage access to those variables/func‐
tions.
For example:
function doSomething(a) {
b = a + doSomethingElse( a * 2 );
console.log( b * 3 );
}
function doSomethingElse(a) { return a - 1;
} var b;
doSomething( 2 ); // 15
In this snippet, the b variable and the doSomethingElse(..) function are likely “private” details of how doSomething(..) does its job. Giving the enclosing scope “access” to b and doSomethingElse(..) is not only unnecessary but also possibly “dangerous,” in that they may be used in unexpected ways, intentionally or not, and this may violate pre-condition assumptions of doSomething(..). A more “proper” design would hide these private details inside the scope of doSometh ing(..), such as:
Hiding in Plain Scope | 25
function doSomething(a) {
function doSomethingElse(a) { return a - 1;
Now, b and doSomethingElse(..) are not accessible to any outside influence, instead controlled only by doSomething(..). The func‐
tionality and end result has not been affected, but the design keeps private details private, which is usually considered better software.
Collision Avoidance
Another benefit of “hiding” variables and functions inside a scope is to avoid unintended collision between two different identifiers with the same name but different intended usages. Collision results often in unexpected overwriting of values.
For example:
The i = 3 assignment inside of bar(..) overwrites, unexpectedly, the i that was declared in foo(..) at the for loop. In this case, it will result in an infinite loop, because i is set to a fixed value of 3 and that will forever remain < 10.
The assignment inside bar(..) needs to declare a local variable to use, regardless of what identifier name is chosen. var i = 3; would fix
26 | Chapter 3: Function Versus Block Scope
the problem (and would create the previously mentioned “shadowed variable” declaration for i). An additional, not alternate, option is to pick another identifier name entirely, such as var j = 3;. But your software design may naturally call for the same identifier name, so utilizing scope to “hide” your inner declaration is your best/only op‐
tion in that case.
Global namespaces
A particularly strong example of (likely) variable collision occurs in the global scope. Multiple libraries loaded into your program can quite easily collide with each other if they don’t properly hide their internal/
private functions and variables.
Such libraries typically will create a single variable declaration, often an object, with a sufficiently unique name, in the global scope. This object is then used as a namespace for that library, where all specific exposures of functionality are made as properties off that object (namespace), rather than as top-level lexically scoped identifiers them‐
selves.
doAnotherThing: function() { // ...
} };
Module management
Another option for collision avoidance is the more modern module approach, using any of various dependency managers. Using these tools, no libraries ever add any identifiers to the global scope, but are instead required to have their identifier(s) be explicitly imported into another specific scope through usage of the dependency manager’s various mechanisms.
It should be observed that these tools do not possess “magic” func‐
tionality that is exempt from lexical scoping rules. They simply use the rules of scoping as explained here to enforce that no identifiers are injected into any shared scope, and are instead kept in private,
Hiding in Plain Scope | 27
non-collision-susceptible scopes, which prevents any accidental scope collisions.
As such, you can code defensively and achieve the same results as the dependency managers do without actually needing to use them, if you so choose. See the Chapter 5 for more information about the module pattern.
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.
For example:
var a = 2;
function foo() { // <-- insert this var a = 3;
console.log( a ); // 3
} // <-- and this foo(); // <-- and this
console.log( a ); // 2
While this technique works, it is not necessarily very ideal. There are a few problems it introduces. The first is that we have to declare a named-function foo(), which means that the identifier name foo itself
“pollutes” the enclosing scope (global, in this case). We also have to explicitly call the function by name (foo()) so that the wrapped code actually executes.
It would be more ideal if the function didn’t need a name (or, rather, the name didn’t pollute the enclosing scope), and if the function could automatically be executed.
Fortunately, JavaScript offers a solution to both problems.
var a = 2;
(function foo(){ // <-- insert this var a = 3;
console.log( a ); // 3
28 | Chapter 3: Function Versus Block Scope
})(); // <-- and this
console.log( a ); // 2
Let’s break down what’s happening here.
First, notice that the wrapping function statement starts with (func tion… as opposed to just function…. While this may seem like a minor detail, it’s actually a major change. Instead of treating the function as a standard declaration, the function is treated as a function-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.
The key difference we can observe here between a function declaration and a function expression relates to where its name is bound as an identifier.
Compare the previous two snippets. In the first snippet, the name foo is bound in the enclosing scope, and we call it directly with foo(). In the second snippet, the name foo is not bound in the enclosing scope, but instead is bound only inside of its own function.
In other words, (function foo(){ .. }) as an expression means the identifier foo is found only in the scope where the .. indicates, not in the outer scope. Hiding the name foo inside itself means it does not pollute the enclosing scope unnecessarily.
Anonymous Versus Named
You are probably most familiar with function expressions as callback parameters, such as:
setTimeout( function(){
console.log("I waited 1 second!");
}, 1000 );
This is called an anonymous function expression, because function()
… has no name identifier on it. Function expressions can be anony‐
mous, but function declarations cannot omit the name—that would be illegal JS grammar.
Functions as Scopes | 29
Anonymous function expressions are quick and easy to type, and many libraries and tools tend to encourage this idiomatic style of code.
However, they have several drawbacks to consider:
1. Anonymous functions have no useful name to display in stack traces, which can make debugging more difficult.
2. Without a name, if the function needs to refer to itself, for recur‐
sion, etc., the deprecated arguments.callee reference is unfortu‐
nately required. Another example of needing to self-reference is when an event handler function wants to unbind itself after it fires.
3. 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 draw-backs, 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 );
Invoking Function Expressions Immediately
var a = 2;
(function foo(){
var a = 3;
console.log( a ); // 3 })();
console.log( a ); // 2
Now that we have a function as an expression by virtue of wrapping it in a ( ) pair, we can execute that function by adding another () on the end, like (function foo(){ .. })(). The first enclosing ( ) pair makes the function an expression, and the second () executes the function.
30 | Chapter 3: Function Versus Block Scope
This pattern is so common, a few years ago the community agreed on a term for it: IIFE, which stands for 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 cer‐
tainly less common, naming an IIFE has all the aforementioned ben‐
efits over anonymous function expressions, so it’s a good practice to adopt.
var a = 2;
(function IIFE(){
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
There’s a slight variation on the traditional IIFE form, which some prefer: (function(){ .. }()). Look closely to see the difference. In the first form, the function expression is wrapped in ( ), and then the invoking () pair is on the outside right after it. In the second form, the invoking () pair is moved to the inside of the outer ( ) wrapping pair.
These two forms are identical in functionality. It’s purely a stylistic choice which you prefer.
Another variation on IIFEs that is quite common is to use the fact that they are, in fact, just function calls, and pass in argument(s).
For instance:
var a = 2;
(function IIFE( global ){
var a = 3;
console.log( a ); // 3 console.log( global.a ); // 2
})( window );
console.log( a ); // 2
We pass in the window object reference, but we name the parameter global, so that we have a clear stylistic delineation for global versus
Functions as Scopes | 31
nonglobal references. Of course, you can pass in anything from an enclosing scope you want, and you can name the parameter(s) any‐
thing that suits you. This is mostly just stylistic choice.
Another application of this pattern addresses the (minor niche) con‐
cern that the default undefined identifier might have its value incor‐
rectly overwritten, causing unexpected results. By naming a parameter undefined, but not passing any value for that argument, we can guar‐
antee that the undefined identifier is in fact the undefined value in a block of code:
undefined = true; // setting a land-mine for other code! avoid!
(function IIFE( undefined ){
var a;
if (a === undefined) {
console.log( "Undefined is safe here!" );
}
})();
Still another variation of the IIFE inverts the order of things, where the function to execute is given second, after the invocation and pa‐
rameters to pass to it. This pattern is used in the UMD (Universal Module Definition) project. Some people find it a little cleaner to un‐
derstand, though it is slightly more verbose.
var a = 2;
(function IIFE( def ){
def( window );
})(function def( global ){
var a = 3;
console.log( a ); // 3 console.log( global.a ); // 2
});
The def function expression is defined in the second-half of the snip‐
pet, and then passed as a parameter (also called def) to the IIFE func‐
tion defined in the first half of the snippet. Finally, the parameter def (the function) is invoked, passing window in as the global parameter.
32 | Chapter 3: Function Versus Block Scope
Blocks as Scopes
While functions are the most common unit of scope, and certainly the most widespread of the design approaches in the majority of JS in circulation, other units of scope are possible, and the usage of these other scope units can lead to even better, cleaner to maintain code.
Many languages other than JavaScript support block scope, and so developers from those languages are accustomed to the mindset, whereas those who’ve primarily only worked in JavaScript may find the concept slightly foreign.
But even if you’ve never written a single line of code in block-scoped fashion, you are still probably familiar with this extremely common idiom in JavaScript:
for (var i=0; i<10; i++) { console.log( i );
}
We declare the variable i directly inside the for loop head, most likely because our intent is to use i only within the context of that for loop, and essentially ignore the fact that the variable actually scopes itself to the enclosing scope (function or global).
That’s what block-scoping is all about. Declaring variables as close as possible, as local as possible, to where they will be used. Another ex‐
ample:
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.
Block scope is a tool to extend the earlier Principle of Least Privilege from hiding information in functions to hiding information in blocks of our code.
Blocks as Scopes | 33
Consider the for loop example again:
for (var i=0; i<10; i++) { console.log( i );
}
Why pollute the entire scope of a function with the i variable that is only going to be (or only should be, at least) used for the for loop?
But more important, developers may prefer to check themselves against accidentally (re)using variables outside of their intended pur‐
pose, such being issued an error about an unknown variable if you try to use it in the wrong place. Block-scoping (if it were possible) for the i variable would make i available only for the for loop, causing an error if i is accessed elsewhere in the function. This helps ensure vari‐
ables are not reused in confusing or hard-to-maintain ways.
But, the sad reality is that, on the surface, JavaScript has no facility for block scope.
That is, until you dig a little further.
with
We learned about with in Chapter 2. While it is a frowned-upon con‐
struct, it is an example of (a form of) block scope, in that the scope that is created from the object only exists for the lifetime of that with statement, and not in the enclosing scope.
try/catch
It’s a very little known fact that JavaScript in ES3 specified the variable declaration in the catch clause of a try/catch to be block-scoped to the catch block.
For instance:
try {
undefined(); // illegal operation to force an exception!
}
catch (err) {
console.log( err ); // works!
}
console.log( err ); // ReferenceError: `err` not found
As you can see, err exists only in the catch clause, and throws an error when you try to reference it elsewhere.
34 | Chapter 3: Function Versus Block Scope
While this behavior has been specified and true of practically all standard JS environments (except perhaps old IE), many linters seem to still complain if you have two or more catch clauses in the same scope that each declare their error vari‐
able with the same identifier name. This is not actually a re‐
definition, since the variables are safely block-scoped, but the linters still seem to, annoyingly, complain about this fact.
To avoid these unnecessary warnings, some devs will name their catch variables err1, err2, etc. Other devs will simply turn off the linting check for duplicate variable names.
The block-scoping nature of catch may seem like a useless academic fact, but see Appendix B for more information on just how useful it might be.
let
Thus far, we’ve seen that JavaScript only has some strange niche be‐
haviors that expose block scope functionality. If that were all we had, and it was for many, many years, then block scoping would not be terribly useful to the JavaScript developer.
Fortunately, ES6 changes that, and introduces a new keyword let, which sits alongside var as another way to declare variables.
The let keyword attaches the variable declaration to the scope of whatever block (commonly a { .. } pair) it’s contained in. In other words, let implicitly hijacks any block’s scope for its variable decla‐
ration.
var foo = true;
if (foo) {
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
console.log( bar ); // ReferenceError
Using let to attach a variable to an existing block is somewhat implicit.
It can confuse if you’re not paying close attention to which blocks have variables scoped to them and are in the habit of moving blocks around, wrapping them in other blocks, etc., as you develop and evolve code.
Blocks as Scopes | 35
Creating explicit blocks for block-scoping can address some of these concerns, making it more obvious where variables are attached and not. Usually, explicit code is preferable over implicit or subtle code.
This explicit block-scoping style is easy to achieve and fits more nat‐
urally with how block-scoping works in other languages:
var foo = true;
if (foo) {
{ // <-- explicit block let bar = foo * 2;
bar = something( bar );
console.log( bar );
} }
console.log( bar ); // ReferenceError
We can create an arbitrary block for let to bind to by simply including a { .. } pair anywhere a statement is valid grammar. In this case, we’ve made an explicit block inside the if statement, which may be easier as a whole block to move around later in refactoring, without affecting the position and semantics of the enclosing if statment.
For another way to express explicit block scopes, see Appen‐
dix B.
In Chapter 4, we will address hoisting, which talks about declarations being taken as existing for the entire scope in which they occur.
However, declarations made with let will not hoist to the entire scope of the block they appear in. Such declarations will not observably “ex‐
ist” in the block until the declaration statement.
{
console.log( bar ); // ReferenceError!
let bar = 2;
}
Garbage collection
Another reason block-scoping is useful relates to closures and garbage collection to reclaim memory. We’ll briefly illustrate here, but the clo‐
sure mechanism is explained in detail in Chapter 5.
36 | Chapter 3: Function Versus Block Scope
Consider:
function process(data) { // do something interesting }
var someReallyBigData = { .. };
process( someReallyBigData );
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt){
console.log("button clicked");
}, /*capturingPhase=*/false );
The click function click handler callback doesn’t need the someReal lyBigData variable at all. That means, theoretically, after pro cess(..) runs, the big memory-heavy data structure could be garbage collected. However, it’s quite likely (though implementation depen‐
dent) that the JS engine will still have to keep the structure around, since the click function has a closure over the entire scope.
Block-scoping can address this concern, making it clearer to the en‐
gine that it does not need to keep someReallyBigData around:
function process(data) { // do something interesting }
// anything declared inside this block can go away after!
{
let someReallyBigData = { .. };
process( someReallyBigData );
}
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt){
console.log("button clicked");
}, /*capturingPhase=*/false );
Declaring explicit blocks for variables to locally bind to is a powerful tool that you can add to your code toolbox.
let loops
A particular case where let shines is in the for loop case as we dis‐
cussed previously.
Blocks as Scopes | 37
for (let i=0; i<10; i++) { console.log( i );
}
console.log( i ); // ReferenceError
Not only does let in the for loop header bind the i to the for loop body, but in fact, it rebinds it to each iteration of the loop, making sure to reassign it the value from the end of the previous loop iteration.
Here’s another way of illustrating the per-iteration binding behavior that occurs:
The reason why this per-iteration binding is interesting will become
The reason why this per-iteration binding is interesting will become