What is a scope?
Let's start by answering the most important question, what is a scope? In JavaScript, a scope refers to the current execution context in which expressions are defined or can be referenced. I like to think of scope as layers where each layer has its own context that determines the accessibility of variables, functions and classes, let's see an example:
const rootLayer = 0;
{
const layer = 1;
console.log(rootLayer) // 0
console.log(layer) // 1
}
console.log(rootLayer) // 0
console.log(layer) // ❌ Uncaught ReferenceError: layer is not defined
The global scope, the accessible one
When you start writing code at the root of any given file, you are in the global scope, there, if you define a variable, it will be accessible everywhere in your project. In the previous example the rootLayer variable is accessible everywhere including inside the block (represented by braces {}), contrary to the layer variable which is defined inside the block.
The local scope, the hold
If you assign a variable inside a function (or any block for that matter) the declaration will happen in the local scope of that function so it is only accessible inside it.
function scoped() {
const scope = "local";
console.log(scope) // local
}
console.log(scope) // ❌ Uncaught ReferenceError: scope is not defined
The same goes for blocks, if you assign a variable inside a conditional statement for example, it will only be accessible inside the block.
if (true) {
const scope = "local";
console.log(scope) // local
}
console.log(scope) // ❌ Uncaught ReferenceError: scope is not defined
Note that: with var, the scope of the variable is its current execution context which is either:
- In the case of a function, the enclosing function and the functions declared in it
- In the case of a block, the variables declared within it are accessible from the global scope.
Otherwise just don't use **var* and you'll be fine.*
function scoped() {
var scope = "local"
}
console.log(scope) // ❌ Uncaught ReferenceError: scope is not defined
if (true) {
var scope = "local";
}
console.log(scope) // local
The lexical scope, the nerdy?
The lexical scope is the definition area of an expression. Also called static scope, think of it as the place where it was defined, not to be confused with the place where the expression is invoked (or called).
const dog = "Tom";
const parentFunction = () => {
// Layer 1
const cat = "Jerry"; // Lexical scope
const childFunction = () => {
// Layer 2
console.log(cat);
console.log(dog);
}
return childFunction();
}
parentFunction();
// "Jerry"
// "Tom"
As you can see, in the childFunction, we get to call cat and dog without errors but how, since they're both defined outside the block?
Well that's exactly what the lexical scoping does, inside the inner scope you can access variables of outer scopes. (It's called lexical (or static) because the engine determines (at lexing time) the nesting of scopes just by parsing JavaScript source code, without executing it).
The how, the closure
Okay, so now we know about the lexical scope allowing us to access variables from outer scopes, but how does it work exactly?
Let's get back to our previous example:
const dog = "Tom";
const parentFunction = () => {
// Layer 1
const cat = "Jerry";
const childFunction = () => {
// Layer 2
console.log(cat);
console.log(dog);
}
return childFunction();
}
parentFunction();
// "Jerry"
// "Tom"
Inside childFunction() we get to call cat and dog but did you notice that childFunction() was called inside the lexical scope of parentFunction()?
Would it work if we instead did something like this:
const dog = "Tom";
const parentFunction = () => {
// Layer 1
const cat = "Jerry";
const childFunction = () => {
// Layer 2
console.log(cat);
console.log(dog);
}
return childFunction();
}
function run() {
const myParentFunction = parentFunction();
myParentFunction()
}
run();
// "Jerry"
// "Tom"
Now childFunction() is executed outside the lexical scope of parentFunction() but inside the run() . For this to work childFunction has to "remember" cat and dog from its lexical scope, the place where it was defined.
So there it is the final part, closure which is the ability of a function to "remember" and access its lexical scope regardless of where it's executed.
Thanks to closure the compiler does this:
- Check if cat and dog are called in the local scope of childFunction, otherwise look for them up the chain (back to the parent function's scope).
- Look for cat and dog inside the local scope of his parent, here parentFunction, the first one is found, perfect, we can print Jerry, but the other dog can't be found so we have to go back up the chain again.
- Look for dog in the global scope, and here, Tom is finally printed.
This concept is called scope chain, going from one scope to another in order to find what needs to be stored for later use.
Note that: hoisting and closure allows functions, variables and classes to be used safely in the code (limited to their lexical scope) before being declared.
Note that it is generally not recommended to define everything explicitly at the top of the file, as this can lead to unexpected errors and memory as the engine cannot decide whether this variable should be garbage collected or not.
Conclusion
Understanding the concept of scope and hoisting is crucial to writing efficient and effective code aswell as help you write better code and avoid common pitfalls when working with JavaScript.
Top comments (0)