Scope
Scope is the set of variables, functions, or arguments that JavaScript has access to at a given point in execution.
In JavaScript, there are 3 kinds of scopes:
Global
Variables declared outside all functions have a global scope and are accessible anywhere in the program. They can be views as being at the highest level of scope or at the last link of the scope chain.
Local
Variables declared within a function have a local scope and are only accessible within that function as well as any nested functions.
Eval
Variables declared within the
eval()
function adopt the current scope in whicheval()
was invoked. For instance, ifeval()
was called within the global scope, any variable declared within the function will have a global scope. However, ifeval()
was called within a function, any variable declared within the function will have the local scope of that function. (Not sure if this is correct)The following code demonstrates all 3 scopes:
var a = 'outer'; // global eval("var c = 'outer'"); // global function foo() { var a = 'inner'; // local eval("var b = 'inner'"); // local a; // 'inner' b; // 'inner' c; // 'outer' } a; // 'outer' b; // [1] error c; // 'outer'
/* Footnotes: [1] b is not defined */
var a
is defined both outside as well as withinfoo
. The variablea
defined outside offoo
is global, and the variablea
defined withinfoo
is local tofoo
.Moreover, notice that
var c
can be referenced both outside as well as withinfoo
. Becausec
is defined in a call toeval
that is made in the global scope,c
also inherits a global scope. Similarly,var b
is defined in a call toeval
made withinfoo's
local scope, makingb
also local tofoo
.Unlike languages such as Java or C++, JavaScript has no block scoping.
// JavaScript var a = 'outer'; { var a = 'inner'; a; // 'inner' } a; // 'inner' // changed
Compare this with Java:
// Java String a = 'outer'; { String a = 'inner'; a; // 'inner' } a; // 'outer' // unchanged
In JavaScript, variables within a block (
{ ... }
) have the same scope as variables directly outside of the block.Forgetting to declare a variable with a
var
keyword creates a global variable, regardless of where the variable is declared. It is best practice to always include thevar
keyword.function foo() { var a = 'local'; a; // 'local' b = 'global'; b; // 'global' } a; // error - a is not defined b; // 'global'
Notice that even though
b
was defined locally infoo
, it is accessible in the global scope. This is bad for several reasons. First, it makes your code much more unclear. It is not difficult to mistakenb
as a local variable when you are, in reality, dealing with a global variable. Second, you can easily override existing global variables within the code. Third, creating global variables in and of itself is bad practice, as your code will likely be in the presence of other programs—programs not written by you—and using true blue global variables will likely lead to naming conflicts and collisions with other existing code.
- The stack of currently accessible scopes, from the most immediate context to the global context, is called the scope chain.
function foo4() { function foo3() { function foo2() { function foo1() { ... } } } }
foo1's
scope chain would look something like:global \ foo4 \ foo3 \ foo2 \ foo1
When a variable is referenced in an execution context, JavaScript performs an identifier resolution within the context's scope chain. First, the JavaScript engine looks at the current scope for the variable, and if it finds it, it returns the variable. However, if it cannot find the variable, JavaScript bubbles up to the outer scope, and looks for it there. It continues to do this until it reaches the end of the chain. If it still cannot find the variable, it returns
undefined
. This is similar to the process JavaScript goes through when looking for an object's property within the prototype chain.Consider the following:
var a = 'global'; function foo() { var a = 'local'; a; // [1] 'local' } a; // 'global'
When
[1]
is reached,var a
is referenced, and the JavaScript engine looks fora
within the current scope (local scopefoo
). Upon finding the linevar a = 'local';
, it then returnsa
, which is why we get the value'local'
.Now, consider the following:
var a = 'global'; function foo() { var b = 'local'; a; // [1] 'global' }
This time, at
[1]
, whenvar a
is called, the JavaScript engine looks at the current scope (local scopefoo
) fora
and finds nothing. It then bubbles up to the outer scope and findsvar a = 'global';
and returns it, leaving us with the value'global'
.The scope of a variable is determined based on the location of a function during definition, not during invocation. This means that the scope chain is created before any variable or function is referenced or invoked, allowing us to create closures.
Closures are functions that refer to independent (or free) variables. In other words, the function defined in the closure "remembers" the environment in which it was created even after the function has returned.
Closure example #1:
var foo = (function () { var bob = "You have referenced bob."; return function () {return bob} })(); bob; // error - bob is not defined foo(); // "You have referenced bob."
Closure example #2:
var foo = (function () { var foo2 = function () {return "Running foo2()..."}; return foo2; })(); foo2(); // error - foo2 is not defined foo(); // "Running foo2()..."
Closure example #3:
var foo = (function () { var counter = 0; return function () {return "counter is now at " + (counter += 1)} })(); counter; // error - counter is not defined foo(); // "counter is now at 1" foo(); // "counter is now at 2" foo(); // "counter is now at 3"
The following code is equivalent to the code written above:
function foo1 () { var counter = 0; function foo2 () {return "counter is now at " + (counter += 1)} return foo2; } var foo3 = foo1(); // foo1 is invoked, returning foo2 into foo3 var foo4 = foo1(); // foo1 is invoked, returning foo2 into foo3 foo3(); // "counter is now at 1" foo3(); // "counter is now at 2" foo4(); // "counter is now at 1" foo4(); // "counter is now at 2" foo4(); // "counter is now at 3" foo3(); // "counter is now at 3"
Since
foo2
is nested withinfoo1
at the time of definition, it has access to all offoo1's
variables no matter where it is in the program. When we callfunction foo1
, it returnsfunction foo2
as a result, and we store it intovar foo3
(foo3
is now a function). Becausefoo2
still has access tofoo1's
counter
variable,foo3
has access to it as well. As a result, each time we invokefoo3
, it rememberscounter's
state, even thoughcounter
technically should not be able to be accessed outside offunction foo1
. This is possible becausefoo1
is within the scope chain offoo3
:global \ foo1 ------> { counter = ... } \ foo2 \ foo3
What's actually happening under the hood is that the JavaScript engine creates an execution context in memory for every function instantiation, in this case, for
var foo3
andvar foo4
. Each execution context remembers the state offoo3
andfoo4
as well as their scope chains, respectively. I will dive deeper into this topic next.Understanding the Execution Context
All JavaScript code is associated and executed within an execution context. JavaScript always starts out in the global execution context. Once a function is called, a new execution context associated with that function is created, and JavaScript enters into that context. If another function is called within the current function, another execution context is created, and JavaScript enters that context for the duration of the function call. After that function returns, JavaScript re-enters the previous execution context. You can see JavaScript's flow of execution as a stack of execution contexts, where only one context can be executed at a given time, and the current execution context is always at the top (similar to a stack data structure).
This code is copied and pasted from an earlier example:
function foo1 () { var counter = 0; function foo2 () {return "counter is now at " + (counter += 1)} return foo2; } var foo3 = foo1(); var foo4 = foo1(); foo3(); // [1] foo4(); // [2]
The stack of execution contexts would look something like:
[1] [2] *========* *========* | foo3 | | foo4 | *========* *========* *========* *========* *========* | global | --> | global | --> | global | --> | global | --> | global | *========* *========* *========* *========* *========*
When a function returns, its execution context gets popped off of the stack, and JavaScript returns to the stack below it.
Several things take place when an execution context is created:
JavaScript creates an
Activation
object associated with the function being called. This object has no defined prototype and cannot be directly referenced by us.JavaScript also creates an
arguments
object and assigns it as a property of theActivation
object. This object resembles anarray
and collects any arguments that might be passed to the function.The following function returns its own arguments object:
var foo = function() { return arguments; }; foo(); // [] foo(1, 2, 3); // [1, 2, 3] foo(1, 2, 3, "four"); // [1, 2, 3, "four"]
The actual
arguments
object returned byfoo(1, 2, "three")
would look something like:// arguments object of foo(1, 2, "three") Object { 0: 1, 1: 2, 2: "three", callee: function () {}, length: 3, Symbol(Symbol.iterator): function ArrayValues() {}, __proto__: Object }
NOTE: Although the
arguments
object acts like anArray
object, it is not actually an instance ofArray
—it is an instance ofObject
.var foo = function() {return typeof arguments}; foo(); // "object"
The execution context is then assigned a scope. The scope of a function is simply a property of the function, consisting of a list of objects that the function has access to. The
Activation
object associated with the function is placed at the top of the list. These objects make up the function's scope chain. Every function has this internal scope property (denoted as[[scope]]
), and just like theActivation
object, it, too, is not accessible to us.Here is a diagram of a function along with its
[[scope]]
property andActivation
object:// Scope Chain (list) | V function foo() { |===================| [[scope]]: -------> | Activation object | ------@ |===================| | ... | some object | | |===================| | } | ... | | |===================| | | some object | | |===================| | | | @-------------------------------------------@ | V // Activation object Object { arguments: Object { ... }, // arguments object ... }
NOTE: Notice that the
arguments
object mentioned earlier is a property of theActivation
object, and theActivation
object is at the top of the[[scope]]
property list.Next, the JavaScript engine creates new properties within the
Activation
object that correspond with the function's formal parameters, as well as any inner functions and local variables defined within the function.function foo(a, b, c) { // a, b, c -> formal parameters var d = 0; // d -> local variable function foo2() { ... } // foo2 -> inner function }
These values are added as properties to the
Activation
object:// Activation object Object { arguments: Object {...}, a: [reference to a], b: [reference to b], c: [reference to c], d: [reference to d], foo2: [reference to foo2], ... }
This is why a function can access all data members within its scope chain.