Mary Rose Cook

A pop quiz purporting to be about scope in JavaScript that is actually a polemic about why modules are a good idea

This is the blog post version of a lightning talk I gave at the Recurse Center last week.

I will ask you some questions. For each question, there will be some JavaScript files loaded into an HTML page. In these JavaScript files, there will be some console.log() invocations. Your job is to predict what happens after each invocation.

For this first question, there is one JavaScript file, a.js, loaded into this HTML page:

<!doctype html>
<html>
  <head>
    <script src="a.js"></script>
  </head>
</html>

One

// a.js
var blah = "blah";
console.log(blah); // Option 1: ReferenceError: blah is not defined
                   // Option 2: print 'blah' to the console

blah will be printed. The variable, blah, is in the same scope as the console.log() invocation.

For this rest of the questions, there will be two JavaScript files, a.js and b.js, loaded into this HTML page:

<!doctype html>
<html>
  <head>
    <script src="a.js"></script>
    <script src="b.js"></script>
  </head>
</html>

Two

// a.js
blah = "blah";

// b.js
console.log(blah); // Option 1: ReferenceError: blah is not defined
                   // Option 2: print 'blah' to the console

blah will be printed. It doesn’t matter that blah was declared in a different file. All files in JavaScript are loaded into a shared scope.

Three

// a.js
var blah = "blah";

// b.js
console.log(blah); // Option 1: ReferenceError: blah is not defined
                   // Option 2: print 'blah' to the console

blah will be printed. It doesn’t matter that the var keyword was used to declare blah. The declaration was in a shared scope, and that scope is global, so blah becomes a global variable.

Four

// a.js
var blahFn = function() {
  return "blah";
};

// b.js
console.log(blahFn()); // Option 1: ReferenceError: blah is not defined
                       // Option 2: print 'blah' to the console

blah will be printed. All the logic shown so far applies to functions.

Five

// a.js
function blahFn() {
  var blah = "blah";
};

// b.js
blahFn();
console.log(blah); // Option 1: ReferenceError: blah is not defined
                   // Option 2: print 'blah' to the console

There will be a reference error. The blah variable is created inside a function, in the function’s own, private scope. It is not available in the global scope.

Six

// a.js
function blahFn() {
  blah = "blah";
};

// b.js
blahFn();
console.log(blah); // Option 1: ReferenceError: blah is not defined
                   // Option 2: print 'blah' to the console

blah will be printed. With the var keyword omitted, the blah variable goes into the global scope.

Seven

// a.js
;(function() {
  function blahFn() {
    return "blah";
  };

  console.log(blahFn()); // Option 1: ReferenceError: blahFn is not defined
                         // Option 2: print 'blah' to the console
})();

a.js is loaded. The anonymous function at the top is invoked, creating our first JavaScript module. The module instantiates blahFn. The console.log() invocation prints blah because the code inside a module has a shared scope.

// b.js
console.log(blahFn()); // Option 1: ReferenceError: blahFn is not defined
                       // Option 2: print 'blah' to the console

When console.log() is invoked in b.js, a ReferenceError is thrown. The variables inside the module in a.js are locked away. The global scope is not polluted.

Eight

// a.js
;(function(exports) {
  var Blah = {};

  Blah.blahFn = function() {
    return "blah";
  };

  exports.Blah = Blah;
})(this);

// b.js
console.log(Blah.blahFn()); // Option 1: ReferenceError: Blah is not defined
                            // Option 2: print 'blah' to the console

When the console.log() is invoked, blah is printed. The module has made itself available as an object in the global scope. It has also made available one of its variables on this object.

To do this, it made an empty object, Blah, and attached blahFn to it as an attribute. The anonymous function enclosing the module was passed this. Because the anonymous function was invoked in the global context, this is the global object, also referenced with window. The module attached Blah to the exports/global/window object, making it available to the console.log() invocation.

Nine

// a.js
(function woo() {
  return function() {
    console.log("woo")
  };
})

// b.js
(function() {
  console.log("boo")
})();

What is printed? woo or boo?

It’s boo. The code in a.js makes a function called woo and does nothing with it.

In questions seven and eight, both modules were preceded by a semicolon. Why is this?

Often, a module is put in its own JavaScript file. Often, JavaScript files that are part of a website are joined together. This is part of a process to reduce download time. If you join the a.js and b.js together and then execute that, woo will be printed. The operation of the boo module was changed by the random code preceding it in the a.js file. Because the random code in a.js had no semicolon at the end, the parentheses around the boo module invoked the woo function. This returned the anonymous function inside the woo function which was then invoked by the parentheses sitting at the end of b.js.

Modules protect themselves from these sorts of nightmares with a leading semicolon.