Scope Cheatsheet

  • Revision slug: JavaScript/Reference/Scope_Cheatsheet
  • Revision title: Scope Cheatsheet
  • Revision id: 105308
  • Created:
  • Creator: syg
  • Is current revision? No
  • Comment 1 words added, 1 words removed

Revision Content

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

let

  • block-scoped
  • hoist to the top of its block
  • 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
    • bound in the child block only
    • does not hoist
  • 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.

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
  }
}

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.

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
}

Revision Source

<p>JavaScript with Mozilla extensions has both function-scoped <strong><code>var</code></strong>s and block-scoped <strong><code>let</code></strong>s. Along with hoisting and dynamic behavior, scope in JavaScript is sometimes surprising.</p>
<div class="warning">Much covered here is <em>not</em> standard ECMAScript.</div>
<h3>var</h3>
<ul> <li><strong><code>function</code></strong>-scoped</li> <li>hoist to the top of its function</li> <li>redeclarations of the same name in the same scope are no-ops</li>
</ul>
<h3>let</h3>
<ul> <li>block-scoped</li> <li>hoist to the top of its block</li> <li>redeclarations illegal</li> <li>behaves <em>exactly</em> the same as <code><strong>var</strong></code>s at function top-level (i.e. can be redeclared at function top-level even though cannot be elsewhere)</li>
</ul>
<h3>function</h3>
<p>Three forms with different scope behavior:</p>
<ul> <li><strong>declared</strong>: as a statement at the parent function top-level<br> <ul> <li>behaves like a <code><strong>var</strong></code> binding that gets initialized to that function</li> <li>initialization "hoists" to the very top of the parent function, <em>above</em> <code><strong>var</strong></code>s</li> </ul> </li> <li><strong>statement:</strong> as a statement in a child block <ul> <li>bound in the child block only</li> <li>does not hoist</li> </ul> </li> <li><strong>expressed: </strong>inside an expression<br> <ul> <li>bound in the expression only</li> </ul> </li>
</ul><h2>Hoisting</h2>
<p>Hoisting is perhaps the most surprising behavior and prone to the most hiccups. The general thing to remember is:</p>
<div class="note">Every definition of a variable is really a <strong>declaration</strong> of the variable at the <em>top of its scope</em> and an <strong>assignment</strong> at the <em>place where the definition is.</em></div>
<p>This figures into computation of upvars and shadowing as well.</p>
<p>Hoisting also cannot "cross paths", as a consequence of the coexistence of <code><strong>var</strong></code>s and <code><strong>let</strong></code>s. Doing so results in error.</p>
<ul> <li><code><strong>let</strong></code>s cannot hoist above <code><strong>var</strong></code>s of the same name</li>
</ul>
<pre class="brush: js">function f() {
  {
    var x;
    let x; // error, hoisting crosses var x
  }
}
</pre>
<ul> <li><code><strong>var</strong></code>s cannot hoist above <code><strong>let</strong></code>s of the same name</li>
</ul>
<pre class="brush: js">function f() {
  {
    let x;
    {
      var x; // error, hoisting crosses let x
    }
  }
}
</pre>
<p>Due to <code><strong>let</strong></code>s being <code><strong>var</strong></code>s at the function top-level, however, the following is okay.</p>
<pre class="brush: js">function f() {
  let x;
  {
    var x; // okay, actually redeclaring a var, so acts as a no-op
  }
}
</pre>
<h2>Parameters</h2>
<ul> <li>Multiple function parameters may share the same name. The last one is bound.</li>
</ul>
<pre class="brush: js">function f(x, x) {
  print(x);
}
f("foo", "bar"); // "bar"
</pre>
<ul> <li>Parameter names may shadow the function name itself inside the scope of the function.</li>
</ul>
<pre class="brush: js">function f(f) {
   print(f);
}
f("foo"); // "foo"
</pre>
<ul> <li><code><strong>var</strong></code>s, however, do not shadow parameter names. And since function top-level <code><strong>let</strong></code>s are <code><strong>var</strong></code>s, <code><strong>let</strong></code>s don't either! The following prints "foo" because the declaration acts as a no-op as there is already a parameter named <code>x</code>.</li>
</ul>
<pre class="brush: js">function f(x, y) {
  var x;
  arguments[0] = "foo";
  print(x); // "foo"
}
</pre>
<h2>with captures assignments, but not var declarations</h2>
<p>Recall the hoisting rule above. Since <code><strong>with</strong></code> 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 <code><strong>var</strong></code>s inside a <code><strong>with</strong></code> might not do what you think. The following two examples are equivalent.</p>
<pre class="brush: js">function f() {
  var o = {x: "foo"};
  with (o) {
    var x = "bar";
  }
  print(o.x); // "bar"
}
</pre>
<pre class="brush: js">function f() {
  var x;
  var o = {x: "foo"};
  with (o) {
    x = "bar";
  }
  print(o.x); // "bar"
} 
</pre>
<p>Note that <code><strong>let</strong></code>s still behave unsurprisingly as their declarations do not hoist outside of the <code><strong>with</strong></code>. They shadow properties of the same name.</p>
<h2>for heads</h2>
<ul> <li><code><strong>var</strong></code>s in <code><strong>for</strong></code> heads hoist to the top of the function. The following two examples are equivalent.</li>
</ul>
<pre class="brush: js">function f() {
  for (var i = 0; i &lt; c; i++) {
    ...
  }
}
</pre>
<pre class="brush: js">function f() {
  var i;
  for (i = 0; i &lt; c; i++) {
    ...
  }
}
</pre>
<p>So it is <em>not</em> safe to nest <strong>var</strong>s of the same name in <code><strong>for</strong></code> heads, even if it is your intention to shadow the variable from the outer loop.</p>
<ul> <li><strong><code>let</code></strong>s in <code><strong>for</strong></code> heads create an implicit block around the condition, update, and body parts of the for loop. The following two examples are equivalent.</li>
</ul>
<pre class="brush: js">function f() {
  for (let i = 0; i &lt; c; i++) {
    ...
  }
}
</pre>
<pre class="brush: js">function f() {
  {
    let i;
    for (i = 0; i &lt; c; i++) {
      ...
    }
  }
}
</pre>
<p>There is no new <code><strong>let</strong></code> every iteration. There is one <code><strong>let</strong></code> around the entire loop. This behavior might change in the future: <a class=" link-https" href="https://bugzilla.mozilla.org/show_bug.cgi?id=449811" rel="freelink">https://bugzilla.mozilla.org/show_bug.cgi?id=449811</a></p>
<h2>catch variables are block-scoped</h2>
<p>Variables that are caught in catch blocks are block-scoped, like <code><strong>let</strong></code>s.</p>
<pre class="brush: js">function f() {
  try {
    throw "foo";
  } catch (e) {
  }
  // e undefined here
}
</pre>
<h2>let statements and expressions</h2>
<ul> <li><strong><code>let</code></strong> statements creates bindings in the accompanying block.</li>
</ul>
<pre class="brush: js">function f() {
  let (x) {
    x = "foo";
    print(x); // "foo"
  }
  // x is undefined here
  let (x = "bar") {
    print(x); // "bar"
  }
  // x is undefined here
}
</pre>
<ul> <li><code><strong>let</strong></code> expressions creates binding in the accompanying expression.</li>
</ul>
<pre class="brush: js">function f() {
  (1 + (let (i = 1) i)); // 2
  ((let (i = 1) i) + i); // error, second use of i is unbound
}
</pre>
<h2>function oddities</h2>
<ul> <li><code><strong>function</strong></code>s do not hoist when declared inside a child block.</li>
</ul>
<pre class="brush: js">function f() {
  {
    g(); // error, g undefined
    function g() {
      ...
    }
  }
}
</pre>
<ul> <li>"dynamic scope", where the scope of the parent function in which an inner function is defined can be mutated at run-time.</li>
</ul>
<pre class="brush: js">function g() {
  print("global");
}
function f(cond) {
  if (cond) {
    function g() {
      print("inner");
   }
  }
  g(); // "inner" when cond, "global" when !cond
}</pre>
<ul> <li>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.</li>
</ul>
<pre class="brush: js">function f() {
  (function g() { print("g"); })();
  g(); // error, g undefined
}</pre>
<ul> <li>Functions initializations happen at the top of the parent function (above <code><strong>var</strong></code>s). Since <strong><code>var</code></strong>s declarations with names already existent as a parameter or a function are no-ops, we get some surprising results.</li>
</ul>
<pre class="brush: js">function f() {
  function g() {
    print("foo");
  }
  var g;
  g(); // "foo"
}
</pre>
<pre class="brush: js">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
}
</pre>
<ul> <li>Functions are <em>not</em> hoisted at all if they're inside a block, but they can still mutate existing scope.</li>
</ul>
<pre class="brush: js">function f() {
  var g = 0;
  if (cond) {
    function g() {
      print("foo");
    }
  }
  g(); // prints "foo" when cond, error when !cond
}
</pre>
Revert to this revision