JavaScript Modules

Here is a trivial web page that loads and calls some JavaScript code to print console messages.

    <!DOCTYPE HTML>
      <html>
        <head>
          <title>Console test</title>
        </head>
        <body>
          <p>Check the console for messages</p>
          <script src="./utils.js"></script>
          <script>
            display('this is a test message');
          </script>
        </body>
      </html>
    const display = (text) => {
        console.log(text);
      };

    This uses two script elements to add JavaScript code to a browser page.

    <script src="script-url"></script>
    <script>
      ...your code...
    </script>

    The first form loads external code using the src attribute. The second form loads embedded code directly in the web page.

    Unfortunately, this approach leads to name conflicts. As more script libraries are included, there is more chance that some libraries will try to use the same name for different functions. The better approach is to use modules. Here is the same web site with modules.

      <!DOCTYPE HTML>
      <html>
        <head>
          <title>Console test</title>
        </head>
        <body>
          <p>Check the console for messages</p>
          <script type="module"
            import { display } from './utils.js';
            display('this is a test message');
          </script>
        </body>
      </html>
      export const display = (text) => {
        console.log(text);
      };

      The changes are small but critical:

      • An export has been added to the definition of display in utils.js.
      • The script element that loaded utils.js has been deleted.
      • The attribute type="module" has been added to the script element with the embedded code
      • An import statement to import display has been added to the start of the embedded code.

      This is JavaScript's modern module system. A module is a file of JavaScript code or a script element with embedded code. By default, all of the top-level variable and function names declared in a module are private.

      Two things need to happen to make names defined in one module available to another.

      • The module that defines the names needs to export them.
      • The module that uses the names must import them.

      There are many ways to export a name from a module, but the simplest is to use the keyword export when declaring the name, as shown above.

      Only those functions needed by other code should be exported. This reduces the chances of accidental name conflicts with other code.

      A script module that wants to use some named code will import it at the start of the module. An import statement has the form

      import { list-of-names } from 'script-url';

      The list of names should be a comma-separated. The URL for the code can be a local file, as in the example, or a complete web address for code on some central JavaScript repository. The external code must export the names listed. An error will occur when trying to import a name not exported by the module specified.

      For more about import see this page.

      We must use the type="module" attribute to load external modules with a script element. Otherwise, we will get an error message about trying to use import and export. For example, if we moved the embedded code in our example above into the file main.js, the code would look like this:

        <!DOCTYPE HTML>
        <html>
          <head>
            <title>Console test</title>
          </head>
          <body>
            <p>Check the console for messages</p>
            <script src="./main.js" type="module"></script>
          </body>
        </html>
        import { display } from './utils.js';
        
        display('this is a test message');
        export const display = (text) => {
          console.log(text);
        };

        Callbacks and closures

        Functions in JavaScript are "first-class objects". That means that a function is a value, just like a number or a string. A function can be assigned to a variable or passed as an argument to another function. Many built-in methods take functions as arguments, including find, sort, map, filter, and reduce. When functions are passed to other functions, they are often called callbacks, because they provide a way for the function receiving the callback to use ("call back") code provide by the calling function.

        A function expression embedded inside another function can use the local variables of the surrounding function. For example, the following function multiplies every element of an array of numbers by a given number:

        const scale = (lst, n) => lst.map(x => x * n);
        scale([5, 7, 9, 3], 10)

        The function expression x => x * n is called a closure, because it refers to variables defined outside the function expression itself. The concept of closures was a major advance in programming languages. By passing this closure to map, map was able to run code using the variable n defined in scale.

        It is not particularly hard to define functions that take functions as arguments. Let's define sum to take list and a function. sum should call the function on every element of the list and return the sum of the results. For example, to sum the squares of a list of numbers

        sum([5, 7, 9, 3], x => x * x)

        Since sum takes a list and returns a single value, it makes sense to use reduce to define sum with an initial value of zero.

        const sum = (lst, fn) => (
          lst.reduce((n, x) => n + fn(x), 0)
        );

        This works fine, but looks a bit odd when mixed with pipeline style code. For example, if you want to first filter negative numbers out of the list, you'd end up with code like this:

        sum([5, -7, 9, -3].filter(x => x > 0), x => x * x)

        An alternative is to define sum to just return a closure for reduce:

        const sum = (fn) => (n, x) => n + fn(x);

        Then you could sum the squares of the positive numbers like this:

        [5, -7, 9, -3].filter(x => x > 0).reduce(sum(x => x * x), 0)

        Asynchronous code

        Usually statements in a JavaScript function block are executed synchronously. That means that JavaScript waits for one statement to finish before executing the next one. This works fine with code that does calculations and printing and so on. It does not work well with code that does things like downloading data from the network. Such actions can take seconds or more to finish. If JavaScript waited for such an action to finish, your browser would stop responding, while it waits. If the action never finishes, e.g., the network is down, the browser could be frozen forever.

        For this reason, there's a special syntax for calling such functions that doesn't stop and wait. We'll illustrate using fetch, a built-in JavaScript function for getting data from a URL. Open your browser JavaScript console window, and try the following. You can copy and paste or just double-click the code:

        const promise = fetch('https://jsonplaceholder.typicode.com/todos/1')

        JSON Placeholder is a website that serves up sample JSON for free! The JSON represents items in a to-do list.

        In JavaScript, fetch is an asynchronous function. That means it immediately returns a promise object, with no waiting. After executing the above example, the variable promise has a promise in it. It's called a promise because its a "promise of a value". If and when fetch gets a response from the network, it creates Response object and stores that response in the promise object. When this happens, we say the promise is resolved. But how do we get that response?

        The secret is to define our own asynchronous function, by putting the keyword async in front of the function expression, like this

        const showFirstToDo = async () => {
          ...
        };

        Inside an async function, we can use the keyword await in front of any expression that returns a promise, like this

        const showFirstToDo = async () => {
          const response = await fetch('https://jsonplaceholder.typicode.com/todos/1')
          ...
        };

        When the operator await gets the promise object, it exits the function showFirstToDo immediately, saving the promise object somewhere. That way, the browser is not waiting for our function to finish. The browser can respond to user inputs.

        If and when the promise is resolved, JavaScript restarts execution of showFirstToDo where it left off. await returns the data that was placed in the promise. In the code above, when JavaScript restarts showFirstToDo, await will return the response that fetch received from the network. The code stores that in the variable response

        Many things can be fetched -- text, JSON, images. To get the actual JSON data, we have to call the response method json. This method also happens to be asynchronous and return a promise, so we need another await. Then we can print the data on the console, like this

        const showFirstToDo = async () => {
          const response = await fetch('https://jsonplaceholder.typicode.com/todos/1')
          const json = await response.json();
          console.log(json);
        };
        showFirstToDo()

        Async functions return promises

        In order for async functions to return immedately and restart later, they have to return promises just like built-in asynchronous functions. There's no way around it. A common mistake is to think that async functions can return values like normal functions. Here's an example of what happens.

        const getFirstToDo = async () => {
          const response = await fetch('https://jsonplaceholder.typicode.com/todos/1')
          const json = await response.json();
          return json;
        };
        getFirstToDo()

        Notice what happened. getFirstDo did not return a JSON object. It returned a promise. The only way to get the value returned by getFirstDo is to call it inside an async function using await, as we did with fetch above. It is not an error to return a value. We often want to do that. We just need to realize that we must process that value inside an asynchronous function.

        function, new, and this

        There are three keywords in core JavaScript that we have barely talked about: function, this, and new. They have been a part of JavaScript since the beginning, inherited from previous object-oriented programming languages. You'll see them in many online examples of older JavaScript and React code. We'll explain them briefly here, but note that there is rarely any need to use them in modern JavaScript.

        function

        function is the original way to define functions in JavaScript. Instead of using arrow syntax like this

        const triangleArea = (a, b, c) => {
          const s = (a + b + c) / 2;
          return Math.sqrt(s * (s - a) * (s - b) * (s - c));
        };

        you can write this

        const triangleArea = function(a, b, c) {
          const s = (a + b + c) / 2;
          return Math.sqrt(s * (s - a) * (s - b) * (s - c));
        };

        or this

        function triangleArea(a, b, c) {
          const s = (a + b + c) / 2;
          return Math.sqrt(s * (s - a) * (s - b) * (s - c));
        };

        All of these forms define a triangleArea that takes the same arguments and returns the same value. For a list of the differences between arrow functions and function functions, see this page. You don't normally need the extra features function adds, and sometimes those extra features get in the way.

        The new operator

        Many uses of function in classic JavaScript can be replaced with arrow syntax. One place where you can't is this:

        function Animal(sound, repeats) {
          this.sound = `${sound}! `;
          this.repeats = repeats;
          this.speak = function() { return this.sound.repeat(this.repeats)};
        }

        This function looks odd in several ways. Why is the name capitalized? Where did the variable this get defined? Why is nothing returned?

        The reason this function looks this way is that is designed not to be called directly, but used as a constructor with the new operator, like this:

        const dog = new Animal('Bark', 2);

        This assigns dog to an instance of Animal. dog is an object with three properties: sound, repeats, and speak. These properties can be retrieved and changed. Try the following:

        dog.sound
        dog.speak()
        dog.repeats = 4;
        dog.speak()

        The interesting property is speak(). It's value is a function, so speak is called a method of Animal. When you call dog.speak(), that variable this is magically set to the dog object. Changing sound or repeat changes what speak() returns.

        Every call to new returns a new separate instance of Animal.

        const cat = new Animal('Meow', 3);
        cat.speak()
        dog.speak()

        How new works: bind

        This section is just a rough outline of how new works for those curious about the internals of JavaScript. We will define a function make so that

        make(Animal, 'Bark', 2);

        does basically the same thing as

        new Animal('Bark', 2);

        The difference in how make is called is because we can only define functions, not operators, in JavaScript.

        The secret to defining make is the JavaScript function method bind. Given a function fn and some value,

        fn.bind(value)

        returns a new function that does what fn does, but with this temporarily set to value.

        Using bind we can define a function make to do roughly what new does:

        const make = (fn, ...args) => {
          const obj = {};
          // call the constructor to store data in obj
          fn.bind(obj)(...args);
          // bind every function stored in obj so that this points to obj
          Object.keys(obj).forEach(key => {
            if (typeof obj[key] === 'function') {
              obj[key] = obj[key].bind(obj)
            }
          })
          // return the new instance
          return obj;  
        }
        const dog = make(Animal, 'Bark', 2);
        dog.speak()
        dog.repeats = 4;
        dog.speak()
        const cat = make(Animal, 'Meow', 3);
        cat.speak()

        There is far more to JavaScript's use of functions for object-oriented programming than can be covered in an intermediate tutorial. For more, see Details of the Object Model.

        In the programming world, there are those who emphasize functional programming and those who emphasize object-oriented programming. JavaScript supports both, as do most languages. We emphasize the former because it is a simpler model of computation. Both are worth learning.

        Node

        Node is a program that runs JavaScript applications all by themselves, not in a browser. We won't be writing Node programs ourselves, but vite uses Node to create and run React applications. That's why you need to install Node first.

        Here's a simple example of a Node program:

          const today = new Date();
          console.log(`Welcome! Today is ${today.toDateString()}.`);

          It looks just like JavaScript we could write in the browser. The difference is that how we run it and where output goes.

          Create a file welcome.js with the above code somewhere. Then run the following command in the terminal window, in the directory where you put welcome.js.

          node welcome.js

          The welcome message should appear in the shell with the current date.

          npm and npx

          Most JavaScript programs, whether in the browser or in Node applications, use libraries written by other programmers. Those libraries in turn use libraries. A new React app created with vite will have 100s of libraries installed!

          To manage libraries, Node includes a command-line tool called npm.

          Most programmers assume "npm" is short for "Node Package Manager", but the top of the npm home page shows something different. Is it official? Click on it and see!

          Over and over, you will see instructions telling you to install Node packages with npm, like this:

          npm install package-list

          This command must be run in the root directory of a Node application. For a React web app, the root directory is the one that contains the subdirectories src and public and, most critically, the file package.json.

          npm install installs the library packages specified, as well as any libraries those libraries need. Such libraries are called dependencies.

          You may see instructions that add --save when doing an npm install. This is no longer necessary.

          There's another form of npm. This is used to install code that is only needed to build or test our code, but not needed to run it. It is the same as the above with an additional flag at the end.

          npm install package-list --save-dev

          Libraries installed this way are development dependencies. They are not included when an application is assembled for deployment to a web server.

          Be careful when copying npm install commands to include the --save-dev if it's specified.

          Sometimes, instructions will say to install packages globally. This is for installing tools that you will use many times in different projects. We do this by add -g to the npm command, like this:

          npm install -g package-list

          For example, npm itself is installed globally.

          For tools that are used once in a while, it is better to use npx than to install a package globally. npx comes with npm. It is a way to run tools like vite without first installing them globally.

          npx vite name-of-app

          This will download and run the latest version of vite, if one is not installed locally. While downloading makes each call to vite/strong> slower, it means the most current version is run.

          To update the packages that have been installed in a project,

          npm update

          There are some complicated rules for updating packages. Packages have semantic version numbers. For example, the version number "3.1.5" means major version 3, minor version 1, patch version 5. When bugs are fixed in a package, the patch version is incremented. When features are added to a package, the minor version is incremented. When breaking changes are made, i.e., some features are removed or now behave differently, the major version is incremented.

          npm update will typically get the most recent minor and patch version, but will not update to a new major version. See this page for information on how update rules can be specified in the package.json dependencies list.

          To force an update to a new major version, do this

          npm update package-name@version-number

          Always test code thoroughly after this kind of update, to make sure nothing has been broken.

          Fixing npm when it breaks

          Sadly, issues arise fairly often with npm when the chains of dependencies between libraries get confused somehow. One way to try when this happens is to clear out everything that has been installed and tell npm to start over. Here are the steps to do this:

          • Delete the entire node_modules subdirectory.
          • Delete the file package-lock.json -- note, not package.json!
          • Run the terminal command
            npm cache clean --force
          • Run the terminal command
            npm install

          If you want to know what these steps do, here's how npm install package-list works:

          • It adds the items in package-list to the list of dependencies in the JSON object in package.json.
          • It goes through that list of dependencies and downloads any packages not already installed. Installed packages are stored in the subdirectory node_modules.
          • It updates the file package-lock.json to record exactly which version of which package has been downloaded.

          Every Node project will have a node_modules subdirectory with thousands of files in it. node_modules should never be stored in a remote repository like Github. That's a waste of space and network bandwidth. When vite is used with standard React templates, it automatically creates the file .gitignore -- note the name starts with a period -- that says "don't track node_modules". Anyone who downloads a Node app just needs to run npm install with no arguments to download all the dependencies listed in package-lock.json.

          Yarn

          yarn is an alternative package manager for Node. Because yarn and React are both from Facebook, many React tutorials use yarn in their examples. This cheat sheet shows how to translate yarn commands into npm commands.

          We use npm in our tutorials because it comes with Node. You are free to use yarn, but do not use both tools. They have different ways of tracking what's been installed. Mysterious bugs can arise if you mix yarn and npm.

          If you accidentally used yarn, but intend to use npm, look for and delete the file yarn.lock.

          © 2024 Chris Riesbeck
          Template design by Andreas Viklund