Scope Cheatsheet

JavaScript with Mozilla extensions has both function-scoped vars and block-scoped lets. Along with hoisting and dynamic behavior, scope in JavaScript is sometimes surprising.

Much covered here is not standard ECMAScript.

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 vars 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 vars
  • 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
  • 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:

Every definition of a variable is really a declaration of the variable at the top of its scope and an assignment at the place where the definition is (see ECMAScript 6 exception below).

This figures into computation of upvars and shadowing as well.

Hoisting also cannot "cross paths", as a consequence of the coexistence of vars and lets. Doing so results in error.

  • lets cannot hoist above vars of the same name
function f() {
  {
    var x;
    let x; // error, hoisting crosses var x
  }
}
  • vars cannot hoist above lets of the same name
function f() {
  {
    let x;
    {
      var x; // error, hoisting crosses let x
    }
  }
}

Due to lets being vars 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"
  • vars, however, do not shadow parameter names. And since function top-level lets are vars, lets don't either! The following prints "foo" because the declaration acts as a no-op as there is already a parameter named x.
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 vars 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 lets 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 evals may capture assignments similar to with:

function f() {
  {
     let x = "inner";
     eval("var x = 'outer'");
     print(x); // "outer"
  }
}

for heads

  • vars in for 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.

  • lets in for 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 lets.

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

  • functions 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 vars). Since vars 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")

Document Tags and Contributors

Contributors to this page: Sheppy, syg, rogerhc, Dherman, getify
Last updated by: rogerhc,