Lesson notes

  • Environment is a somewhat isolated area of code.
  • Things created outside of functions, if statements, loops etc. are in the global environment
  • Squiggly brackets { } defined a new local environment.
  • The scope of something are the locations where it is accessible. Things in the global environment have global scope. Things in some local environment have local scope.

Global vs. Local

Local constants and variables are not visible outside their scope:

const multiplier = (num) => {
  const x = 10;
  return num * x;
}

console.log(x);     // ReferenceError: x is not defined

But if x is present globally, it is used and inside x doesn't matter:

const x = 55;

const multiplier = (num) => {
  const x = 10;
  return num * x;
}

console.log(x);     // 55

Local environment has access to the outside environment:

let a = 0;

const changer = () => {
  a++;
}

console.log(a);   // 0
changer();
console.log(a);   // 1

The function actually does something only when it's called, not when it's defined, so at first a is 0, but after changer is called, a becomes 1.

Lexical scope

JavaScript is trying to find the value in the current environment. If the value is not found, JavaScript goes outside, one level at a time, until it finds the value or realizes it cannot be found anywhere.

let a = 7;
let b = 10;

const multiplier = () => {
  let a = 5;
  return a * b;
}

multiplier(); // 50

Here, in the a * b expression multiplier function uses local a (because it's found locally), and outside b (because it wasn't found locally).

Closures

const createPrinter = () => {
  const name = "King";
  
  const printName = () => {
    console.log(name);
  }

  return printName;
}

const myPrinter = createPrinter();
myPrinter();    // King

myPrinter is a function, that was returned by createPrinter. Even though the call to createPrinter is over and name constant doesn't exist anymore, the value is remembered in the myPrinter.

This is a closure: a combination of a function and the environment where it was declared.

Optional reading


Lesson transcript

Part I. The Environment

Let's talk about the environment. The planet is big, but we all share it. If you build a chemical plant, it's a good idea to somehow isolate it from the outside world, so that the inside stuff stays inside. You can say this building has its own environment, local environment. Isolated from the outside global environment.

Your program has a similar structure for similar reasons. Things you create outside of everything, outside of functions, if statements, loops, and other code blocks, is located in the global environment.

const age = 29;

const multiplier = (num) => {
  const x = 10;
  return num * x;
}

let result = true;

Here age constant, multiplier function and result variable are all in the global environment. These things are said to have "global scope". Scope means "the locations where the thing is accessible".

Inside of multiplier function, there is a constant named x. Since it's inside of a code block, it's a local constant, not a global one. So it's only visible inside that function, but not outside. It has local scope.

There is another locally scoped thing in the multiplier function — the num argument. It's not defined as clearly as constants or variables, but it behaves more or less like a local variable.

We can't access x from outside, it's like it doesn't exist there:

const multiplier = (num) => {
  const x = 10;
  return num * x;
}

console.log(x);     // ReferenceError: x is not defined

console.log is called in the global environment, and x is not defined in the global environment. So we get a Reference Error.

We can define x globally:

const x = 55;

const multiplier = (num) => {
  const x = 10;
  return num * x;
}

console.log(x);     // 55

There is a global x now, and its value is being printed, but the local x inside the multiplier function is still only visible inside that function. Those two x's have nothing to do with each other, they are in different environments. They don't clash together even though they have the same name.

Any code block surrounded by squiggly brackets creates a local environment. Here is an example with an if block:

let a = 0;

if (a === 0) {
  const local = 10;
}

console.log(local);

Same goes for while and for loops.

Okay, local is not visible outside. But global is visible everywhere. Even inside something? Yes!

let a = 0;

const changer = () => {
  a++;
}

console.log(a);   // 0
changer();
console.log(a);   // 1

This global variable a is changed inside the changer function. The function actually does something only when it's called, not when it's defined, so at first a is 0, but after changer is called, a becomes 1.

While it's tempting to put everything into one global scope and forget about the complexities of having isolated environments, this is a terrible idea. Global variables make your code very, very fragile. Basically, everything can break everything. So, avoid the global scope, store your stuff where it belongs.

Part II. Lexical scope

Take a look at this program:

let a = 7;
let b = 10;

const multiplier = () => {
  let a = 5;
  return a * b;
}

multiplier(); // 50

The multiplier function returns the product of a and b. a is defined inside, but not b.

When trying to resolve a * b JavaScript looks for the values of a and b. It starts looking locally and goes outside, one scope at a time until it finds the thing or realizes it cannot be found.

So, in this example, JavaScript starts by looking for a inside the local scope — inside the multiplier function. It finds the value right away and proceeds to b. It's unable to find the value of b in the local scope, so it goes to the outside scope. There it can find b — it's 10. So, a * b is resolved to 5 * 10 and then to 50.

This whole piece of code could've been inside of another function inside of yet another function. And if b wasn't found here, JavaScript would've kept going outwards, layer by layer, looking for b.

Notice that a = 7 didn't affect the calculation. a was found inside, so the outside a doesn't matter.

This is called lexical scoping. The scope of any thing is defined by its location within the code. And nested blocks have access to their outer scopes.

Part III. Closures

Most programming languages have a notion of scope or environment, and in most languages this mechanism allows closures to exist. A closure is just a fancy name for a function that remembers the outside things that are used inside.

Before we continue, let's remember how functions are created and used:

const f = () => {
  return 0;
}

f is a pretty useless function, it always returns 0. This whole thing consists of two things: a constant, and a function itself.

It's important to remember that those two things are separate. First, there is a constant named f. Its value could've been a number or a string. But in this case its value is a function.

We used an analogy in the previous lessons: constants are like pieces of paper: name on one side, the value on the other side. This f is, therefore, a piece of paper with an f on one side and the description of the running function on the other side.

When you call this function like this:

f();

... a new box is created, based on the description on that piece of paper.

Okay, back to closures. Consider the following code:

const createPrinter = () => {
  const name = "King";
  
  const printName = () => {
    console.log(name);
  }

  return printName;
}

const myPrinter = createPrinter();
myPrinter();    // King

Function createPrinter creates a constant name and then a function called printName. Both of them are local to the createPrinter function, and only accessible from inside of createPrinter.

printName has no local things on its own, but it has access to the enclosing scope, the outside area where name is defined.

Then the createPrinter function returns the printName function. Remember, function definitions are descriptions of the running functions, just pieces of information like numbers or strings. So we can return a function definition like we can return a number.

In the outside scope, we create a constant myPrinter and set its value to whatever createPrinter returns. It returns a function, so now myPrinter is a function. Call it and "King" is printed.

Here is the weird thing: this name constant was created inside the createPrinter function. That function was called and finished. As we know, when the function finishes working it no longer exists. That magic box disappears with everything inside.

BUT it returned another function, and that another function somehow remembered the name constant. So when we call myPrinter it prints "King", the value it remembered even though the scope where it was defined doesn't exist anymore.

This function that was returned from createPrinter is called a closure. A closure is a combination of a function and the environment where it was declared.

This might seem like a JavaScript party trick, but closures, when used right, can allow for nicer, clearer and more readable code. And the whole idea of returning functions the same way your return numbers or strings gives a lot of power and flexibility.

You'll see these ideas used a lot in programming, and we'll explore their power in the next courses.