Understanding Scope

Jun 15, 2023 - 7 min read

Understanding Scope

Scope

Scope defines where the JS compiler looks for things like variables and functions. It is a set of rules that determines where and how a variable can be accessed in your code.

Lexical scope is a reference to JS on where to look for things, fixed at compile time ahead of execution.

Why? JS processes a scope & puts identifiers to recognise their position and how to execute them afterwards properly. Lexical scope is determined at compile time; once it runs through, it stays the same.

Why do we need scope?

  • Avoid name collisions
  • Security, extra layer against misuse
  • Protect yourself for future refactoring

scope

Think of lexical scope as a building, going from the ground floor all the way to the top (global scope). It is a set of rules that determines where and how a variable can be accessed in your code. For example:

scope-example.js
const teacher = 'Ilya'; // global scope
function otherClass() { // function scope
const teacher = 'Ilya'; function ask(question) { console.log(teacher, question); } ask('Why?'); } otherClass(); // Ilya Why? ask('Why?'); // ReferenceError: ask is not defined

The scope of a variable is the region of your program in which you can access the variable, by its identifier, in which it is declared. In JavaScript, each function creates a new scope. Variables defined inside a function are not accessible (visible) from outside the function. For more reference on the topic of scope, check out this book by Kyle Simpson. He is a fantastic author who helped me understand the topic of scopes once and for all.

Historical bad part of JS: auto-global variables; if you try to assign a variable that's never been formally declared, JS auto-creates it in the global scope. NEVER DO IT!

Variables are either a target position of an assignment(receiving it), or retrieving it (source position)

Hoisting

Hoisting - convenience term which describes the lexical scope behaviour in which variable declaration is being moved up to the top of their module/function-level scope. However, only the declaration is moved, not the assignment of variable value.

console.log(foo); // undefined var foo = 1; console.log(foo); // 1

A function declaration is being hoisted, not the function expression! When you define a function expression to a variable, the variable's declaration is hoisted, but the assignment happens at runtime

// DECLARATION console.log(foo); // Function: foo foo(); // 'Fugazi' function foo { console.log('Fugazi') } // EXPRESSION console.log(foo); // undefined foo(); // TypeError: foo is not a function const foo = () => { console.log('Fugazi') }

'let' and 'const' are hoisted as well, but accessing them before the declaration will give ReferenceError, you need to initialise them first.

Closure

Closure is a function that remembers its lexical scope even when the function is executed outside that lexical scope. It is a function that has access to the parent scope, even after the parent function has closed.

It is a key 🔑 building block for JS, which makes it so functional. ( ͡° ͜ʖ ͡°)_

Closure is when a function can remember and access its lexical scope, the variables outside of itself, even when the outer function executes outside that scope.

Closure - is an inner function within a function that has access to the outer (enclosing) function's variables and parameters- via a scope chain.

Closure has three scope chains:

  • access to its own scope
  • access to enclosing function scope
  • access to global variables

To use a closure, define a function inside another function and expose it. To expose a function, return it or pass it to another function.

function Counter(start) { const count = start; return { increment: () => {count++}, get: () => {return count}; } } const foo = Counter(4); foo.increment(); foo.get(); // 5

Since it is impossible to reference or assign scopes in JS, there is no way of accessing count from the outside.

Closure is not capturing a value; it preserves access to variables.

Rules:

  1. Inner function still has access to the variables in the outer function even after the outer function finished executing
  2. Closures store references to the outer function variables; they do not store the actual value. Just like const a = const b Values & References of primitive and objects
  3. Because closures have access to updated values of outer variables, it can lead to bugs using a for loop.
for (var i = 0; i < 10; i++) { setTimeout(() => { console.log(i); }, 1000); } // will print 10 ten times.

Anonymous arr Fn keeps a reference to i, at the time console.log is called, the for loop has already finished. To fix this, use an anonymous wrapper IIFE or initialise let:

for (var i = 0; i < 10; i++) { ((e) => { setTimeout(() => { console.log(e); }, 1000); })(i); } for (let i = 0; i < 10; i++) { setTimeout(() => { console.log(i); }, 1000); }

Anonymous Fn get called immediately with i and will receive a copy of i as its parameter e.

Assume that closure is a scope-based operation.

Use case:

Used for object data privacy, in event handlers and callback functions

💡 React: useEffect. It can lead to stale closure - variable falling out of sync.

💡 Nodejs: asynchronous non-blocking code functions

Some additional reading on closures.

Modules

Modules encapsulate data and behaviour methods together. The state (data) is held (private) by its methods via closure.

JavaScript modules are a way to include and reuse code in your applications. They can help you organise your code, prevent naming conflicts, and make it more maintainable.

There are two main types of modules in JavaScript:

  1. CommonJS modules are used in Node.js and designed to run on the server side. CommonJS modules use the module.exports and require() keywords to export and import code.

  2. ECMAScript (ES) modules are a modern standard for JavaScript modules designed to run in the browser. ES modules use the export and import keywords to export and import code.

To create a module in JavaScript, you can define your code in a separate file and then use the export keyword to make the code available to other parts of your application. For example, if you have a file called math.js that contains a simple function to add two numbers, you can use the following code to export the function:

export function add(a, b) { return a + b; }

To use the module in another file, you can use the import keyword to import the code. For example:

import { add } from './math'; console.log(add(1, 2)); // 3

Modules can also export variables, objects, and class definitions in addition to functions. You can also use the export default syntax to specify a default export for your module.

Using modules can help you structure your code and make it easier to manage and maintain. It's a good practice to use modules whenever possible to make your code more modular and reusable.

What is the number one lesson you have learned from this article?