Scoping

For beginners, JavaScript scoping is slightly confusing. These concepts may seem straightforward; however, they are not. Some important subtleties exist that must be understood in order to master the concept. So what is Scope? In JavaScript, scope refers to the current context of code.

A variable's scope is the context in which the variable exists. The scope specifies from where you can access a variable and whether you have access to the variable in that context. Scopes can be globally or locally defined.

Global scope

Any variable that you declare is by default defined in global scope. This is one of the most annoying language design decisions taken in JavaScript. As a global variable is visible in all other scopes, a global variable can be modified by any scope. Global variables make it harder to run loosely coupled subprograms in the same program/module. If the subprograms happen to have global variables that share the same names, then they will interfere with each other and likely fail, usually in difficult-to-diagnose ways. This is sometimes known as namespace clash. We discussed global scope in the previous chapter but let's revisit it briefly to understand how best to avoid this.

You can create a global variable in two ways:

  • The first way is to place a var statement outside any function. Essentially, any variable declared outside a function is defined in the global scope.
  • The second way is to omit the var statement while declaring a variable (also called implied globals). I think this was designed as a convenience for new programmers but turned out to be a nightmare. Even within a function scope, if you omit the var statement while declaring a variable, it's created by default in the global scope. This is nasty. You should always run your program against ESLint or JSHint to let them flag such violations. The following example shows how global scope behaves:
    //Global Scope
    var a = 1;
    function scopeTest() {
      console.log(a);
    }
    scopeTest();  //prints 1

Here we are declaring a variable outside the function and in the global scope. This variable is available in the scopeTest() function. If you assign a new value to a global scope variable within a function scope (local), the original value in the global scope is overwritten:

//Global Scope
var a = 1;
function scopeTest() {
  a = 2; //Overwrites global variable 2, you omit 'var'
  console.log(a);
}
console.log(a); //prints 1
scopeTest();  //prints 2
console.log(a); //prints 2 (global value is overwritten)

Local scope

Unlike most programming languages, JavaScript does not have block-level scope (variables scoped to surrounding curly brackets); instead, JavaScript has function-level scope. Variables declared within a function are local variables and are only accessible within that function or by functions inside that function:

var scope_name = "Global";
function showScopeName () {
  // local variable; only accessible in this function
  var scope_name = "Local";
  console.log (scope_name); // Local
}
console.log (scope_name);     //prints - Global
showScopeName();             //prints – Local

Function-level scope versus block-level scope

JavaScript variables are scoped at the function level. You can think of this as a small bubble getting created that prevents the variable to be visible from outside this bubble. A function creates such a bubble for variables declared inside the function. You can visualize the bubbles as follows:

-GLOBAL SCOPE---------------------------------------------|
var g =0;                                                 |
function foo(a) { -----------------------|                |
    var b = 1;                           |                |
    //code                               |                |
    function bar() { ------|             |                |
        // ...             |ScopeBar     | ScopeFoo       |
    }                ------|             |                |
    // code                              |                |
    var c = 2;                           |                |
}----------------------------------------|                |
foo();   //WORKS                                          |
bar();   //FAILS                                          |
----------------------------------------------------------|

JavaScript uses scope chains to establish the scope for a given function. There is typically one global scope, and each function defined has its own nested scope. Any function defined within another function has a local scope that is linked to the outer function. It's always the position in the source that defines the scope. When resolving a variable, JavaScript starts at the innermost scope and searches outwards. With this, let's look at various scoping rules in JavaScript.

In the preceding crudely drawn visual, you can see that the foo() function is defined in the global scope. The foo() function has its local scope and access to the g variable because it's in the global scope. The a, b, and c variables are available in the local scope because they are defined within the function scope. The bar() function is also declared within the function scope and is available within the foo() function. However, once the function scope is over, the bar() function is not available. You cannot see or call the bar() function from outside the foo() function—a scope bubble.

Now that the bar() function also has its own function scope (bubble), what is available in here? The bar() function has access to the foo() function and all the variables created in the parent scope of the foo() function—a, b, and c. The bar() function also has access to the global scoped variable, g.

This is a powerful idea. Take a moment to think about it. We just discussed how rampant and uncontrolled global scope can get in JavaScript. How about we take an arbitrary piece of code and wrap it around with a function? We will be able to hide and create a scope bubble around this piece of code. Creating the correct scope using function wrapping will help us create correct code and prevent difficult-to-detect bugs.

Another advantage of the function scope and hiding variables and functions within this scope is that you can avoid collisions between two identifiers. The following example shows such a bad case:

function foo() {
  function bar(a) {
    i = 2; // changing the 'i' in the enclosing scope's for-loop
    console.log(a+i);
  }
  for (var i=0; i<10; i++) {
    bar(i); // infinite loop
  }
}
foo();

In the bar() function, we are inadvertently modifying the value of i=2. When we call bar() from within the for loop, the value of the i variable is set to 2 and we never come out of an infinite loop. This is a bad case of namespace collision.

So far, using functions as a scope sounds like a great way to achieve modularity and correctness in JavaScript. Well, though this technique works, it's not really ideal. The first problem is that we must create a named function. If we keep creating such functions just to introduce the function scope, we pollute the global scope or parent scope. Additionally, we have to keep calling such functions. This introduces a lot of boilerplate, which makes the code unreadable over time:

var a = 1;
//Lets introduce a function -scope
//1. Add a named function foo() into the global scope
function foo() { 
  var a = 2;
  console.log( a ); // 2
} 
//2. Now call the named function foo()
foo();
console.log( a ); // 1

We introduced the function scope by creating a new function foo() to the global scope and called this function later to execute the code.

In JavaScript, you can solve both these problems by creating functions that immediately get executed. Carefully study and type the following example:

var a = 1;
//Lets introduce a function -scope
//1. Add a named function foo() into the global scope
(function foo() { 
    var a = 2;
    console.log( a ); // 2
})(); //<---this function executes immediately
console.log( a ); // 1

Notice that the wrapping function statement starts with function. This means that instead of treating the function as a standard declaration, the function is treated as a function expression.

The (function foo(){ }) statement as an expression means that the identifier foo is found only in the scope of the foo() function, not in the outer scope. Hiding the name foo in itself means that it does not pollute the enclosing scope unnecessarily. This is so useful and far better. We add () after the function expression to execute it immediately. So the complete pattern looks as follows:

(function foo(){ /* code */ })();

This pattern is so common that it has a name: IIFE, which stands for Immediately Invoked Function Expression. Several programmers omit the function name when they use IIFE. As the primary use of IIFE is to introduce function-level scope, naming the function is not really required. We can write the earlier example as follows:

var a = 1;
(function() { 
    var a = 2;
    console.log( a ); // 2
})(); 
console.log( a ); // 1

Here we are creating an anonymous function as IIFE. While this is identical to the earlier named IIFE, there are a few drawbacks of using anonymous IIFEs:

  • As you can't see the function name in the stack traces, debugging such code is very difficult
  • You cannot use recursion on anonymous functions (as we discussed earlier)
  • Overusing anonymous IIFEs sometimes results in unreadable code

Douglas Crockford and a few other experts recommend a slight variation of IIFE:

(function(){ /* code */ }());

Both these IIFE forms are popular and you will see a lot of code using both these variations.

You can pass parameters to IIFEs. The following example shows you how to pass parameters to IIFEs:

(function foo(b) { 
    var a = 2;
    console.log( a + b ); 
})(3); //prints 5

Inline function expressions

There is another popular usage of inline function expressions where the functions are passed as parameters to other functions:

function setActiveTab(activeTabHandler, tab){
  //set active tab
  //call handler
  activeTabHandler();
}
setActiveTab( function (){ 
  console.log( "Setting active tab" );
}, 1 );
//prints "Setting active tab"

Again, you can name this inline function expression to make sure that you get a correct stack trace while you are debugging the code.

Block scopes

As we discussed earlier, JavaScript does not have the concept of block scopes. Programmers familiar with other languages such as Java or C find this very uncomfortable. ECMAScript 6 (ES6) introduces the let keyword to introduce traditional block scope. This is so incredibly convenient that if you are sure your environment is going to support ES6, you should always use the let keyword. See the following code:

var foo = true;
if (foo) {
  let bar = 42; //variable bar is local in this block { }
  console.log( bar );
}
console.log( bar ); // ReferenceError

However, as things stand today, ES6 is not supported by default in most popular browsers.

This chapter so far should have given you a fair understanding of how scoping works in JavaScript. If you are still unclear, I would suggest that you stop here and revisit the earlier sections of this chapter. Research your doubts on the Internet or put your questions on Stack Overflow. In short, make sure that you have no doubts related to the scoping rules.

It is very natural for us to think of code execution happening from top to bottom, line by line. This is how most of JavaScript code is executed but with some exceptions.

Consider the following code:

console.log( a );
var a = 1;

If you said this is an invalid code and will result in undefined when we call console.log(), you are absolutely correct. However, what about this?

a = 1;
var a;
console.log( a );

What should be the output of the preceding code? It is natural to expect undefined as the var a statement comes after a = 1, and it would seem natural to assume that the variable is redefined and thus assigned the default undefined. However, the output will be 1.

When you see var a = 1, JavaScript splits it into two statements: var a and a = 1. The first statement, the declaration, is processed during the compilation phase. The second statement, the assignment, is left in place for the execution phase.

So the preceding snippet would actually be executed as follows:

var a;   //----Compilation phase

a = 1;    //------execution phase
console.log( a );

The first snippet is actually executed as follows:

var a;     //-----Compilation phase

console.log( a );   
a = 1;     //------execution phase  

So, as we can see, variable and function declarations are moved up to the top of the code during compilation phase—this is also popularly known as hoisting. It is very important to remember that only the declarations themselves are hoisted, while any assignments or other executable logic are left in place. The following snippet shows you how function declarations are hoisted:

foo();
function foo() {
  console.log(a); // undefined
  var a = 1;
}

The declaration of the foo() function is hoisted such that we are able to execute the function before defining it. One important aspect of hoisting is that it works per scope. Within the foo() function, declaration of the a variable will be hoisted to the top of the foo() function, and not to the top of the program. The actual execution of the foo() function with hoisting will be something as follows:

function foo() {
  var a;
  console.log(a); // undefined
  a = 1;
}

We saw that function declarations are hoisted but function expressions are not. The next section explains this case.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset