How Does Webpack Achieve Module Scope?

Early in this series on Webpack, I mentioned that it allows us to write modular JavaScript using ES6 modules. This is a huge gain because until recently, browsers did not support any kind of module system natively. Everything that you included in a <script> tag was executed in the global scope. What wizardy does Webpack use to accomplish this?

When I was teaching myself to code about 10 years ago, the most notable instance of this problem came about with jQuery. JQuery created a global instance of itself in the variable jQuery, and also aliased itself with the $ variable.

// These two lines are equivalent, and
// most people used the second form
jQuery(".some-selector").click(/*...*/)
$(".some-selector").click(/*...*/)

That all works fine unless you're using another library that also uses the dollar sign in the global scope. Now you have a naming collision. You solve this with what's known as an Immediately Invoked Function Expression:

// outer scope is a function that is invoked immediately with the argument jQuery,
// which is named *within* the function as $. Now your dollar sign variable is
// safely scoped within the body of this function.
(function($) {
  $(".some-selector").click(/*...*/)
})(jQuery)

An IIFE (pronounced "Iffy"), is exactly what it sounds like. You create a function expression, and instead of saving that to a named constant, you immediately invoke it by wrapping it in parentheses and appending parens after it with any necessary arguments to be passed to the function. In the example above, we are passing in the global variable jQuery in the last line, which is bound to $ in the first line.

Additionally, I could return a function or object from the IIFE which has access to variables defined inside the IIFE, even if it is invoked outside the IIFE. This is known as closure, and it is how we can create private variables in a language that otherwise doesn't support them:

const counter = (() => {
  // count is defined in the internal scope of the IIFE
  let count = 0
  // We return a function, which is what gets assigned to counter.
  return () => count+=1
})()

// in the outer scope, we can invoke counter, but
// can't access the count variable.
counter() // returns 1
counter() // returns 2
counter() // returns 3
console.log(count) // Error, count is not defined in this scope

This is how Webpack takes the modules you write and bundles them all up into a single file to be executed in the browser. It takes each module and wraps it in an IIFE, using closure to control the outside visibility of functions and variables. It registers the result of each IIFE in its own module system, and has its own set of functions for loading modules within the resulting bundle. These functions and the registry of modules are what is known as the runtime, which I mentioned in my previous email.

What this means practically for you is that the final bundle, even when not minified, is very difficult for a human to read and make sense of. It will bear very little resemblance to the source code that you wrote. So how do you map the transpiled code back to your source code when debugging?

Sourcemaps!

More on that tomorrow.


Did you like this?

I send a daily email with tips and ideas like this one. Join the party!

Icon