Now, let's create a module app.js to import the modules we just created.
Let's have a look at app.js file:
1. import User from './user.js';
2. import * as Roles from './roles.js';
3. import completeTask from './tasks.js';
4. import {completedCount} from './tasks.js';
5.
6. let user = new User('Ted', Roles.USER);
7. completeTask(user);
8. console.log(`Total completed ${completedCount}`);
9. // completedCount++;
10. // Only to show that you can change imported object.
11. // NOT a good practice to do it though.
12. User.prototype.walk = function () {
13. console.log(`${this.name} walks`);
14. };
15. user.walk();
In line 1, we use default import to import the User class from the user.js module. You can use a different name other than User here, for example, import AppUser from './user.js'. default import doesn't have to match the name used in the default export.
In line 2, we use namespace import to import the roles.js module and named it Roles. And as you can see from line 6, we access the named exports of the roles.js module using the dot notation.
In line 3, we use default import to import the completeTask function from the tasks.js module. And in line 4, we use named import to import completedCount from the same module again. Because ES6 modules are singletons, even if we import it twice here, the code of the tasks.js module is only evaluated once. You will see only one Inside tasks module in the output when we run it. You can put default import and named import together. The following is equivalent to the preceding lines 3 and 4:
import completeTask, {completedCount} from './tasks.js';
You can rename a named import in case it collides with other local names in your module. For example, you can rename completedCount to totalCompletedTasks like this:
import {completedCount as totalCompletedTasks} from './tasks.js';
Just like function declarations, imports are hoisted. So, if we put line 1 after line 6 like this, it still works. However, it is not a recommended way to organize your imports. It is better to put all your imports at the top of the module so that you can see the dependencies at a glance:
let user = new User('Ted', Roles.USER);
import User from './user.js';
Continue with the app.js module. In line 7, we invoke the completeTask() function and it increases the completedCount inside the tasks.js module. Since it is exported, you can see the updated value of completedCount in another module, as shown in line 8.
Line 9 is commented out. We were trying to change the completedCount directly, which didn't work. And if you uncomment it, when we run the example later, you will see TypeError, saying that you cannot modify a constant. Wait. completedCount is defined with let inside the tasks.js module; it is not a constant. So what happened here?
Import declarations have two purposes. One, which is obvious, is to tell the JavaScript engine what modules need to be imported. The second is to tell JavaScript what names those exports of other modules should be. And JavaScript will create constants with those names, meaning you cannot reassign them.
However, it doesn't mean that you cannot change things that are imported. As you can see from lines 12 to 15, we add the walk() method to the User class prototype. And you can see from the output, which will be shown later, that the user object created in line 6 has the walk() method right away.
Now, let's load the app.js module in an HTML page and run it inside Chrome.
Here is the index.html file:
1. <!DOCTYPE html>
2. <html>
3. <body>
4. <script type="module" src="./app.js"></script>
5. <script>console.log('A embedded script');</script>
6. </body>
7. </html>
In line 4, we load app.js as a module into the browser with <script type="module">, which is specified in HTML and has the defer attribute by default, meaning the browser will execute the module after it finishes parsing the DOM. You will see in the output that line 5, which is script code, will be executed before the code inside app.js.
Here are all the files in this example:
/app.js
/index.html
/roles.js
/tasks.js
/user.js
You need to run it from an HTTP server such as NGINX. Opening index.html directly as a file in Chrome won't work because of the CORS (short for Cross-Origin Resource Sharing) policy, which we will talk about in another chapter.
If you need to spin up a simple HTTP server real quick, you can use http-server, which requires zero configuration and can be started with a single command. First of all, you need to have Node.js installed and then run npm install http-server -g. Once the installation completes, switch to the folder that contains the example code, run http-server -p 3000, and then open http://localhost:3000 in Chrome.
You will need to go to Chrome's Developer Tools to see the output, which will be similar to the following:
A embedded script
Inside tasks module
Ted completed a task
Total completed 1
Ted walks
As you can see from the output, the browser defers the execution of the module's code, while the script code is executed immediately, and the tasks.js module is only evaluated once.
Starting from ES6, there are two types in JavaScript—scripts and modules. Unlike scripts code, where you need to put 'use strict'; at the top of a file to render the code in strict mode, modules code is automatically in strict mode. And top-level variables of a module are local to that module unless you use export to make them available to the outside. And, at the top level of a module, this refers to undefined. In browsers, you can still access a window object inside a module.