Encapsulating Code with Modules
JavaScript loads code using a "share everything" approach, which is one of the most error-prone and confusing aspects of the language. Other languages use concepts like packages to define code scope. Before ES6, everything defined in every JS file of an application shared a global scope. As web applications became more complex and the use of JS code grew, this practice led to problems such as naming conflicts and security issues. One of ES6's goals was to solve the scope problem and to make JS applications more organized, thus introducing modules.
What are Modules
Modules are JavaScript code that automatically runs in strict mode and cannot exit it. In contrast to the "share everything" architecture, variables created at the top level of a module are not automatically added to the global shared scope; they only exist within the module's top-level scope. Modules must also export elements that external code can access, such as variables or functions. Modules can also import bindings from other modules.
Two other module characteristics are less related to scope but are also important. First, at the top level of a module, the value of this is undefined; second, modules do not support HTML-style code comments, a remnant of JavaScript's early browser days.
Scripts, which are any JavaScript code that is not a module, lack these characteristics. The differences between modules and other JavaScript code might seem subtle at first glance (as if they are the same). However, they represent a significant change in how JavaScript code is loaded and evaluated. The true magic of modules lies in exporting and importing only the bindings you need, rather than putting everything into one file. Only by thoroughly understanding exports and imports can one grasp the distinction between modules and scripts.
Basic Export Syntax
You can use the export keyword to expose a portion of published code to other modules. In the simplest use case, you can place export before any variable, function, or class declaration to export them from the module. For example:
// Exporting data
export var color = "red"
export let name = "Nicholas";
export const magicNumber = 7;
// Exporting functions
export funciton sum(num1,num2){
return num1+num2
}
// Exporting classes
export class Rectangle {
constructor(length,width) {
this.length = length;
this.width = width
}
}
// Define a function
function multiply(num1,num2){
return num1 * num2;
}
// ...then export it
export multiply
Note that, apart from the
exportkeyword, each declaration is identical to those in scripts. Because exported function and class declarations require a name, every function or class in the code indeed has one. Unless using thedefaultkeyword, you cannot directly export an anonymous function or class.multiplywas not immediately exported when it was defined; since you don't always have to export declarations, you can export references.
Any variables, functions, or classes not explicitly exported are private to the module and cannot be accessed from outside the module.
Import Syntax
import { identifier1, identifier2 } from './example.js';
The list of imported bindings looks similar to destructuring objects, but it is not. When you import a binding from a module, it's as if it were defined with const. As a result, you cannot define another variable with the same name (including importing another binding with the same name), nor can you use the identifier before the import statement or change the value of the binding.
// Import one
import { sum } from './example.js'
console.log(sum(1,2)) //3
sum = 1 // Throws an error
// Cannot reassign an imported binding
// Import multiple bindings
import { sum, multiply, magicNumber } from './example.js'
console.log(sum(1, magicNumber)); //8
console.log(multiply(1,2)); // 2
// Import the entire module as a single object. All exports can then be used as properties of the object.
import * as exapmle from './example.js'
console.log(exapmle.sum(1, magicNumber)); //8
console.log(exapmle.multiply(1,2)); // 2
import * as example from './example.js' This import format is called a namespace import. Because the example object does not exist in example.js, it is created as a namespace object for all exported members of example.js. However, please note that no matter how many times a module is written in import statements, the module will only be executed once. After the imported module's code is executed, the instantiated module is stored in memory and can be reused whenever another import statement references it.
import { smu } from './example.js';
import { multiply } from './example.js';
import { magicNumber } from './example.js';
Although there are three import statements for the module, example.js will only execute once. If other modules in the same application also import bindings from example.js, then those modules will be using the same module as this code.
An important restriction of
importandexportis that they must be used outside of other statements and functions (e.g.,if,else,function).A Subtle Peculiarity of Imported Bindings
ES6 import statements create read-only bindings for variables, functions, and classes, rather than simply referencing the original bindings like normal variables. The identifier can only be modified within the module that exports it; even the module that imports the binding cannot change its value. For example:
// example.js
export var name = "Nicholas";
export funtion setName(newName){
name = newName
}
// Using the module
import { name, setName } from './example.js'
console.log(name); // Nicholas
setName("Greg");
// Will go back to the module that exports setName() to execute, and set name to Greg
// Then the change to name will be reflected (automatically) in the imported name module,
// The imported name is the local name for the exported name identifier.
// The two names are not the same thing.
console.log(name); // Greg
name = "hehe" // Throws an error
Using
asfor Imports and Exports
function sum(num1, num2) {
return num1 + num2;
}
export { sum as add}
// sum is exported as add, so it can only be referenced as add when imported
import { add } from './sample.js';
// Imports can also use as to rename it
import { add as sum } from './sample.js'
// In the current context, there is only sum, no add
Default values. Because exporting and importing default values is a common practice in other module systems like CommonJS (another JS specification outside of browsers), this syntax has been optimized. A module's default value refers to a single variable, function, or class specified by the
defaultkeyword. Only one default export value can be set per module; using thedefaultkeyword multiple times during export is a syntax error.defaultis a keyword and cannot appear in a variable name.
The following are 3 ways to export defaults
// 1
export default funciton(num1, num2) {
return num1 + num2
}
// 2
function sum(num1, num2) {
return num1 + num2
}
export default sum
// 3
function sum(num1, num2) {
return num1 + num2
}
export {sum as default}
Using Default Imports
import sum from './sample.js'
console.log(sum(1,2)) //3
// Importing a mix of default and non-default values
// example.js
export let color = 'red';
export default function(num1, num2) {
return num1 + num2
}
// When used
import sum,{ color } from './sample.js'
// The default export is renamed to sum, and color is imported
Re-exporting Already Imported Values
// 1
import { sum } from './example.js'
export { sum }
// This can also be done with a single statement
export { sum } from './example.js'
// Or export it with a different name
export { sum as add } from './example.js'
Bare Imports (or Importing for Side Effects)
Some modules may not export anything; instead, they might only modify objects in the global scope. Although top-level variables, functions, and classes within a module do not automatically appear in the global scope, this does not mean that modules cannot affect the global scope. Shared definitions of built-in objects (such as Array and Object) can be accessed within modules, and changes made to these objects will be reflected in other modules.
For example, if we add a pushAll method to Array, bare imports are most likely to be used for creating Polyfills and Shims.
// sample.js
// No exports or imports
Array.prototype.pushAll = function(items){
if(!Array.isArray(items)){
throw new TypeError('参数必须是一个数组')
}
return this.push(...items)
}
// Usage
import './example.js' // Nothing imported, just executed
let colors = ["red", "green", "blue"];
let items = []
items.pushAll(colors)
Using Modules in Web Applications
Before ES6, web browsers also had various ways to include JavaScript in web applications. These scripts were loaded in the following ways:
- Loading JavaScript code files by specifying a code loading address via the
srcattribute in a<script>element. - Embedding JavaScript code directly within a
<script>element without asrcattribute. - Loading and executing JavaScript code files via Web Workers or Service Workers.
To fully support module functionality, web browsers had to update these mechanisms. The specific details are summarized as follows:
Using Modules in
<script>
When not loading modules, if the type attribute is missing or contains a JavaScript content type like "text/javascript", it is loaded as a script. The <script> element can execute inline code or load files specified by src. When the type attribute value is type="module", it supports loading modules. In this mode, the browser loads all inline code or code contained in files specified by src as a module rather than a script. The distinction between a module and a script depends on whether type="module" is present.
<!-- Load a JavaScript module file -->
<script type="module" src="module.js"></script>
<!-- Inline import a module -->
<script type="module">
import { sum } from './example.js'
let result = sum(1,2);
</script>
Module Loading Order in Web Browsers
When modules are loaded, the optional defer attribute is mandatory (this is the default, so it doesn't necessarily need to be written explicitly). This is because modules are unique compared to scripts; they can specify other files they depend on via the import keyword, and these files must be loaded into the module for it to execute correctly.
Modules are executed in the order they appear in the HTML file. This means that regardless of whether a module contains inline code or has a src attribute, the first <script type="module"> will always execute before the second, as shown below:
<!-- This executes first -->
<script type="module" src="module1.js"></script>
<!-- This executes second -->
<script type="module">
import { sum } from './example.js'
let result = sum(1,2);
</script>
<!-- This executes last -->
<script type="module" src="module2.js"></script>
Because each module can import from one or more other modules, this complicates matters. Therefore, modules are first parsed to identify all import statements. Then, each import statement triggers a fetch process (from the network or cache), and the current module only executes after all imported resources have been loaded and executed.
All modules explicitly introduced with <script type="module"> and implicitly imported with import are loaded and executed on demand. The execution process is described as follows:
- Download and parse module1.js
- Recursively download and parse resources imported in module1.js
- Parse inline module
- Recursively download and parse resources imported in the inline module
- Download and parse module2.js
- Recursively download and parse resources imported in module2.js
After loading is complete, other operations will only execute after the document has been fully parsed. After document parsing, the following operations occur:
- Recursively execute resources imported in module1.js
- Execute module1.js
- Recursively execute resources imported in the inline module
- Execute inline module
- Recursively execute resources imported in module2.js
- Execute module2.js
Asynchronous Module Loading in Web Browsers
The async attribute in script: when applied to a script, the script file will execute after it has been fully downloaded and parsed. However, the order of async scripts in the document does not affect their execution order; scripts execute immediately after download is complete, without waiting for the containing document to finish parsing. When this attribute is applied to a module, the situation is similar. The only difference is that all imported resources within the module must be downloaded before the module can execute. This largely ensures that a module only executes after all resources required for its execution have been downloaded, but it does not guarantee the timing of the module's execution.
<!-- Cannot guarantee which executes first; whichever finishes downloading its related resources first will execute first -->
<script type="module" src="module1.js" asnyc></script>
<script type="module" src="module2
Loading Modules as Workers
// The second argument is used to specify the type
let worker = new Worker('module.js',{type:'module'})
Browser Module Specifier Resolution
Browsers require module specifiers to have one of the following formats:
- Starting with
/resolves from the root directory. - Starting with
./resolves from the current directory. - Starting with
../resolves from the parent directory. - URL format.
主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://walker-learn.xyz/archives/4339