JavaScript Scope
# JavaScript Scope
# What is scope?
From MDN, "The scope is the current context of execution in which values and expressions are "visible" or can be referenced. If a variable or expression is not in the current scope, it will not be available for use. "
Scope is a set of rules that determine if variables are visible or accessible to the current executing program.
# Why should I care about scope?
- Understanding scope is essential for understanding closure which is heavily used in functional programming, hoisting, and
this
keyword. - Scope provides encapsulation that can protect variables from unauthorized changes.
- Locally scoped variables are easier to garbage collect. Leading to efficient resource usage and performance gain.
- Correctly scoped variables are easier to understand, track, and debug.
# Types of scope
| Global Scope, Function Scope, Block Scope |
# Global scope
The outermost scope where variables, functions, and objects are accessible from any part of the code.
Any variable declared outside of a function or block fails in the "global scope." This means the variable can be accessed and modified (except for const
) from anywhere in the code after it has been declared.
Global variables are automatically also properties of the global object (window
in browsers, global
in node.js), so it is possible to reference a global variable not directly by its lexical name, but instead indirectly as a property reference of the global object.
window.a
This technique gives access to a global variable that would otherwise be inaccessible due to it being shadowed. However, non-global shadowed variables cannot be accessed.
# Best Practices
Limit Global Variables: Use them sparingly to avoid collisions and unexpected behaviors.
Name-spacing: If you must use global variables, consider grouping them under a single global object to manage them better.
Use
let
andconst
: These keywords don't create properties on the global object when they're used to declare global variables, providing a slight layer of insulation.Module Pattern: In modern JavaScript, you can use modules to encapsulate code and expose only the necessary parts, thereby avoiding the global scope altogether.
# Function scope
A function creates a scope, so that variables declared exclusively within a function cannot be accessed outside the function.
function exampleFunction() {
// start of function scope
const x = "declared inside function";
console.log("Inside function");
console.log(x); // OK
// end of function scope
}
console.log(x); // ReferenceError: x not defined
Instead of thinking that we are declaring a function and adding some code in it, we could think inversely that we are putting some code inside a function in order to "hide" declarations of variables or functions from the outer scope. -> encapsulation
# Function expression
The previous approach of wrapping code inside a function can help encapsulate declarations within the function. But it comes with drawbacks: 1) we still need to expose this function to the outer scope. 2) we need to explicitly call the function we defined to run the code.
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.
Function expression solves both of these problems.
var a = 1(
// Immediately invoked function expression (IIFE)
function foo() {
doSomething();
var a = 2;
}
)(); // foo is bounded by its own scope
console.log(a); // 1
Anonymous function expression: no name identifier associated with a function. For example:
setTimeout(function () {
doSomethingAfterSec();
}, 1000);
It's easy to type and we could use () => {}
arrow function to further simply it. However, there are several drawbacks to consider:
- Without an identifiable name, we cannot locate it on stack trace.
- Function cannot refer to itself (rip recursion).
- Code may not be readable and understandable sometimes.
setTimeout(function timeoutHandler() {
// better
doSomethingAfterSec();
}, 1000);
# Block scope
The scope created with curly braces pair.
{
// this is within block scope
let a = 1;
const b = 2;
var c = 3;
}
console.log(a, b); // ReferenceError!
console.log(c); // 3
Blocks only scope let
and const
declarations, but not var
declarations.
The let
and const
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 declaration.
We can also be more explicit in hijacking block scope:
var foo = true;
if (foo) {
{
// ^-- explicit block
let bar = foo * 2;
bar = something(bar);
console.log(bar);
}
}
console.log(bar); // ReferenceError!
# Garbage collection
Block scope can be useful for reclaiming memory with garbage collection.
For example:
function processData(data) {
// do something with the data
}
const someBigData = {...};
processData(someBigData);
function handleClick(e) {
doSomethingElse(); // not using someBigData
}
In the above example, if we are sure we don't need the someBigData
after we processing it, we should garbage collect it. However, in some case, the data may continue to live in memory due to closure in subsequent functions.
To guarantee the release of someBigData
, we can create an explicit block scope for it.
function processData(data) {
// do something with the data
}
{
const someBigData = {...};
processData(someBigData);
}
function handleClick(e) {
doSomethingElse(); // not using someBigData
}
Now the resource should be released after the end of the block scope.
# Nested scope
Scopes could be nested, think of them as containers -- one scope can contain multiple scopes, but not as Venn diagrams -- two scopes should either have no overlap or one completely overlaps the other.
Simple rules for nested scope traversal: Engine starts at the currently executing scope, looks for the variable there, then if not found, keeps going up one level, and so on. If the outermost global scope is reached, the search stops, whether it finds the variable or not.