DEV Community

loading...
Cover image for Illustrating Lexical Scope in JavaScript

Illustrating Lexical Scope in JavaScript

Rajat Verma
Hi, I am a Software Developer, currently building some cool projects. I love to contribute to open source and believe in building with collaboration.
Updated on ・3 min read

Chapter 2: Illustrating Lexical Scope

  • These are the notes of second chapter of the book "You Don't Know JS: Scope and Closures".
  • In this chapter, we will discuss how our program is handled by the JS Engine and how the JS Engine works.

Marbles, and Buckets, and Bubbles... Oh My!

  • Let's say we have marbles of three different colors Red, Blue, and Green. To sort all the marbles we will drop the red marbles into a red bucket, blue into a blue bucket, and green into a green bucket.
  • Now if we need a red marble we know the red bucket is where to get it from.
  • Now apply this analogy to scope and variables, the marbles are the variables and the buckets are the scopes.
  • Let's understand this with the help of an example:
// outer/global scope: RED

var students = [
  { id: 14, name: "Kyle" },
  { id: 73, name: "Suzy" },
  { id: 112, name: "Frank" },
  { id: 6, name: "Sarah" },
];

function getStudentName(studentID) {
  // function scope: BLUE
  for (let student of students) {
    // loop scope: GREEN
    if (student.id == studentID) {
      return student.name;
    }
  }
}

var nextStudent = getStudentName(73);
console.log(nextStudent); // Suzy

Enter fullscreen mode Exit fullscreen mode
  • As you can see that we have designated three scope colors with code comments: RED (outermost global scope), BLUE (scope of function), and GREEN (scope inside the for loop).
  • Now let's see the boundaries of these scope buckets by drawing colored bubbles:

  • Bubble 1 (RED): surround global scope, holds three identifiers: students, getStudentName and nextStudent.
  • Bubble 2 (BLUE): surround scope of function getStudentName(..), holds one identifier: studentID.
  • Bubble 3 (GREEN): surround the scope of the for-loop, holds one identifier: student.

NOTE: Scope bubbles are determined during compilation. Each marble is colored based on which bucket it's declared in, not the color of the scope it may be accessed from.

  • Scopes can nest inside each other, to any depth of nesting as your program needs.
  • References (non-declarations) to variables/identifiers are allowed if there's a matching declaration either in the current scope, or any scope above/outside the current scope, but not with declarations from lower/nested scopes.
  • An expression in the RED(1) bucket only has access to RED(1) marbles, not BLUE(2) or GREEN(3). An expression in the BLUE(2) bucket can reference either BLUE(2) or RED(1) marbles, not GREEN(3). And an expression in the GREEN(3) bucket has access to RED(1), BLUE(2), and GREEN(3) marbles.

Nested Scope

  • Scopes are lexically nested to any arbitrary depth as the program defines.
  • In the above example, the function scope for getStudentName(..) is nested inside the global scope. The block scope of the for loop is similarly nested inside that function scope.
  • Any time an identifier reference cannot be found in the current scope, the next outer scope in the nesting is consulted; that process is repeated until an answer is found or there are no more scopes to consult.

Undefined Mess

  • If the variable is a source, an unresolved identifier lookup is considered an undeclared (unknown, missing) variable, which always results in a ReferenceError being thrown.
  • If the variable is a target, and the code at that moment is running in strict-mode, the variable is considered undeclared and similarly throws a ReferenceError.
  • The error message for an undeclared variable condition, in most JS environments, will look like, "Reference Error: XYZ is not defined."
  • "Not defined" means "not declared" or "undeclared".
  • "Undefined" means that the variable was found, but it has no other value at the moment. So it defaults to the undefined value.
  • To perpetuate the confusion even further, JS's typeof operator returns the string "undefined" for variable references in either state:
var studentName;

typeof studentName; // "undefined"
typeof doesntExist; // "undefined"
Enter fullscreen mode Exit fullscreen mode
  • So, we as developers have to pay close attention to not mix up which kind of "undefined" we're dealing with.

Global... What!?

  • If the variable is a target and the program is not in strict-mode, the engine creates an accidental global variable to fulfill that target assignment. For Example:
function getStudentName() {
  // assignment to an undeclared variable :(
  nextStudent = "Suzy";
}

getStudentName();
console.log(nextStudent);
// "Suzy" -- oops, an accidental-global variable!
Enter fullscreen mode Exit fullscreen mode
  • This is another reason why we should use strict-mode. It prevents us from such incidents by throwing a ReferenceError.

That concludes this chapter. I'll be back with the notes for the next chapter soon.

Till then, Happy Coding :)

If you enjoyed reading the notes or have any suggestions or doubts, then feel free to share your views in the comments.
In case you want to connect with me, follow the links below:

LinkedIn | GitHub | Twitter | Medium

Discussion (6)

Collapse
lukeshiru profile image
LUKE知る

Loved the image explaining the different levels! One thing worth mentioning is that this is a quite simple example that can be resolved without the use of for, but this might be quite useful if you're working with async iterables.
When I say that the example in the post doesn't need for is that you can simply do this instead:

const students = [
    { id: 14, name: "Kyle" },
    { id: 73, name: "Suzy" },
    { id: 112, name: "Frank" },
    { id: 6, name: "Sarah" }
];

/** @param {number} id */
const getStudentName = id =>
    students.find(student => student.id === id)?.name;

console.log(getStudentName(73)); // "Suzy"
Enter fullscreen mode Exit fullscreen mode

Or you can take it one step further and make it more reusable like this:

const students = [
    { id: 14, name: "Kyle" },
    { id: 73, name: "Suzy" },
    { id: 112, name: "Frank" },
    { id: 6, name: "Sarah" }
];

/**
 * Function that takes an array of objects with a `key` property of type
 * `number` and returns a function to search by `id` on that array.
 *
 * @template {readonly { id: number }[]} Input
 * @param {Input} array
 */
const findById =
    array =>
    /**
     * @param {number} id
     * @returns {Input[number] | undefined}
     */
    id =>
        array.find(item => item.id === id);

const findStudentById = findById(students);

console.log(findStudentById(73)?.name); // Suzy
Enter fullscreen mode Exit fullscreen mode

Using the find method of Array, I just made a curried function that can be reused with any array of objects with a number id property.

Cheers!

PS: I used JSDocs in both snippets mainly because I wanted to have better autocompletion and type checking in my editor, but you can obviously skip that if you prefer a more "typeless" approach.

Collapse
rajat2502 profile image
Rajat Verma Author

Hey, thanks for reading the article, yeah we can definately use the find method for that function. The for loop was used just to show how nested scopes work.

Collapse
rajat2502 profile image
Rajat Verma Author
Collapse
rajat2502 profile image
Rajat Verma Author • Edited

These are the notes of Chapter 2 of YDKJS: Scope & Closures.

If you want to read the complete chapter, book, or series, please head over to their repository:
github.com/getify/You-Dont-Know-JS

Collapse
geekquad profile image
Aditya Kumar Gupta

Hey @rajat2502 ,
Although I am just starting with js, I absolutely love reading and learning from your articles. Good work man 🌟

Collapse
rajat2502 profile image
Rajat Verma Author

Thanks a lot @geekquad :)