Basic Importing
Once you have a module with exports, you can access the functionality in another module by using the import
keyword. The two parts of an import
statement are the identifiers you’re importing and the module from which those identifiers should be imported. This is the statement’s basic form:
import { identifier1, identifier2 } from "./example.js";
The curly braces after import
indicate the bindings to import from a given module. The keyword from
indicates the module from which to import the given binding. The module is specified by a string representing the path to the module (called the module specifier). Browsers use the same path format you might pass to the <script>
element, which means you must include a file extension. Node.js, on the other hand, follows its traditional convention of differentiating between local files and packages based on a filesystem prefix. For example, example
would be a package and ./example.js
would be a local file.
I> The list of bindings to import looks similar to a destructured object, but it isn’t one.
When importing a binding from a module, the binding acts as if it were defined using const
. That means you can’t define another variable with the same name (including importing another binding of the same name), use the identifier before the import
statement, or change its value.
Importing a Single Binding
Suppose that the first example in the “Basic Exporting” section is in a module with the filename example.js
. You can import and use bindings from that module in a number of ways. For instance, you can just import one identifier:
// import just one
import { sum } from "./example.js";
console.log(sum(1, 2)); // 3
sum = 1; // error
Even though example.js
exports more than just that one function this example imports only the sum()
function. If you try to assign a new value to sum
, the result is an error, as you can’t reassign imported bindings.
W> Make sure to include /
, ./
, or ../
at the beginning of the file you’re importing for best compatibility across browsers and Node.js.
Importing Multiple Bindings
If you want to import multiple bindings from the example module, you can explicitly list them out as follows:
// import multiple
import { sum, multiply, magicNumber } from "./example.js";
console.log(sum(1, magicNumber)); // 8
console.log(multiply(1, 2)); // 2
Here, three bindings are imported from the example module: sum
, multiply
, and magicNumber
. They are then used as if they were locally defined.
Importing All of a Module
There’s also a special case that allows you to import the entire module as a single object. All of the exports are then available on that object as properties. For example:
// import everything
import * as example from "./example.js";
console.log(example.sum(1,
example.magicNumber)); // 8
console.log(example.multiply(1, 2)); // 2
In this code, all exported bindings in example.js
are loaded into an object called example
. The named exports (the sum()
function, the multiple()
function, and magicNumber
) are then accessible as properties on example
. This import format is called a namespace import because the example
object doesn’t exist inside of the example.js
file and is instead created to be used as a namespace object for all of the exported members of example.js
.
Keep in mind, however, that no matter how many times you use a module in import
statements, the module will only be executed once. After the code to import the module executes, the instantiated module is kept in memory and reused whenever another import
statement references it. Consider the following:
import { sum } from "./example.js";
import { multiply } from "./example.js";
import { magicNumber } from "./example.js";
Even though there are three import
statements in this module, example.js
will only be executed once. If other modules in the same application were to import bindings from example.js
, those modules would use the same module instance this code uses.
A> ### Module Syntax Limitations A> A> An important limitation of both export
and import
is that they must be used outside other statements and functions. For instance, this code will give a syntax error: A> A> js A> if (flag) { A> export flag; // syntax error A> } A>
A>The export
statement is inside an if
statement, which isn’t allowed. Exports cannot be conditional or done dynamically in any way. One reason module syntax exists is to let the JavaScript engine staticly determine what will be exported. As such, you can only use export
at the top-level of a module. A> A> Similarly, you can’t use import
inside of a statement; you can only use it at the top-level. That means this code also gives a syntax error: A> A> js A> function tryImport() { A> import flag from "./example.js"; // syntax error A> } A>
A> A> You can’t dynamically import bindings for the same reason you can’t dynamically export bindings. The export
and import
keywords are designed to be static so that tools like text editors can easily tell what information is available from a module.
A Subtle Quirk of Imported Bindings
ECMAScript 6’s import
statements create read-only bindings to variables, functions, and classes rather than simply referencing the original bindings like normal variables. Even though the module that imports the binding can’t change its value, the module that exports that identifier can. For example, suppose you want to use this module:
export var name = "Nicholas";
export function setName(newName) {
name = newName;
}
When you import those two bindings, the setName()
function can change the value of name
:
import { name, setName } from "./example.js";
console.log(name); // "Nicholas"
setName("Greg");
console.log(name); // "Greg"
name = "Nicholas"; // error
The call to setName("Greg")
goes back into the module from which setName()
was exported and executes there, setting name
to "Greg"
instead. Note this change is automatically reflected on the imported name
binding. That’s because name
is the local name for the exported name
identifier. The name
used in the code above and the name
used in the module being imported from aren’t the same.