Scope Cheatsheet

  • Revision slug: JavaScript/Reference/Scope_Cheatsheet
  • Revision title: Scope Cheatsheet
  • Revision id: 105295
  • Created:
  • Creator: syg
  • Is current revision? No
  • Comment 1 words added, 2 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

  • function-scoped
  • hoists to the top of its parent function above vars iff declared at the top of the function (i.e. not inside a child block inside the function)
  • do not hoist at all when declared in a child block

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

function oddities

  • functions do not hoist when declared inside a child block.
function f() {
  {
    g(); // error, g undefined
    function g() {
      ...
    }
  }
}
  • "dynamic scope", where the parent scope 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
}

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
}

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>
<ul> <li><code><strong>function</strong></code>-scoped</li> <li>hoists to the top of its parent function <em>above <code><strong>var</strong></code>s</em> iff declared at the top of the function (i.e. not inside a child block inside the function)</li> <li>do not hoist at all when declared in a child block</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>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 parent scope 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>
<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>
Revert to this revision