A binding associates a name with a value. Bindings live in Environment Records, one per scope, and each record points to the record around it (ECMA-262, Environment Records). A name is resolved by walking outwards through that chain.
Kinds of declaration
| Declaration | Scope | Before the declaration runs | Redeclare in same scope | Reassign | Global object property (script) |
|---|---|---|---|---|---|
var | Function or script | undefined | Yes | Yes | Yes |
function | Function or script; block in strict code | The function | Yes, except in a module or strict block | Yes | Yes |
let | Block | ReferenceError | No | Yes | No |
const | Block | ReferenceError | No | No | No |
class | Block | ReferenceError | No | Yes | No |
using, await using (draft) | Block | ReferenceError | No | No | No |
import | Module | ReferenceError until the exporter is evaluated | No | No | No |
var
VariableStatement:
var VariableDeclarationList ;
VariableDeclaration:
BindingIdentifier [Initializer]
BindingPattern Initializer
A var binding belongs to the nearest function, or to the script or
module. It ignores blocks. A later var of the same name is the same
binding, and a var with no initialiser leaves the value alone.
let and const
LexicalDeclaration:
LetOrConst BindingList ;
LexicalBinding:
BindingIdentifier [Initializer]
BindingPattern Initializer
- Both are scoped to the enclosing block, or to the statement for a
forhead. - A
constmust have an initialiser. Aletwith no initialiser starts asundefined. - A
constbinding can never be assigned again, and an assignment throws aTypeErrorwhen it runs. The value itself may still change:const a = []; a.push(1);is fine. - The same name cannot be declared twice in one scope, or with
varin the same scope. That is an early error.
Hoisting
Before any code in a scope runs, all of its declarations are instantiated (for example, GlobalDeclarationInstantiation and FunctionDeclarationInstantiation). The name is in the scope from the start, wherever the declaration is written. This is called hoisting.
| Declaration | Created when entering scope | Value at first |
|---|---|---|
var | Yes | undefined |
function declaration | Yes | The function object, so it can be called above its definition |
let, const, class | Yes | Uninitialised, so reading it throws |
import | Yes (at link time) | A live view of the exporter’s binding |
console.log(a); // undefined
console.log(f()); // 1
var a = 2;
function f() { return 1; }
console.log(b); // ReferenceError
let b = 3;
Functions in blocks
In strict code, a function declared in a block is scoped to that block. In sloppy
code, for web compatibility, Annex B also makes a var-like binding of
that name in the enclosing function, starting as undefined, and assigns
the function to it when the declaration is reached (Block-Level Function Declarations
Web Legacy Compatibility Semantics). This does not happen if it would conflict with
a let or const.
// sloppy mode
console.log(typeof g); // "undefined"
{
function g() {}
}
console.log(typeof g); // "function"
The temporal dead zone
A let, const or class binding is uninitialised
from the start of its scope until its declaration is evaluated. Any use during that
time throws a ReferenceError. The period is called the temporal dead zone
because it depends on the time of execution, not the position in the text.
function read() { return x; }
// read(); would throw: x is in its dead zone
let x = 1;
read(); // 1: the declaration has now run
let y = y + 1; // ReferenceError: y is read before it is initialised
typeof z; // ReferenceError if z is declared later with let
let z = 2;
Default parameters are evaluated in order, so they follow the same rule:
function f(a = b, b) {} throws when a’s default is used.
Kinds of scope
| Scope | Created by |
|---|---|
| Global | Each realm; shared by all scripts |
| Module | Each module |
| Function | Each call of a function |
| Block | { }, the case block of switch, and for heads |
| Catch | The parameter of a catch clause |
| Class | A class’s name and private names, plus each field initialiser and static block |
| Object environment | The with statement, and the global object |
Loop bindings
A let in a for head gets a new binding for each iteration
(CreatePerIterationEnvironment), copied from the previous one, so a closure made in
the body sees that iteration’s value.
const a = [], b = [];
for (var i = 0; i < 3; i++) a.push(() => i);
for (let j = 0; j < 3; j++) b.push(() => j);
a.map(f => f()); // [3, 3, 3]
b.map(f => f()); // [0, 1, 2]
var there is one binding; with let there is one per iterationClosures
A function keeps a reference to the Environment Record where it was created. Later, it can read and change the bindings in that scope, even after the scope’s code has finished. Together, the function and that environment are a closure.
function counter() {
let n = 0;
return { next: () => ++n, reset: () => { n = 0; } };
}
const c = counter();
c.next(); // 1
c.next(); // 2
c.reset();
c.next(); // 1
nClosures hold bindings, not copies of values. A binding is kept alive for as long as a function that can reach it exists.
The global object
Each realm has one global object, and globalThis (ES2020) refers to it
everywhere. Hosts add their own names for it: window and
self in browsers, and global in Node.js.
The global scope is a Global Environment Record, which combines an Object Environment Record for the global object with a Declarative Environment Record for lexical declarations.
| Top level of a script | Goes to | Property of the global object | Configurable |
|---|---|---|---|
var, function | Object record | Yes | No |
| Assignment to an undeclared name (sloppy) | Object record | Yes | Yes, so delete works |
let, const, class | Declarative record | No |
Lexical top-level names are shared by all scripts in the realm, so declaring the same
let in two scripts throws a SyntaxError on the second. A
module’s top-level names stay inside the module. The standard global objects
are listed on the Built-ins page.
Eval and with
A direct call eval(text) runs code in the caller’s scope. Any other
call, for example (0, eval)(text), is indirect and runs it in the
global scope. In sloppy code, a direct eval can add var
bindings to the calling function. In strict code, and when the evaluated text is strict,
it gets a variable environment of its own.
The with statement adds an Object Environment Record for its object to
the chain, so names are looked up as properties first. It is a syntax error in strict
code. See Statements.