In JavaScript, scope is the context in which code is executed. There are three types of scope: global scope, local scope (sometimes referred to as "function scope"), and eval scope.
Code defined using var
inside of a function is locally scoped, and is only "visible" to other expressions in that function, which includes code inside any nested/child functions. Variables defined in the global scope can be accessed from anywhere because it is the highest level and last stop in the scope chain.
Examine the code that follows and make sure you understand that each declaration of foo
is unique because of scope.
Sample: sample110.html
<!DOCTYPE html><html lang="en"><body><script> var foo = 0; // Global scope. console.log(foo); // Logs 0. var myFunction = function () { var foo = 1; // Local scope. console.log(foo); // Logs 1. var myNestedFunction = function () { var foo = 2; // Local scope. console.log(foo); // Logs 2. } (); } (); eval('var foo = 3; console.log(foo);'); // eval() scope. </script></body></html>
Make sure you understand that each foo
variable contains a different value because each one is defined in a specifically delineated scope.
An unlimited number of function and eval scopes can be created, while only one global scope is used by a JavaScript environment.
The global scope is the last stop in the scope chain.
Functions that contain functions create stacked execution scopes. These stacks, which are chained together, are often referred to as the scope chain.
JavaScript Does Not Have Block Scope
Since logic statements (if
) and looping statements (for
) do not create a scope, variables can overwrite each other. Examine the following code and make sure you understand that the value of foo
is being redefined as the program executes the code.
Sample: sample111.html
<!DOCTYPE html><html lang="en"><body><script> var foo = 1; // foo = 1. if (true) { foo = 2; // foo = 2. for (var i = 3; i <= 5; i++) { foo = i; // foo = 3, 4, then 5. console.log(foo); // Logs 3, 4, 5. } } </script></body></html>
So foo
is changing as the code executes because JavaScript has no block scope—only function, global, or eval scope.
Use var
Inside of Functions to Declare Variables and Avoid Scope Gotchas
JavaScript will declare any variables lacking a var
declaration (even those contained in a function or encapsulated functions) to be in the global scope instead of the intended local scope. Have a look at the code that follows and notice that without the use of var
to declare bar, the variable is actually defined in the global scope and not the local scope, where it should be.
Sample: sample112.html
<!DOCTYPE html><html lang="en"><body><script> var foo = function () { var boo = function () { bar = 2; // No var used, so bar is placed in the global scope at window.bar } (); } (); console.log(bar); // Logs 2, because bar is in the global scope. // As opposed to... var foo = function () { var boo = function () { var doo = 2; } (); } (); // console.log(doo); logs undefined. doo is in the boo function scope, so an error occurs </script></body></html>
The concept to take away here is that you should always use var
when defining variables inside of a function. This will prevent you from dealing with potentially confusing scope problems. The exception to this convention, of course, is when you want to create or change properties in the global scope from within a function.
The Scope Chain (aka Lexical Scoping)
There is a lookup chain that is followed when JavaScript looks for the value associated with a variable. This chain is based on the hierarchy of scope. In the code that follows, I am logging the value of sayHiText
from the func2
function scope.
Sample: sample113.html
<!DOCTYPE html><html lang="en"><body><script> var sayHiText = 'howdy'; var func1 = function () { var func2 = function () { console.log(sayHiText); // func2 scope, but it finds sayHiText in global scope. } (); } (); </script></body></html>
How is the value of sayHiText
found when it is not contained inside of the scope of the func2
function? JavaScript first looks in the func2
function for a variable named sayHiText
. Not finding func2
there, it looks up to func2
s parent function, func1
. The sayHiText
variable is not found in the func1
scope, either, so JavaScript then continues up to the global scope where sayHiText
is found, at which point the value of sayHiText
is delivered. If sayHiText
had not been defined in the global scope, undefined
would have been returned by JavaScript.
This is a very important concept to understand. Let's examine another code example, one in which we grab three values from three different scopes.
Sample: sample114.html
<!DOCTYPE html><html lang="en"><body><script> var x = 10; var foo = function () { var y = 20; var bar = function () { var z = 30; console.log(z + y + x); // z is local, y and z are found in the scope chain. } (); } () foo(); // Logs 60. </script></body></html>
The value for z
is local to the bar
function and the context in which the console.log
is invoked. The value for y
is in the foo
function, which is the parent of bar()
, and the value for x
is in the global scope. All of these are accessible to the bar
function via the scope chain. Make sure you understand that referencing variables in the bar
function will check all the way up the scope chain for the variables referenced.
The scope chain, if you think about it, is not that different from the prototype chain. Both are simply a way for a value to be looked up by checking a systematic and hierarchical set of locations.
The Scope Chain Lookup Returns the First Found Value
In the code sample that follows, a variable called x
exists in the same scope in which it is examined with console.log
. This "local" value of x
is used, and one might say that it shadows, or masks, the identically named x
variables found further up in the scope chain.
Sample: sample115.html
<!DOCTYPE html><html lang="en"><body><script> var x = false; var foo = function () { var x = false; bar = function () { var x = true; console.log(x); // Local x is first in the scope so it shadows the rest. } (); } foo(); // Logs true. </script></body></html>
Remember that the scope lookup ends when the variable is found in the nearest available link of the chain, even if the same variable name is used further up the chain.
Scope Is Determined During Function Definition, Not Invocation
Since functions determine scope and functions can be passed around just like any JavaScript value, one might think that deciphering the scope chain is complicated. It is actually very simple. The scope chain is decided based on the location of a function during definition, not during invocation. This is also called lexical scoping. Think long and hard about this, as most people stumble over it often in JavaScript code.
The scope chain is created before you invoke a function. Because of this, we can create closures. For example, we can have a function return a nested function to the global scope, yet our function can still access, via the scope chain, its parent function's scope. In the following sample, we define a parentFunction
that returns an anonymous function, and we call the returned function from the global scope. Because our anonymous function was defined as being contained inside of parentFunction
, it still has access to parentFunctions
scope when it is invoked. This is called a closure.
Sample: sample116.html
<!DOCTYPE html><html lang="en"><body><script> var parentFunction = function () { var foo = 'foo'; return function () { // Anonymous function being returned. console.log(foo); // Logs 'foo'. } } // nestedFunction refers to the nested function returned from parentFunction. var nestedFunction = parentFunction(); nestedFunction(); // Logs foo because the returned function accesses foo via the scope chain. </script></body></html>
The idea you should take away here is that the scope chain is determined during definition literally in the way the code is written. Passing around functions inside of your code will not change the scope chain.
Closures Are Caused by the Scope Chain
Take what you have learned about the scope chain and scope lookup from this article, and a closure should not be overly complicated to understand. In the following sample, we create a function called countUpFromZero
. This function actually returns a reference to the child function contained within it. When this child function (nested function) is invoked, it still has access to the parent function's scope because of the scope chain.
Sample: sample117.html
<!DOCTYPE html><html lang="en"><body><script> var countUpFromZero = function () { var count = 0; return function () { // Return nested child function when countUpFromZero is invoked. return ++count; // count is defined up the scope chain, in parent function. }; } (); // Invoke immediately, return nested function. console.log(countUpFromZero()); // Logs 1. console.log(countUpFromZero()); // Logs 2. console.log(countUpFromZero()); // Logs 3. </script></body></html>
Each time the countUpFromZero
function is invoked, the anonymous function contained in (and returned from) the countUpFromZero
function still has access to the parent function's scope. This technique, facilitated via the scope chain, is an example of a closure.
Conclusion
If you feel I have over-simplified closures, you are likely correct in this thought. But I did so purposely as I believe the important parts come from a solid understanding of functions and scope, not necessarily the complexities of execution context. If you are in need of an in-depth dive into closures, have a look at JavaScript Closures.
Comments