Chapter 6: Limiting Scope Exposure
Least Exposure
Software engineering articulates a fundamental discipline, typically applied to software security, called "The Principle of Least Privilege" (POLP). Components of the system should be designed to function with least privilege, least access, least exposure.
When variables used by one part of the program are exposed to another part of the program, via scope, there are three main hazards that often arise:
Naming Collisions
Unexpected Behavior: If you expose variables/functions whose usage is otherwise private to a piece of the program, it allows other developers to use them in ways you didn't intend
Unintended Dependency: If you expose variables/functions unnecessarily, it invites other developers to use and depend on those otherwise private pieces.
Hiding in Plain (Function) Scope
It's important to hide our variable and function declarations in the most deeply nested scopes possible.
The cache
variable is a private detail of how factorial(..)
works. We could define another middle scope for cache
to be located:
In the Functional Programming world, caching a function's computed output is referred to as "memoization". This caching relies on closure.
A better solution is to use a function expression:
Since hideTheCache(..)
is defined as a function expression instead of a function declaration, its name is in its own scope—essentially the same scope as cache—rather
than in the outer/global scope. That means we can name every single occurrence of such a function expression the exact same name, and never have any collision.
Invoking Function Expressions Immediately
Notice that we surrounded the entire function expression in a set of ( .. )
, and then on the end, we added that second ()
parentheses set; that's actually calling the function expression we just defined.
In other words, we're defining a function expression that's then immediately invoked. Immediately Invoked Function Expression (IIFE)
Function Boundaries
Beware that using an IIFE to define a scope can have some unintended consequences, depending on the code around it.
Non-arrow function IIFEs will change the binding of a
this
keyword.Statements like
break
andcontinue
won't operate across an IIFE function boundary to control an outer loop or block.
Scoping with Blocks
In general, any { .. }
curly-brace pair which is a statement will act as a block, but not necessarily as a scope.
Not all { .. }
curly-brace pairs create blocks:
Object literals use
{ .. }
curly-brace pairs to delimit their key-value lists, but such object values are not scopes.class
uses{ .. }
curly-braces around its body definition, but this is not a block or scope.A function uses
{ .. }
around its body, but this is not technically a block—it's a single statement for the function body. It is, however, a function scope.The
{ .. }
curly-brace pair on a switch statement does not define a block/scope.
In most languages that support block scoping, an explicit block scope is an extremely common pattern for creating a narrow slice of scope for one or a few variables.
Each variable is defined at the innermost scope possible for the program to operate as desired.
To minimize the risk of TDZ errors with let
/const
declarations, always put those declarations at the top of their scope.
var and let
Any variable that is needed across all (or even most) of a function should be declared by var
so it can be used across the entire function.
Stylistically, var has always, from the earliest days of JS, signaled "variable that belongs to a whole function." var
should be reserved for use in the top-level scope of a function.
What's the Catch?
So far we've asserted that var
and parameters are function-scoped, and let
/const
signal block-scoped declarations.
The
err
variable declared by the catch clause is block-scoped to that block.The
catch
clause block can hold other block-scoped declarations vialet
.A
var
declaration inside this block still attaches to the outer function/global scope.
In ES2019, the catch
clauses do not require the declaration. It's not a scope but a block.
Function Declarations in Blocks (FiB)
The JS specification says that function declarations inside of blocks are block-scoped. (ReferenceError
) However, most browser-based JS engines make that the identifier is scoped outside the if block but the function value is not automatically initialized, so it remains undefined
. (TypeError
)
These engines already had certain behaviors around FiB before ES6 introduced block scoping, and there was concern that changing to adhere to the specification might break some existing website JS code.
Place a function declaration in a block to conditionally define a function one way or another:
The only practical answer to avoiding the vagaries of FiB is to simply avoid FiB entirely. Never place a function declaration directly inside any block. However, it's perfectly fine and valid, for function expressions to appear inside blocks.
FiB is not worth it, and should be avoided.
Last updated