|

JS Scopes & Closures Simplified

Hi folks! I’m gearing up for a presentation on JavaScript scopes and closures, in the process, gathering my findings and insights here. My goal is to transform what can be a complex topic into something more relatable and easier to understand, especially for those just starting out. Let me know what you think!

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:

  • The globalItem (TV) is like an item in the living room (global scope), accessible from anywhere in the house.
  • The kitchenItem (Refrigerator) is like an item in the kitchen (local scope to the kitchen function), only visible when in the kitchen.
  • The bedroomItem (Bed) is like an item in the bedroom (local scope to the bedroom function), 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 let, const, or 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 kitchen function, kitchenItem is set without a declaration keyword (let, const, var). This causes kitchenItem to be a global variable.
  • Once the kitchen function is called, kitchenItem is 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 kitchenItem before calling the kitchen function, it would result in an error because kitchenItem would not have been initialized yet.

Understanding var, let, and const in scopes

Having understood the basics of JavaScript scope, let’s delve into how different types of variables—var, let, and 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

var is the oldest form of variable declaration in JavaScript. It’s like an attic where you store things; you can access them from anywhere in the house, but it’s a bit old-fashioned and comes with some quirks:

  • Function Scope: Variables declared with var are 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.

Example:

JavaScript var example

2. 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 let are 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.

Example:

JavaScript let example

3. 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 const variable. It’s like setting a painting on the wall; you can’t just replace it with another.
  • Block Scope: Like let, const is also block-scoped.

Example:

JavaScript const example

Scope Chains: The House Layout

Understanding scope chains is like understanding the layout of your house. In JavaScript, scopes are nested within each other. When you look for an item (variable), you start in the current room (local scope). If you don’t find it there, you move to the next outer room (outer scope), and so on, until you reach the global scope (the living room).

Example:

JavaScript scope chains

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:

Example of JavaScript scope creep

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 let or 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:

  • createRoom function represents a local scope, like a room in a house. Inside this room, we have a variable roomItem which is like an item in the room.
  • showItem is a function defined inside createRoom, and it has access to roomItem. It acts like a person carrying a backpack (closure) that contains the roomItem.
  • When createRoom is called, it returns the showItem function. This returned function is like a person stepping out of the room but still carrying the backpack that has the roomItem.
  • When we call roomWithBackpack, which is the returned showItem function, it still has access to roomItem from 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:

Example of a JavaScript memory leak

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

Taking a deeper dive into the nuances of JavaScript, let’s explore the ‘Closure Rule‘ introduced by a colleague of mine at 10Up. This rule offers a straightforward way to understand how closures interact with variables, functions, and scope.

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:

Example of breaking the house rule for JavaScript variables in strict mode

Here, instead of globalItem being left carelessly in the living room (global scope), JavaScript throws an error, effectively telling us we’ve forgotten to declare where this item belongs. This mechanism helps keep our code’s structure organized and free from unintended clutter.

The Takeaway

As we conclude our dive into JavaScript scopes and closures, I hope it’s clarified these essential but often complex concepts. My aim was to break them down into more understandable parts, particularly for those just starting their journey in JavaScript. Your thoughts and feedback on this approach would be greatly appreciated. Let’s keep the conversation going and continue to support each other in mastering these fundamental aspects of JavaScript programming.

Learn more

Share Your Thoughts

Your email address will not be published. Required fields are marked *

Latest Articles