What is JS Scope?
Let’s start off with scope. Per MDN docs,
…scope is the current context of execution in which values and expressions are “visible” or can be referenced…MDN Web Docs Glossary: Scope
or more simply put, where to look for things.
In everyday terms…
Scope is like having different levels of privacy in a house.
Your living room or “global scope” is where anything you place there is visible to everyone who enters.
Then, you’ve got rooms or “local scopes” where things you put inside are only visible to those in the room.
So just like in a house, certain variables are accessible everywhere… global, while other variables are only available in specific areas… local.
In the example above:
globalItem(TV) is like an item in the living room (global scope), accessible from anywhere in the house.
kitchenItem(Refrigerator) is like an item in the kitchen (local scope to the
kitchenfunction), only visible when in the kitchen.
bedroomItem(Bed) is like an item in the bedroom (local scope to the
bedroomfunction), only visible when in the bedroom.
What if a variable hasn’t been declared?
When a variable is set inside a function without being declared with
var, it implicitly becomes a global variable. This is often considered a bad practice as it can lead to unexpected results and hard-to-track bugs.
Here’s an example:
In this code:
- Inside the
kitchenItemis set without a declaration keyword (
var). This causes
kitchenItemto be a global variable.
- Once the
kitchenfunction is called,
kitchenItemis accessible anywhere in the script, just like an item that was initially in the kitchen but then moved to the living room.
- If we tried to access
kitchenItembefore calling the
kitchenfunction, it would result in an error because
kitchenItemwould not have been initialized yet.
const in scopes
const—behave in these scopes. It’s like choosing different storage solutions for your house’s items; each has its own rules and peculiarities.
var: The Old Attic Storage
- Function Scope: Variables declared with
varare function-scoped, meaning they are only confined to the function they are declared in, not to any blocks like if-statements or loops.
- Hoisting: These variables are hoisted to the top of their scope. Think of it as putting an item in the attic before you even decide to use it.
let: The Room-Specific Cabinet
Introduced in ES6,
let provides a more controlled way of declaring variables, akin to installing cabinets in specific rooms:
- Block Scope: Variables declared with
letare block-scoped. They exist only within the block (like an if-statement or loop) they are declared in.
- No Hoisting: These are not hoisted to the top of their scope, which means you can’t use them before declaration.
const: The Permanent Fixture
const is similar to
let in terms of block scope, but it’s like installing a permanent fixture in a room:
- Immutable Binding: You can’t reassign a
constvariable. It’s like setting a painting on the wall; you can’t just replace it with another.
- Block Scope: Like
constis also block-scoped.
Scope Chains: The House Layout
In this scenario,
houseItem is like a household item that everyone can access, while
kitchenItem is like a kitchen-specific item, only known within the kitchen.
Navigating scope creep
Scope creep in our code is like unintentionally letting items wander into rooms where they don’t belong, leading to confusion. A typical mistake is when we forget to declare an item (variable) properly, and it ends up in the global living room, causing unexpected messes:
In this snippet,
counter is not declared properly, unintentionally making it a global item. To avoid such scope creep, always declare your items (variables) with
const, ensuring they stay in their designated rooms.
What is a JS Closure?
According to MDN docs a closure
…is the combination of a function bundled together with references to its surrounding state…MDN Web Docs Glossary: Closure
or another definition from Kyle Simpson in his Frontend Masters talk:
Closure is when a function “remembers” the variables outside of it, even if you pass that function elsewhere.Kyle Simpson, Frontend Masters
In everyday terms…
Think of it like a backpack that a function carries around. Every function has its own backpack or… closure where it stores all the variables that were in scope when the function was created.
Even when the function is executed elsewhere, it still remembers and accesses these variables from its backpack. This allows functions to keep and use their original context, like having a personal diary that remembers everything from when it was written, no matter where you take it.
In this example:
createRoomfunction represents a local scope, like a room in a house. Inside this room, we have a variable
roomItemwhich is like an item in the room.
showItemis a function defined inside
createRoom, and it has access to
roomItem. It acts like a person carrying a backpack (closure) that contains the
createRoomis called, it returns the
showItemfunction. This returned function is like a person stepping out of the room but still carrying the backpack that has the
- When we call
roomWithBackpack, which is the returned
showItemfunction, it still has access to
roomItemfrom its original room, even though it’s being called in the global scope.
This demonstrates closure: the function
showItem “remembers” the variables (
roomItem) from its original scope (the room where it was created).
Memory hoarding: Avoid a cluttered backpack
Closures, while incredibly useful, can be akin to carrying a backpack that’s too full. Imagine if a function’s backpack – its closure – keeps holding onto large items (large variables or data) that are no longer needed. This can lead to a sort of ‘memory hoarding,’ where unnecessary items fill up space (memory leaks).
Here’s how it can happen:
In this example, the
largeFurniture stays in the function’s backpack, occupying memory unnecessarily. This underlines the importance of being mindful about what our closures are carrying to prevent ‘memory hoarding‘.
Taking a deeper dive
The Closure Rule
A function must have access to variables in the scope where it was declared and in all its outer scopes.Gabriel Manussakis, Senior Front End Engineer at 10up
Keeping on theme with our house and backpack analogy, a function, much like a person in a house, always has access to items in the room (scope) where it was created, as well as to items from all the outer rooms (outer scopes) it has passed through.
You can think of it as a person moving through a house, picking up various objects from different rooms. Once they step outside (when the function is called elsewhere), they still have all these items in their backpack (closure).
This backpack then contains not just items from the room where they were last (the function’s local scope), but also items from all other rooms they’ve been through (all outer scopes).
Strict Mode: The House Rules for Variables
In our house analogy, think of
'strict mode' as setting strict house rules. In the context of global variables, these rules play a vital role. When
'strict mode' is active, trying to place an item (variable) in the global living room without declaring it properly is like breaking a house rule – it results in an error, not a new item in the global space. For example:
Here, instead of