TLDR
JavaScript has lexical (also called static) scoping and closures. This means you can tell the scope of an identifier by looking at the source code.
The four scopes are:
- Global - visible by everything
- Function - visible within a function (and its sub-functions and blocks)
- Block - visible within a block (and its sub-blocks)
- Module - visible within a module
Outside of the special cases of global and module scope, variables are declared using var
(function scope), let
(block scope), and const
(block scope). Most other forms of identifier declaration have block scope in strict mode.
Overview
Scope is the region of the codebase over which an identifier is valid.
A lexical environment is a mapping between identifier names and the values associated with them.
Scope is formed of a linked nesting of lexical environments, with each level in the nesting corresponding to a lexical environment of an ancestor execution context.
These linked lexical environments form a scope "chain". Identifier resolution is the process of searching along this chain for a matching identifier.
Identifier resolution only occurs in one direction: outwards. In this way, outer lexical environments cannot "see" into inner lexical environments.
There are three pertinent factors in deciding the scope of an identifier in JavaScript:
- How an identifier was declared
- Where an identifier was declared
- Whether you are in strict mode or non-strict mode
Some of the ways identifiers can be declared:
var
, let
and const
- Function parameters
- Catch block parameter
- Function declarations
- Named function expressions
- Implicitly defined properties on the global object (i.e., missing out
var
in non-strict mode)
import
statements
eval
Some of the locations identifiers can be declared:
- Global context
- Function body
- Ordinary block
- The top of a control structure (e.g., loop, if, while, etc.)
- Control structure body
- Modules
Declaration Styles
var
Identifiers declared using var
have function scope, apart from when they are declared directly in the global context, in which case they are added as properties on the global object and have global scope. There are separate rules for their use in eval
functions.
let and const
Identifiers declared using let
and const
have block scope, apart from when they are declared directly in the global context, in which case they have global scope.
Note: let
, const
and var
are all hoisted. This means that their logical position of definition is the top of their enclosing scope (block or function). However, variables declared using let
and const
cannot be read or assigned to until control has passed the point of declaration in the source code. The interim period is known as the temporal dead zone.
function f() {
function g() {
console.log(x)
}
let x = 1
g()
}
f() // 1 because x is hoisted even though declared with `let`!
Function parameter names
Function parameter names are scoped to the function body. Note that there is a slight complexity to this. Functions declared as default arguments close over the parameter list, and not the body of the function.
Function declarations
Function declarations have block scope in strict mode and function scope in non-strict mode. Note: non-strict mode is a complicated set of emergent rules based on the quirky historical implementations of different browsers.
Named function expressions
Named function expressions are scoped to themselves (e.g., for the purpose of recursion).
Implicitly defined properties on the global object
In non-strict mode, implicitly defined properties on the global object have global scope, because the global object sits at the top of the scope chain. In strict mode, these are not permitted.
eval
In eval
strings, variables declared using var
will be placed in the current scope, or, if eval
is used indirectly, as properties on the global object.
Examples
The following will throw a ReferenceError because the namesx
, y
, and z
have no meaning outside of the function f
.
function f() {
var x = 1
let y = 1
const z = 1
}
console.log(typeof x) // undefined (because var has function scope!)
console.log(typeof y) // undefined (because the body of the function is a block)
console.log(typeof z) // undefined (because the body of the function is a block)
The following will throw a ReferenceError for y
and z
, but not for x
, because the visibility of x
is not constrained by the block. Blocks that define the bodies of control structures like if
, for
, and while
, behave similarly.
{
var x = 1
let y = 1
const z = 1
}
console.log(x) // 1
console.log(typeof y) // undefined because `y` has block scope
console.log(typeof z) // undefined because `z` has block scope
In the following, x
is visible outside of the loop because var
has function scope:
for(var x = 0; x < 5; ++x) {}
console.log(x) // 5 (note this is outside the loop!)
...because of this behavior, you need to be careful about closing over variables declared using var
in loops. There is only one instance of variable x
declared here, and it sits logically outside of the loop.
The following prints 5
, five times, and then prints 5
a sixth time for the console.log
outside the loop:
for(var x = 0; x < 5; ++x) {
setTimeout(() => console.log(x)) // closes over the `x` which is logically positioned at the top of the enclosing scope, above the loop
}
console.log(x) // note: visible outside the loop
The following prints undefined
because x
is block-scoped. The callbacks are run one by one asynchronously. New behavior for let
variables means that each anonymous function closed over a different variable named x
(unlike it would have done with var
), and so integers 0
through 4
are printed.:
for(let x = 0; x < 5; ++x) {
setTimeout(() => console.log(x)) // `let` declarations are re-declared on a per-iteration basis, so the closures capture different variables
}
console.log(typeof x) // undefined
The following will NOT throw a ReferenceError
because the visibility of x
is not constrained by the block; it will, however, print undefined
because the variable has not been initialised (because of the if
statement).
if(false) {
var x = 1
}
console.log(x) // here, `x` has been declared, but not initialised
A variable declared at the top of a for
loop using let
is scoped to the body of the loop:
for(let x = 0; x < 10; ++x) {}
console.log(typeof x) // undefined, because `x` is block-scoped
The following will throw a ReferenceError
because the visibility of x
is constrained by the block:
if(false) {
let x = 1
}
console.log(typeof x) // undefined, because `x` is block-scoped
Variables declared using var
, let
or const
are all scoped to modules:
// module1.js
var x = 0
export function f() {}
//module2.js
import f from 'module1.js'
console.log(x) // throws ReferenceError
The following will declare a property on the global object because variables declared using var
within the global context are added as properties to the global object:
var x = 1
console.log(window.hasOwnProperty('x')) // true
let
and const
in the global context do not add properties to the global object, but still have global scope:
let x = 1
console.log(window.hasOwnProperty('x')) // false
Function parameters can be considered to be declared in the function body:
function f(x) {}
console.log(typeof x) // undefined, because `x` is scoped to the function
Catch block parameters are scoped to the catch-block body:
try {} catch(e) {}
console.log(typeof e) // undefined, because `e` is scoped to the catch block
Named function expressions are scoped only to the expression itself:
(function foo() { console.log(foo) })()
console.log(typeof foo) // undefined, because `foo` is scoped to its own expression
In non-strict mode, implicitly defined properties on the global object are globally scoped. In strict mode, you get an error.
x = 1 // implicitly defined property on the global object (no "var"!)
console.log(x) // 1
console.log(window.hasOwnProperty('x')) // true
In non-strict mode, function declarations have function scope. In strict mode, they have block scope.
'use strict'
{
function foo() {}
}
console.log(typeof foo) // undefined, because `foo` is block-scoped
How it works under the hood
Scope is defined as the lexical region of code over which an identifier is valid.
In JavaScript, every function-object has a hidden [[Environment]]
reference that is a reference to the lexical environment of the execution context (stack frame) within which it was created.
When you invoke a function, the hidden [[Call]]
method is called. This method creates a new execution context and establishes a link between the new execution context and the lexical environment of the function-object. It does this by copying the [[Environment]]
value on the function-object, into an outer reference field on the lexical environment of the new execution context.
Note that this link between the new execution context and the lexical environment of the function object is called a closure.
Thus, in JavaScript, scope is implemented via lexical environments linked together in a "chain" by outer references. This chain of lexical environments is called the scope chain, and identifier resolution occurs by searching up the chain for a matching identifier.
Find out more.
Best Answer
A closure is a pairing of:
A lexical environment is part of every execution context (stack frame) and is a map between identifiers (ie. local variable names) and values.
Every function in JavaScript maintains a reference to its outer lexical environment. This reference is used to configure the execution context created when a function is invoked. This reference enables code inside the function to "see" variables declared outside the function, regardless of when and where the function is called.
If a function was called by a function, which in turn was called by another function, then a chain of references to outer lexical environments is created. This chain is called the scope chain.
In the following code,
inner
forms a closure with the lexical environment of the execution context created whenfoo
is invoked, closing over variablesecret
:In other words: in JavaScript, functions carry a reference to a private "box of state", to which only they (and any other functions declared within the same lexical environment) have access. This box of the state is invisible to the caller of the function, delivering an excellent mechanism for data-hiding and encapsulation.
And remember: functions in JavaScript can be passed around like variables (first-class functions), meaning these pairings of functionality and state can be passed around your program: similar to how you might pass an instance of a class around in C++.
If JavaScript did not have closures, then more states would have to be passed between functions explicitly, making parameter lists longer and code noisier.
So, if you want a function to always have access to a private piece of state, you can use a closure.
...and frequently we do want to associate the state with a function. For example, in Java or C++, when you add a private instance variable and a method to a class, you are associating state with functionality.
In C and most other common languages, after a function returns, all the local variables are no longer accessible because the stack-frame is destroyed. In JavaScript, if you declare a function within another function, then the local variables of the outer function can remain accessible after returning from it. In this way, in the code above,
secret
remains available to the function objectinner
, after it has been returned fromfoo
.Uses of Closures
Closures are useful whenever you need a private state associated with a function. This is a very common scenario - and remember: JavaScript did not have a class syntax until 2015, and it still does not have a private field syntax. Closures meet this need.
Private Instance Variables
In the following code, the function
toString
closes over the details of the car.Functional Programming
In the following code, the function
inner
closes over bothfn
andargs
.Event-Oriented Programming
In the following code, function
onClick
closes over variableBACKGROUND_COLOR
.Modularization
In the following example, all the implementation details are hidden inside an immediately executed function expression. The functions
tick
andtoString
close over the private state and functions they need to complete their work. Closures have enabled us to modularise and encapsulate our code.Examples
Example 1
This example shows that the local variables are not copied in the closure: the closure maintains a reference to the original variables themselves. It is as though the stack-frame stays alive in memory even after the outer function exits.
Example 2
In the following code, three methods
log
,increment
, andupdate
all close over the same lexical environment.And every time
createObject
is called, a new execution context (stack frame) is created and a completely new variablex
, and a new set of functions (log
etc.) are created, that close over this new variable.Example 3
If you are using variables declared using
var
, be careful you understand which variable you are closing over. Variables declared usingvar
are hoisted. This is much less of a problem in modern JavaScript due to the introduction oflet
andconst
.In the following code, each time around the loop, a new function
inner
is created, which closes overi
. But becausevar i
is hoisted outside the loop, all of these inner functions close over the same variable, meaning that the final value ofi
(3) is printed, three times.Final points:
function
from inside another function is the classic example of closure, because the state inside the outer function is implicitly available to the returned inner function, even after the outer function has completed execution.eval()
inside a function, a closure is used. The text youeval
can reference local variables of the function, and in the non-strict mode, you can even create new local variables by usingeval('var foo = …')
.new Function(…)
(the Function constructor) inside a function, it does not close over its lexical environment: it closes over the global context instead. The new function cannot reference the local variables of the outer function.Links