Saturday, January 12, 2013
goto: javascript: Variables, Scoping and Hoisting
goto: javascript: Variables, Scoping and Hoisting
variables-scoping-and-hoisting
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.
variables-scoping-and-hoisting
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:
- Pull the declaration to the top of the function
- 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:
- Hoist function declaration.
- Hoist variable declaration.
- Execute function.
- 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:
- Hoist function declaration name to top of scope.
- 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.
- Assign 'jim' to variable name.
- 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.
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
Subscribe to:
Posts (Atom)
