JavaScript with Mozilla extensions has both function-scoped var
s and block-scoped let
s. Along with hoisting and dynamic behavior, scope in JavaScript is sometimes surprising.
var
function
-scoped- hoist to the top of its function
- redeclarations of the same name in the same scope are no-ops
const
function
-scoped- hoist to the top of its function
- redeclarations of the same name in the same scope are rejected
let
- block-scoped
- hoist to the top of its block (not in ECMAScript 6!)
- redeclarations illegal
- behaves exactly the same as
var
s at function top-level (i.e. can be redeclared at function top-level even though cannot be elsewhere)
function
Three forms with different scope behavior:
- declared: as a statement at the parent function top-level
- behaves like a
var
binding that gets initialized to that function - initialization "hoists" to the very top of the parent function, above
var
s
- behaves like a
- statement: as a statement in a child block
- behaves like a
var
binding that gets initialized to that function - does not hoist to the top of the parent function
- behaves like a
- expressed: inside an expression
- bound in the expression only
Hoisting
Hoisting is perhaps the most surprising behavior and prone to the most hiccups. The general thing to remember is:
This figures into computation of upvars and shadowing as well.
Hoisting also cannot "cross paths", as a consequence of the coexistence of var
s and let
s. Doing so results in error.
let
s cannot hoist abovevar
s of the same name
function f() { { var x; let x; // error, hoisting crosses var x } }
var
s cannot hoist abovelet
s of the same name
function f() { { let x; { var x; // error, hoisting crosses let x } } }
Due to let
s being var
s at the function top-level, however, the following is okay.
function f() { let x; { var x; // okay, actually redeclaring a var, so acts as a no-op } }
May interact surprisingly with catch
-blocks.
function f() { try { throw "e"; } catch(x) { var x; x = "catch"; // assignment to block-local x } print(x); // undefined }
In ECMAScript 6, let
does not hoist the variable to the top of the block. If you reference a variable in a block before the let
declaration for that variable is encountered, this results in a ReferenceError
, because the variable is in a "temporal dead zone" from the start of the block until the declaration is processed.
function f() { console.log(x); // ReferenceError let x = 2; }
Parameters
- Multiple function parameters may share the same name. The last one is bound.
function f(x, x) { print(x); } f("foo", "bar"); // "bar"
- Parameter names may shadow the function name itself inside the scope of the function.
function f(f) { print(f); } f("foo"); // "foo"
var
s, however, do not shadow parameter names. And since function top-levellet
s arevar
s,let
s don't either! The following prints "foo" because the declaration acts as a no-op as there is already a parameter namedx
.
function f(x, y) { var x; arguments[0] = "foo"; print(x); // "foo" }
with captures assignments, but not var declarations
Recall the hoisting rule above. Since with
injects an object into the scope chain, what looks like assignments to variables might actually be assignments into properties of that object. And since variable definitions are actually two-part declaration and assignment, definitions of var
s inside a with
might not do what you think. The following two examples are equivalent.
function f() { var o = {x: "foo"}; with (o) { var x = "bar"; } print(o.x); // "bar" }
function f() { var x; var o = {x: "foo"}; with (o) { x = "bar"; } print(o.x); // "bar" }
Note that let
s still behave unsurprisingly as their declarations do not hoist outside of the with
. They shadow properties of the same name.
eval may capture assignments, but not var declarations
eval'
d vars hoist normally, so eval
s may capture assignments similar to with
:
function f() { { let x = "inner"; eval("var x = 'outer'"); print(x); // "outer" } }
for heads
var
s infor
heads hoist to the top of the function. The following two examples are equivalent.
function f() { for (var i = 0; i < c; i++) { ... } }
function f() { var i; for (i = 0; i < c; i++) { ... } }
So it is not safe to nest vars of the same name in for
heads, even if it is your intention to shadow the variable from the outer loop.
let
s infor
heads create an implicit block around the condition, update, and body parts of the for loop. The following two examples are equivalent.
function f() { for (let i = 0; i < c; i++) { ... } }
function f() { { let i; for (i = 0; i < c; i++) { ... } } }
There is no new let
every iteration. There is one let
around the entire loop. This behavior might change in the future: https://bugzilla.mozilla.org/show_bug.cgi?id=449811
catch variables are block-scoped
Variables that are caught in catch blocks are block-scoped, like let
s.
function f() { try { throw "foo"; } catch (e) { } // e undefined here }
let statements and expressions
let
statements creates bindings in the accompanying block.
function f() { let (x) { x = "foo"; print(x); // "foo" } // x is undefined here let (x = "bar") { print(x); // "bar" } // x is undefined here }
let
expressions creates binding in the accompanying expression.
function f() { (1 + (let (i = 1) i)); // 2 ((let (i = 1) i) + i); // error, second use of i is unbound }
function oddities
function
s do not hoist when declared inside a child block.
function f() { { g(); // error, g undefined function g() { ... } } }
- "dynamic scope", where the scope of the parent function in which an inner function is defined can be mutated at run-time.
function g() { print("global"); } function f(cond) { if (cond) { function g() { print("inner"); } } g(); // "inner" when cond, "global" when !cond }
- Named function expressions are expression-scoped. Their names are only bound inside the expression in which they're defined. They also don't mutate the existing scope.
function f() { (function g() { print("g"); })(); g(); // error, g undefined }
- Functions initializations happen at the top of the parent function (above
var
s). Sincevar
s declarations with names already existent as a parameter or a function are no-ops, we get some surprising results.
function f() { function g() { print("foo"); } var g; g(); // "foo" }
function f() { var g = 0; function g() { print("foo"); } g(); // error, not a function because the function g's initialization to the function is overwritten by its assignment to 0 }
- Functions are not hoisted at all if they're inside a block, but they can still mutate existing scope.
function f() { var g = 0; if (cond) { function g() { print("foo"); } } g(); // prints "foo" when cond, error when !cond }
E4X selector predicates
E4X selector predicates add an XML item to the scope chain to evaluate the filter expression.
list = <><item><name>foo</name></item><item><name>bar</name></item><item><name>baz</name></item></>; subList = list.(String(name) === "bar")