Scope - Learning JavaScript (2016)

Learning JavaScript (2016)

Chapter 7. Scope

Scope determines when and where variables, constants, and arguments are considered to be defined. We’ve already had some exposure to scope: we know that the arguments of a function exist only in the body of the function. Consider the following:

function f(x) {

return x + 3;

}

f(5); // 8

x; // ReferenceError: x is not defined

We know that x lived very briefly (otherwise, how would it have successfully calculated x + 3?), but we can see that outside of the function body, it’s as if x doesn’t exist. Thus, we say the scope of x is the function f.

When we say that the scope of a variable is a given function, we must remember that the formal arguments in the function body don’t exist until the function is called (thereby becoming actual arguments). A function may be called multiple times: each time the function is called, its arguments come into existence, and then go out of scope when the function returns.

We have also taken it for granted that variables and constants do not exist before we create them. That is, they aren’t in scope until we declare them with let or const (var is a special case we’ll cover later in this chapter).

NOTE

In some languages, there’s an explicit distinction between declaration and definition. Typically, declaring a variable means that you are announcing its existence by giving it an identifier. Definition, on the other hand, usually means declaring it and giving it a value. In JavaScript, the two terms are interchangeable, as all variables are given a value when they’re declared (if not explicitly, they implicitly get the value undefined).

Scope Versus Existence

It’s intuitively obvious that if a variable doesn’t exist, it’s not in scope. That is, variables that have not yet been declared, or variables that have ceased to exist because a function exits, are clearly not in scope.

What about the converse? If a variable is not in scope, does that mean it doesn’t exist? Not necessarily, and this is where we must make a distinction between scope and existence.

Scope (or visibility) refers to the identifiers that are currently visible and accessible by the currently executing part of the program (called the execution context). Existence, on the other hand, refers to identifiers that hold something for which memory has been allocated (reserved). We’ll soon see examples of variables that exist but are not in scope.

When something ceases to exist, JavaScript doesn’t necessarily reclaim the memory right away: it simply notes that the item no longer needs to be kept around, and the memory is periodically reclaimed in a process called garbage collection. Garbage collection in JavaScript is automatic, and outside of certain highly demanding applications, isn’t something you’ll need to concern yourself with.

Lexical Versus Dynamic Scoping

When you look at the source code for a program, you are looking at its lexical structure. When the program actually runs, execution can jump around. Consider this program with two functions:

function f1() {

console.log('one');

}

function f2() {

console.log('two');

}

f2();

f1();

f2();

Lexically, this program is simply a series of statements that we generally read from top to bottom. However, when we run this program, execution jumps around: first to the body of function f2, then to the body of function f1 (even though it’s defined before f2), then back to the body of function f2.

Scoping in JavaScript is lexical, meaning we can determine what variables are in scope simply by looking at the source code. That’s not to say that scope is aways immediately obvious from the source code: we’ll see some examples in this chapter that require close examination to determine scope.

Lexical scoping means whatever variables are in scope where you define a function from (as opposed to when you call it) are in scope in the function. Consider this example:

const x = 3;

function f() {

console.log(x); // this will work

console.log(y); // this will cause a crash

}

const y = 3;

f();

The variable x exists when we define the function f, but y doesn’t. Then we declare y and call f, and see that x is in scope inside the body of f when it’s called, but y isn’t. This is an example of lexical scoping: the function f has access to the identifiers that were available when it wasdefined, not when it was called.

Lexical scoping in JavaScript applies to global scope, block scope, and function scope.

Global Scope

Scope is hierarchical, and there has to be something at the base of the tree: the scope that you’re implicitly in when you start a program. This is called global scope. When a JavaScript program starts—before any functions are called—it is executing in global scope. The implication is, then, that anything you declare in global scope will be available to all scopes in your program.

Anything declared in global scope is called a global, and globals have a bad reputation. It’s hard to crack open a book about programming without being warned that using globals will cause the very earth to split open and swallow you whole. So why are globals so bad?

Globals are not bad—as a matter of fact, they are a necessity. What’s bad is the abuse of the global scope. We already mentioned that anything available in global scope is therefore available in all scopes. The lesson there is to use globals judiciously.

The clever reader might think “well, I will create a single function in the global scope, and therefore reduce my globals to one!” Clever, except you’ve now just moved the problem one level down. Anything declared in the scope of that function will be available to anything called from that function…which is hardly better than global scope!

The bottom line is this: you will probably have things that live in global scope, and that’s not necessarily bad. What you should try to avoid is things that rely on global scope. Let’s consider a simple example: keeping track of information about a user. Your program keeps track of a user’s name and age, and there are some functions that operate on that information. One way to do this is with global variables:

let name = "Irena"; // global

let age = 25; // global

function greet() {

console.log(`Hello, ${name}!`);

}

function getBirthYear() {

return new Date().getFullYear() - age;

}

The problem with this approach is that our functions are highly dependent on the context (or scope) that they’re called from. Any function—anywhere in your entire program—could change the value of name (accidentally or intentionally). And “name” and “age” are generic names that might reasonably be used elsewhere, for other reasons. Because greet and getBirthYear rely on global variables, they are basically relying on the rest of the program using name and age correctly.

A better approach would be to put user information in a single object:

let user = {

name = "Irena",

age = 25,

};

function greet() {

console.log(`Hello, ${user.name}!`);

}

function getBirthYear() {

return new Date().getFullYear() - user.age;

}

In this simple example, we’ve only reduced the number of identifiers in the global scope by one (we got rid of name and age, and added user), but imagine if we had 10 pieces of information about the user…or 100.

We could do better still, though: our functions greet and getBirthYear are still dependent on the global user, which can be modified by anything. Let’s improve those functions so they’re not dependent on global scope:

function greet(user) {

console.log(`Hello, ${user.name}!`);

}

function getBirthYear(user) {

return new Date().getFullYear() - user.age;

}

Now our functions can be called from any scope, and explicitly passed a user (when we learn about modules and object-oriented programming, we’ll see better ways still of handling this).

If all programs were this simple, it would hardly matter if we used global variables or not. When your program is a thousand lines long—or a hundred thousand—and you can’t keep all scopes in your mind at once (or even on your screen), it becomes critically important not to rely on global scope.

Block Scope

let and const declare identifiers in what’s known as block scope. You’ll recall from Chapter 5 that a block is a list of statements surrounded by curly braces. Block scope, then, refers to identifiers that are only in scope within the block:

console.log('before block');

{

console.log('inside block');

const x = 3;

console.log(x): // logs 3

}

console.log(`outside block; x=${x}`); // ReferenceError: x is not defined

Here we have a standalone block: usually a block is part of a control flow statement such as if or for, but it’s valid syntax to have a block on its own. Inside the block, x is defined, and as soon as we leave the block, x goes out of scope, and is considered undefined.

NOTE

You might remember from Chapter 4 that there’s little practical use for standalone blocks; they can be used to control scope (as we’ll see in this chapter), but that’s rarely necessary. It is very convenient for explaining how scope works, which is why we’re using them in this chapter.

Variable Masking

A common source of confusion is variables or constants with the same name in different scopes. It’s relatively straightforward when scopes come one after another:

{

// block 1

const x = 'blue';

console.log(x); // logs "blue"

}

console.log(typeof x); // logs "undefined"; x out of scope

{

// block 2

const x = 3;

console.log(x); // logs "3"

}

console.log(typeof x); // logs "undefined"; x out of scope

It’s easy enough to understand here that there are two distinct variables, both named x in different scopes. Now consider what happens when the scopes are nested:

{

// outer block

let x = 'blue';

console.log(x); // logs "blue"

{

// inner block

let x = 3;

console.log(x); // logs "3"

}

console.log(x); // logs "blue"

}

console.log(typeof x); // logs "undefined"; x out of scope

This example demonstrates variable masking. In the inner block, x is a distinct variable from the outer block (with the same name), which in effect masks (or hides) the x that’s defined in the outer scope.

What’s important to understand here is that, when execution enters the inner block, and a new variable x is defined, both variables are in scope; we simply have no way of accessing the variable in the outer scope (because it has the same name). Contrast this with the previous example where one x came into scope and then exited scope before the second variable named x did the same.

To drive this point home, consider this example:

{

// outer block

let x = { color: "blue" };

let y = x; // y and x refer to the same object

let z = 3;

{

// inner block

let x = 5; // outer x now masked

console.log(x); // logs 5

console.log(y.color); // logs "blue"; object pointed to by

// y (and x in the outer scope) is

// still in scope

y.color = "red";

console.log(z); // logs 3; z has not been masked

}

console.log(x.color); // logs "red"; object modified in

// inner scope

console.log(y.color); // logs "red"; x and y point to the

// same object

console.log(z); // logs 3

}

NOTE

Variable masking is sometimes called variable shadowing (that is, a variable with the same name will shadow the variable in the outer scope). I’ve never cared for this term because shadows don’t usually completely obscure things, just make them darker. When a variable is masked, the masked variable is completely inaccessible using that name.

By now, it should be clear that scope is hierarchical: you can enter a new scope without leaving the old one. This establishes a scope chain that determines what variables are in scope: all variables in the current scope chain are in scope, and (as long as they’re not masked), can be accessed.

Functions, Closures, and Lexical Scope

So far, we’ve only been dealing with blocks, which make it simple to see lexical scope, especially if you indent your blocks. Functions, on the other hand, can be defined in one place and used in another, meaning you might have to do some hunting to understand their scope.

In a “traditional” program, all of your functions might be defined in global scope, and if you avoid referencing global scope in your functions (which I recommend), you don’t even need to think about what scope your functions have access to.

In modern JavaScript development, however, functions are often defined wherever they’re needed. They’re assigned to variables or object properties, added to arrays, passed into other functions, passed out of functions, and sometimes not given a name at all.

It’s quite common to intentionally define a function in a specific scope so that it explicitly has access to that scope. This is usually called a closure (you can think of closing the scope around the function). Let’s look at an example of a closure:

let globalFunc; // undefined global function

{

let blockVar = 'a'; // block-scoped variable

globalFunc = function() {

console.log(blockVar);

}

}

globalFunc(); // logs "a"

globalFunc is assigned a value within a block: that block (and its parent scope, the global scope) form a closure. No matter where you call globalFunc from, it will have access to the identifiers in that closure.

Consider the important implications of this: when we call globalFunc, it has access to blockVar despite the fact that we’ve exited that scope. Normally, when a scope is exited, the variables declared in that scope can safely cease to exist. Here, JavaScript notes that a function is defined in that scope (and that function can be referenced outside of the scope), so it has to keep the scope around.

So defining a function within a closure can affect the closure’s lifetime; it also allows us to access things we wouldn’t normally have access to. Consider this example:

let f; // undefined function

{

let o = { note: 'Safe' };

f = function() {

return o;

}

}

let oRef = f();

oRef.note = "Not so safe after all!";

Normally, things that are out of scope are strictly inaccessible. Functions are special in that they allow us a window into scopes that are otherwise inaccessible. We’ll see the importance of this in upcoming chapters.

Immediately Invoked Function Expressions

In Chapter 6, we covered function expressions. Function expressions allow us to create something called an immediately invoked function expression (IIFE). An IIFE declares a function and then runs it immediately. Now that we have a solid understanding of scope and closures, we have the tools we need to understand why we might want to do such a thing. An IIFE looks like this:

(function() {

// this is the IIFE body

})();

We create an anonymous function using a function expression, and then immediately call (invoke) that function. The advantage of the IIFE is that anything inside it has its own scope, and because it is a function, it can pass something out of the scope:

const message = (function() {

const secret = "I'm a secret!";

return `The secret is ${secret.length} characters long.`;

})();

console.log(message);

The variable secret is safe inside the scope of the IIFE, and can’t be accessed from outside. You can return anything you want from an IIFE, and it’s quite common to return arrays, objects, and functions. Consider a function that can report the number of times it’s been called in a way that can’t be tampered with:

const f = (function() {

let count = 0;

return function() {

return `I have been called ${++count} time(s).`;

}

})();

f(); // "I have been called 1 time(s)."

f(); // "I have been called 2 time(s)."

//...

Because the variable count is safely ensconced in the IIFE, there’s no way to tamper with it: f will always have an accurate count of the number of times it’s been called.

While block-scoped variables in ES6 have somewhat reduced the need for IIFEs, they are still quite commonly used, and are useful when you want to create a closure and return something out of it.

Function Scope and Hoisting

Prior to ES6’s introduction of let, variables were declared with var and had something called function scope (global variables declared with var, while not in an explicit function, share the same behavior).

When you declare a variable with let, it doesn’t spring into existence until you declare it. When you declare a variable with var, it’s available everywhere in the current scope…even before it’s declared. Before we see an example, remember that there’s a difference between a variable being undeclared and a variable that has the value undefined. Undeclared variables will result in an error, whereas variables that exist but have the value undefined will not:

let var1;

let var2 = undefined;

var1; // undefined

var2; // undefined

undefinedVar; // ReferenceError: notDefined is not defined

With let, you will get an error if you try to use a variable before it’s been declared:

x; // ReferenceError: x is not defined

let x = 3; // we'll never get here -- the error stops execution

Variables declared with var, on the other hand, can be referenced before they’re declared:

x; // undefined

var x = 3;

x; // 3

So what’s going on here? On the surface, it doesn’t make sense that you should be able to access a variable before it’s declared. Variables declared with var employ a mechanism called hoisting. JavaScript scans the entire scope (either a function or the global scope), and any variables declared with var are hoisted to the top. What’s important to understand is that only the declaration—not the assignment—is hoisted. So JavaScript would interpret the previous example as:

var x; // declaration (but not assignment) hoisted

x; // undefined

x = 3;

x; // 3

Let’s look at a more complicated example, side by side with the way JavaScript interprets it:

// what you write // how JavaScript interprets it

var x;

var y;

if(x !== 3) { if(x !== 3) {

console.log(y); console.log(y);

var y = 5; y = 5;

if(y === 5) { if(y === 5) {

var x = 3; x = 3;

} }

console.log(y); console.log(y);

} }

if(x === 3) { if(x === 3) {

console.log(y); console.log(y);

} }

I’m not suggesting that this is well-written JavaScript—it’s needlessly confusing and error-prone to use variables before you declare them (and there’s no practical reason to do so). But this example does make clear how hoisting works.

Another aspect of variables declared with var is that JavaScript doesn’t care if you repeat the definition:

// what you write // how JavaScript interprets it

var x;

var x = 3; x = 3;

if(x === 3) { if(x === 3) {

var x = 2; x = 2;

console.log(x); console.log(x):

} }

console.log(x); console.log(x);

This example should make it clear that (within the same function or global scope), var can’t be used to create new variables, and variable masking doesn’t happen as it does with let. In this example, there’s only one variable x, even though there’s a second var definition inside a block.

Again, this is not something I’m suggesting that you do: it only serves to confuse. The casual reader (especially one familiar with other languages) may glance at this example and reasonably assume that the author intended to create a new variable x whose scope is the block formed by the ifstatement, which is not what will happen.

If you are thinking “Why does var allow you to do these things that are confusing and useless?”, you now understand why let came about. You can certainly use var in a responsible and clear fashion, but it’s painfully easy to write code that is confusing and unclear. ES6 could not simply “fix” var, as that would break existing code; hence, let was introduced.

I cannot think of an example that uses var that could not be written better or more clearly using let. In other words, var offers no advantage over let, and many in the JavaScript community (including myself) believe that let will eventually completely replace var (it’s even possible thatvar definitions will eventually become deprecated).

So why understand var and hoisting? For two reasons. First, ES6 will not be ubiquitous for some time, meaning code will have to be transcompiled to ES5, and of course much existing code is written in ES5. So for some time to come, it will still be important to understand how var works. Second, function declarations are also hoisted, which brings us to our next topic.

Function Hoisting

Like variables declared with var, function declarations are hoisted to the top of their scope, allowing you to call functions before they’re declared:

f(); // logs "f"

function f() {

console.log('f');

}

Note that function expressions that are assigned to variables are not hoisted; rather, they are subject to the scoping rules of variables. For example:

f(); // TypeError: f is not a function

let f = function() {

console.log('f');

}

The Temporal Dead Zone

The temporal dead zone (TDZ) is a dramatic expression for the intuitive concept that variables declared with let don’t exist until you declare them. Within a scope, the TDZ for a variable is the code before the variable is declared.

For the most part, this should cause no confusion or problems, but there is one aspect of the TDZ that will trip up people who are familiar with JavaScript prior to ES6.

The typeof operator is commonly used to determine if a variable has been declared, and is considered a “safe” way to test for existence. That is, prior to let and the TDZ, for any identifier x, this was always a safe operation that would not result in an error:

if(typeof x === "undefined") {

console.log("x doesn't exist or is undefined");

} else {

// safe to refer to x....

}

This code is no longer safe with variables declared with let. The following will result in an error:

if(typeof x === "undefined") {

console.log("x doesn't exist or is undefined");

} else {

// safe to refer to x....

}

let x = 5;

Checking whether or not variables are defined with typeof will be less necessary in ES6, so in practice, the behavior of typeof in the TDZ should not cause a problem.

Strict Mode

The syntax of ES5 allowed for something called implicit globals, which have been the source of many frustrating programming errors. In short, if you forgot to declare a variable with var, JavaScript would merrily assume you were referring to a global variable. If no such global variable existed, it would create one! You can imagine the problems this caused.

For this (and other) reasons, JavaScript introduced the concept of strict mode, which would prevent implicit globals. Strict mode is enabled with the string "use strict" (you can use single or double quotes) on a line by itself, before any other code. If you do this in global scope, the entire script will execute in strict mode, and if you do it in a function, the function will execute in strict mode.

Because strict mode applies to the entire script if used in the global scope, you should use it with caution. Many modern websites combine various scripts together, and strict mode in the global scope of one will enable strict mode in all of them. While it would be nice if all scripts worked correctly in strict mode, not all of them do. So it’s generally inadvisable to use strict mode in global scope. If you don’t want to enable strict mode in every single function you write (and who would?), you can wrap all of your code in one function that’s executed immediately (something we’ll learn more about in Chapter 13):

(function() {

'use strict';

// all of your code goes here...it

// is executed in strict mode, but

// the strict mode won't contaminate

// any other scripts that are combined

// with this one

})();

Strict mode is generally considered a good thing, and I recommend you use it. If you’re using a linter (and you should be), it will save you from many of the same problems, but doubling up can’t hurt!

To learn more about what strict mode does, see the MDN article on strict mode.

Conclusion

Understanding scope is an important part of learning any programming language. The introduction of let brings JavaScript in line with most other modern languages. While JavaScript is not the first language to support closures, it is one of the first popular (nonacademic) languages to do so. The JavaScript community has used closures to great effect, and it’s an important part of modern JavaScript development.