Wednesday, March 14, 2012

Variables, Scoping and Hoisting

During this post, I relay my personal understanding of how variables are scoped and the effect hoisting has upon program execution for variables and functions in scope.

For brevity, I deliberately avoid discussion of execution context, "this" and the relationship to scoping. This will be an excellent topic for a future post.

You can try the examples using an online javascript interpreter.



Programming languages use scoping to manage the visibility of variables throughout a program's execution path.

While many languages are block-scoped (if block, for loop, curly braces), Javascript is function-scoped, meaning new scopes are created through function definitions. Scopes are chained by nesting functions.

Local or immediate scopes have access to surrounding scopes. However surrounding (outer) scopes do not have access to nested scopes. Think of it as a car with heavily tinted windows. The person on the outside is not able to see in but the person on the inside can see out.

There is one global scope which wraps all  function scopes.  If a variable is defined without preceding it with "var" it's hoisted (more on hoisting later) to the global scope. In the browser, global scope is maintained by "window" object.

 1:  var person = 'mike', position = 'outside';   
 2:     
 3:  function car() {   
 4:   var passenger = 'james';   
 5:   writeln(passenger + ' can see ' + person + ' ' + position);   
 6:  }   
 7:     
 8:  car(); // james can see mike outside   
 9:    
 10: // ReferenceError: passenger is not defined    
 11: writeln( person + ' can not see ' + passenger + ' from ' + position );  

In the above example, there are two scopes - global and function. The function scope has access to all variables in its own scope and the surrounding global scope; Those being passenger: function scope, person: global scope and position: global scope.



This is evident in line: 5 which references passenger, person and position without error.

Line 11, in the above example, throws a ReferenceError because of an attempt to access the passenger variable, which is hidden in the nested function scope.

Javascript implements function-scoping using a technique commonly referred to as lexical scoping.

Lexical (static) Scope

Lexical scoping relates the closest surrounding scope to an identifier's exact location in the code. This is often referred to as static scoping because we're able to determine the scope of an identifier by looking at (parsing) the identifier's exact location (static position) in the script text, prior to runtime . Later we will see that this is not entirely correct due to something called "hoisting".

Lexical scoping relates the closest surrounding scope to an identifiers exact location in the code

 1:  var name = 'james', language = 'javascript';   
 2:     
 3:  function scopeOne () {   
 4:   writeln(name + ' is using ' + language);    
 5:  }   
 6:  scopeOne(); // james is using javascript   
 7:     
 8:  function scopeTwo(language) {   
 9:    var name = 'tony';   
 10:   function nestedScope() {   
 11:    var name = 'mike';   
 12:    writeln(name + ' is using ' + language);   
 13:   }   
 14:   nestedScope();   
 15:  }   
 16:     
 17:  scopeTwo('java'); // mike is using java   
 18:     
 19:  function scopeThree() {   
 20:   var name = 'peter', language = 'python';   
 21:   scopeOne();   
 22:  }   
 23:  scopeThree(); // james is using javascript   

Line 4 in scopeOne is able to reference it's variables by having first looked in its immediate local scope and then, having not found the required identifiers, progressed to the next scope in its scope chain. In this case, the next scope is the global scope which contains 'name' and 'language' on Line 1.

The next function, scopeTwo, introduces variable name shadowing. Recall reference assignment is always applied to the closest scope. In this case, name on line 12, references variable name declaration on line 11, even though name is visible in line 9 and line 1. Here, name on line 11 is said to shadow name on line 9. Likewise, name on line 9 shadows name on line 1.

A more interesting example is scopeThree function, which executes scopeOne declared outside of scopeThree. Although scopeThree contains variables with identical names to those used in scopeOne, scopeOne does not have access to scopeThree's scope because it is not an outer scope of the scopeOne's function declaration. Instead scopeOne references global variables on line 1 as they are the closest scoped variables. If we removed line 1, scopeOne would raise a ReferenceError.

The take away point is to focus on the closest scope of the function or variable declaration.

Hoisting

Hoisting applies to both function and variable declarations. Given our knowledge of scoping thus far, it's a natural progression to understand the workings of hoisting.

Function declarations and variable declarations are always moved ("hoisted") invisibly to the top of their containing scope for pre-processing by the Javascript interpreter.

Hoisting Variable Declarations

Hoisting Variable Declarations follow two simple rules:

  1.     Pull the declaration to the top of the function
  2.     Initialise the identifier in place

In the following example, one might be deceived into thinking a ReferenceError should occur as a result of the code on line 2. This would  be true if Javascript followed static scoping to the letter; But, Javascript interpreters use hoisting in order to know all declarations within a scope prior to execution.

Example before hoisting:

 1: function scopeOne() {   
 2:  writeln('My name is ' + name); // my name is undefined   
 3:  var name = 'james';   
 4:  writeln('and now my name is ' + name); // and now my name is james   
 5: }   

Example after hoisting:

 1: function scopeOne() {   
 2:  var name;   
 3:  writeln('My name is ' + name); // my name is undefined   
 4:  name = 'james';   
 5:  writeln('and now my name is ' + name); // and now my name is james   
 6: }  

After hoisting, only the variable declaration has changed location in the script text; all other lines, including the variable initialisation, remain in the same position.

Note: To minimise surprises resulting from hoisting, it's best to declare all locally scoped variables at the top of a function.

Hoisting Functions

Similarly, functions are also hoisted to the top their containing scope.

It's actually function hoisting that allows you to call functions that are defined further down in the script text.

Before proceeding - a short clarification of terms: Function Declaration vs Function Expression

Function Declaration:
 function myFunctionDeclaration() { writeln('function was declared'); }  

Function Expression:
 var myFunctionExpression = function() { writeln('anonymous function assigned to a variable'); };  


Example before hoisting:

1:  function before() {  
2:     after('jack');  
3:    
4:     function after(name) {  
5:        writeln( name + ' was here');     
6:     }  
7:  }  

Example after hoisting:

1:  function before() {  
2:     function after(name) {  
3:        writeln( name + ' was here');     
4:     }  
5:     after('jack');  
6:  }  

Unlike variable hoisting, which only hoists the variable declaration, function declarations are hoisted, as definition and body.  This is a subtle but important point. Lets mix function and variable hoisting to test our understanding.

Example before hoisting:

1:  function before() {  
2:     after('jack');  
3:    
4:     var after = function(name) {  
5:        writeln( name + ' was here');     
6:     };  
7:  }  
8:  before(); // TypeError: undefined is not a function  

You may be surprised to discover a TypeError has been raised.

Lets take a closer look.

Instead of using a function declaration, we are now using a function expression, which assigns an anonymous function to a variable. therefore replacing function hoisting rules, used in previous example, with variable hoisting rules.

We know variable hoisting raises the variable declaration to the top of scope, leaving the initialisation (in this case the anonymous function) in its original place.

Example after hoisting:

1:  function before() {  
2:     var after;  
3:     after('jack');  
4:    
5:     after = function(name) {  
6:        writeln( name + ' was here');     
7:     };  
8:  }  

Now the result makes much more sense.

Precedence When Hoisting Functions and Variables

Functions have hoisting precedence over variables.

Example before hoisting:
1:  function hoistingExample() {  
2:    functionsFirst() ;  
3:    
4:    var thenVariables = 'ok';   
5:    
6:     function functionsFirst() {  
7:        writeln('functions hoisted before variables: ' + thenVariables);  
8:     }     
9:  }   
10:  hoistingExample() ; // functions hoisted before variables: undefined  


Example after hoisting:
1:  function hoistingExample() {  
2:     function functionsFirst() {  
3:        writeln('functions hoisted before variables: ' + thenVariables);  
4:     }   
5:     var thenVariables;   
6:    functionsFirst();   
7:    thenVariables = 'ok';  
8:  }   
9:  hoistingExample() ; // functions hoisted before variables: undefined  

Looking at the above example, we can see the following steps took place to hoist the function and variable declaration:

  1. Hoist function declaration.
  2. Hoist variable declaration.
  3. Execute function.
  4. Initialise variable.

Notice only the function and variable declarations were moved up in the physical script location.

To solidify our understanding, lets try one more example: Hoisting functions and variables when both the declared function and variable share the same name.

Example before hoisting:
1:  var name = 'jim';  
2:  function name() {  
3:     hoistingVarsAndFunctions();  
4:     var name = 'jack';  
5:    
6:     function hoistingVarsAndFunctions() {  
7:        writeln('My name is ' + name);  
8:     }  
9:  }  
10: name(); // TypeError at line 10: name is not a function  

Is this what you expected?

Example after hoisting:
1:  function name() {  
2:     function hoistingVarsAndFunctions() {  
3:        writeln('My name is ' + name);  
4:     }  
5:     var name;  
6:     hoistingVarsAndFunctions();  
7:     name = 'jack';  
8:  }  
9:  var name;  
10: name = 'jim';  
11: name(); // TypeError at line 11: name is not a function  

Applying the hoisting rules:
  1. Hoist function declaration name to top of scope.
  2. Hoist variable delaration to top of scope, below hoisted function declaration. However function declaration has same name as variable, and so the function is replaced by the variable declaration.
  3. Assign 'jim' to variable name.
  4. Attempt to execute variable which points to a string. This raises a TypeError at line 11, indicating  variable 'name' is not a function.
Hopefully, the examples above make a strong case for favouring function expressions over function declarations in your code.

By declaring your variables at the top of a function, and using function expressions, you only have one set of  hoisting rules to contend with (variable hoisting rules).

Example before hoisting:
1:  function cleanCodeExample() {  
2:       var name = 'james',  
3:            preference = 'function expressions',  
4:            myFunctionExpression = function() {  
5:              writeln(name + ' prefers ' + preference);  
6:            };  
7:    
8:       myFunctionExpression();  
9:  }  
10:    
11: cleanCodeExample(); // james prefers function expressions  


Hoisting still occurs, but with one set of hoisting rules to contend with, these changes should not be an issue. You can see in the example below that the hoisting step still happens although the effects are of little concern.

Example after hoisting:
1:   function cleanCodeExample() {  
2:      var name,  
3:          preference,  
4:          myFunctionExpression;  
5:      name = 'james';   
6:      preference = 'function expressions';   
7:      myFunctionExpression = function() {   
8:         writeln(name + ' prefers ' + preference);   
9:      };   
10:     myFunctionExpression();   
11:   }   
12:   cleanCodeExample(); // james prefers function expressions  


Closing

In this post, I've covered lexical scoping of function and variable declarations, including the effect hoisting has on scope. These concepts lay the ground-work for understanding many of the advanced concepts in Javascript (ES 5.1) programming. 

The model presented is simplistic, avoiding discussion of objects and their properties, functions as objects and execution context state management. However, while simplistic, it is a good basis from which to build our understanding of objects, context,  identifier resolution and closures. I'll present a new model to cover these concepts in my next post. 

We know:
  • Javascript employs function scope only.
  • Nested functions can see their containing functions but containing (outer) functions cant see inside of nested functions
  • Nesting functions create a scope chain which terminates at the global scope.
  • Hoisting applies to both functions and variable declarations within scope
  • Hoisting rules differ for variable and function declarations. These rules catch out the noob and experienced developers alike.
  • Scope applies to declarations.
Further Reading


5 comments:

  1. After long research on HOIST, I have found all answers of my questions. Explanation is concrete and v. easy to understand.

    Well, Huge thanks

    ReplyDelete
  2. I learned more new things about java. Thank you so much for sharing this post.
    primary school website design

    ReplyDelete
  3. Great article. Glad to find your blog. Thanks for sharing.

    wordpress Training in Chennai

    ReplyDelete