Hiding In Plain Scope
The traditional way of thinking about functions is that you declare a function, and then add code inside it. But the inverse thinking is equally powerful and useful: take any arbitrary section of code you’ve written, and wrap a function declaration around it, which in effect “hides” the code.
The practical result is to create a scope bubble around the code in question, which means that any declarations (variable or function) in that code will now be tied to the scope of the new wrapping function, rather than the previously enclosing scope. In other words, you can “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 Privilege” [^note-leastprivilege], 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 which you should otherwise keep private, as proper use of the code would discourage access to those variables/functions.
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 doSomething(..)
, such as:
function doSomething(a) {
function doSomethingElse(a) {
return a - 1;
}
var b;
b = a + doSomethingElse( a * 2 );
console.log( b * 3 );
}
doSomething( 2 ); // 15
Now, b
and doSomethingElse(..)
are not accessible to any outside influence, instead controlled only by doSomething(..)
. The functionality 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:
function foo() {
function bar(a) {
i = 3; // changing the `i` in the enclosing scope's for-loop
console.log( a + i );
}
for (var i=0; i<10; i++) {
bar( i * 2 ); // oops, infinite loop ahead!
}
}
foo();
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 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 option 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 of that object (namespace), rather than as top-level lexically scoped identifiers themselves.
For example:
var MyReallyCoolLibrary = {
awesome: "stuff",
doSomething: function() {
// ...
},
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” functionality 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, 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.