The history of JavaScript modularity
If you really want to understand something, you have to start at its source. We will learn about JavaScript modularity by looking at the history of JavaScript modularity.
Script tags and closures
In HTML web pages, the browser loads JavaScript scripts with the tag.
<! -- Page embedded script -->
<script type="application/javascript">
// module code
</script>
<! -- External script -->
<script type="application/javascript" src="./myModule.js">
</script>
Copy the code
In the above code, type=”application/ JavaScript “can be omitted because the default language of the browser script is JavaScript.
By default, the browser executes code from the top down. Encountering a tag during execution will stop to execute the script script.
If the script is large, the page will freeze. This is obviously bad, so browsers allow these scripts to load asynchronously.
Let’s do an experiment to understand the loading order of defer and Async.
In the code below, the script tag turns on the defer or async property and the script loads asynchronously. When the browser’s rendering engine encounters this line of code, it starts downloading the external script, but does not wait for it to download and execute, instead executing the following commands directly.
<script>
console.log(1)
</script>
<script defer src="./export.js"></script>
<! -- export.js contains only one output statement: console.log('export') -->
<script async src='./import.js'></script>
<! -- import.js contains only one output statement: console.log('import') -->
<script>
console.log(4)
</script>. There are about 5,000 divs below<div>asdasdasd</div>
Copy the code
The output is as follows:
1
4
import
export
Copy the code
From this we can conclude that the difference between defer and Async is:
Defer does not execute until the entire page has been rendered properly (the DOM structure has been fully generated, and other scripts have been executed).
Once async completes the script download, the browser rendering engine will interrupt the rendering, execute the script, and continue rendering the rest of the DOM after execution.
If there are multiple defer scripts, they are loaded in the order they appear on the page. And multiple asyncs do not guarantee the loading order.
Scripts loaded through script share a global scope window, where variables defined are mounted in the window global variable.
<script defer src="./export.js"></script>
<script async src='./import.js'></script>
Copy the code
// export.js
var a = 1
console.log(window)
/* { a: 1, ... , b: 2, ... } * /
Copy the code
// import
var b = 2
Copy the code
As Web applications become more complex, the downside of this shared global scope approach becomes apparent. Thus IIFE (call function expressions now) was invented.
IIFE is a whole piece of code wrapped in a function that is then executed immediately.
We all know that each function has its own scope, but we use IIFE to create a separate scope where variables defined cannot contaminate variables outside.
There are several ways to write IIFE, the first being the most common.
(function () {
// code todo
var a = 1~}) ()function () {
// code todo
var b = 2} ()void function () {
// code todo
var c = 3} ()Copy the code
Variables a, B, and C defined therein will not be mounted on the window.
2. AMD specification -> RequireJS, AngularJS
RequireJS and AngularJS have finally given the front end a little bit of a modular prototype.
The following method is the core of RequireJS. It takes two parameters, an array representing the required modules to be loaded, and a callback function that is executed when all the modules in the array have been loaded. The loaded modules are passed to the function as arguments to use them inside the function.
require(['moduleA'.'moduleB'.'moduleC'].function (moduleA, moduleB, moduleC){... });Copy the code
Those of you who have used RequireJS and AngularJS have noticed that the API is not intuitive and easy to use.
3. The CommonJS specification
Then node.js came out of nowhere. And brought his modular solution CommonJS, or CJS for short.
Files can be accessed in Node.js, and each file is a separate scope.
We can load other files using the require method and export the contents of the file through module.exports.
const { count } = require('./m.js')
Copy the code
// m.js
module.exports = {
count: 0
}
Copy the code
The CommonJS module has the following features:
-
All code runs in the module scope and does not pollute the global scope.
-
Modules can be loaded multiple times, but only run once on the first load, and then the results of the run are cached, and then loaded and read directly from the cache. To get a module running again, you must be aware of the cache.
-
The order in which modules are loaded, in the order they appear in the code.
The CommonJS specification states that for each module content, the module variable represents the current module. This variable is an object whose exports property (module.exports) is the interface to the outside world. Loading or importing a module loads the module.exports property of that module.
Here’s how to print the entire Module object: You can see that in this module we exported a count variable.
Module {
id: '. '.// The module identifier, usually the module filename with an absolute path
path: 'e:\ lxh-code\\study-exm\ js modular '.exports: { count: 0 }, // Represents the output value of the module
parent: null.// An object representing the module that calls the module
filename: 'e:\ lxh-code\\study-exm\\js modularized \\ export-.js'.// Module name with absolute path
loaded: false.// A Boolean value indicating whether the module has finished loading
children: [ // An array representing other modules used by this module
Module {
id: 'e:\ lxh-code\\study-exm\\js modularized \\import.js'.path: 'e:\ lxh-code\\study-exm\ js modular '.exports: {},
parent: [Circular *1].filename: 'e:\ lxh-code\\study-exm\\js modularized \\import.js'.loaded: true.children: [].paths: [Array]}],paths: [
'e:\ lxh-code\\study-exm\\js module \\node_modules'.'e:\\lxh-code\\study-exm\\node_modules'.'e:\\lxh-code\\node_modules'.'e:\\node_modules']}Copy the code
NodeJS also provides an exports variable pointing to module.exports for convenience. This is equivalent to having a line of command in the header of the module.
const exports = module.exports
Copy the code
The results are exactly the same in the following two ways:
/ / write one
module.exports.count = 0
module.exports.fn = () = > {}
Copy the code
/ / write two
exports.count = 0
exports.fn = () = > {}
Copy the code
There is one problem, however, with exports, you can only add attributes to them and cannot re-assign them. This is easy to understand, because when you assign, you break the link with module.exports.
// This is correct
exports.count = 0
// This is wrong
var count = 0
exports = count
Copy the code
So sometimes it’s hard to tell the difference between module.exports and exports, let’s just use one.
4. ECMAScript Modules(ESM)
Based on previous regrets, javascript has finally launched its own modularity, or ESM, in ES6.
In contrast to runtime loading of CommonJS, es6 modules are designed to be as static as possible so that module dependencies, as well as input and output variables, can be determined at compile time.
ES6 modules automatically adopt strict mode, whether or not you add “Use strict” to the module header.
Strict mode has the following major limitations.
The following code in non – strict mode will not error!
If the following code is in strict mode, the comment is in strict mode error message!
- Variables must be declared before being used
a = 1
ReferenceError: A is not defined
Copy the code
- Function arguments cannot have attributes of the same name; otherwise, an error is reported
function fn (type, type) {
console.log(type)
}
fn(1.2)
// SyntaxError: Duplicate parameter name not allowed in this context
Copy the code
- The with statement cannot be used
const obj = {
a: {
b: {
name: 'aa'.age: 14}}}with (obj.a.b) {
console.log(name)
console.log(age)
}
// SyntaxError: Strict mode code may not include a with statement
Copy the code
-
Cannot assign a value to a read-only attribute, otherwise an error is reported
-
Octal numbers cannot be represented with the prefix 0, otherwise an error is reported
-
You cannot delete attributes that cannot be deleted; otherwise, an error is reported
-
Cannot delete variable delete prop, error will be reported, only delete property delete global[prop]
-
Eval does not introduce variables in its outer scope
-
Eval and arguments cannot be reassigned
-
Arguments do not automatically reflect changes to function arguments
-
Arguments.callee cannot be used
-
Can’t use arguments.caller
-
Disallow this from pointing to a global object
-
You cannot use fn.caller and fn.arguments to get the stack of function calls
-
Added reserved words (such as protected, static, and interface)
The ESM consists of two commands: export and import.
export
For external output,import
Used for internal input.
// export.js
export let count = 1
Copy the code
// import.js
import { count } from './export.js'
console.log(count)
/ / 1
Copy the code
Note that the export command specifies that the external interface must have a one-to-one relationship with the variables inside the module.
There are two ways to write this:
//
export let count = 1
//
let count = 1
export {
count
}
Copy the code
Other writing methods are not linked with export, so they will not be effective.
For example, the following is incorrect:
let count = 1
export count
Copy the code
It is also important that the interface output by export is dynamically bound to the variables in the module, that is, the latest values (or real-time values) in the module can be fetched through this interface.
// export.js
export let count = 1
setTimeout(() = > { count++ }, 500)
Copy the code
// import.js
import { count } from './export.js'
console.log(count)
setTimeout(() = > { console.log(count)}, 500)
/ / 1
// After a delay of 500ms, output 2
Copy the code
Export and import commands can appear anywhere in the module, as long as they appear at the top level. If you are in a block-level scope, an error is reported. This is because when you’re inside a conditional block, you can’t do static optimization, which is the opposite of what ES6 was designed for.
The following code will report an error:
setTimeout(() = > {
export let count = 1
}, 500)
// SyntaxError: Unexpected token 'export'
Copy the code
What if we have duplicate names in our module?
We can rename it using the AS keyword.
// export.js
let api = 'http://www.baidu.com'
export {
api as $__API
}
Copy the code
// import.js
import { $__API as api } from './export.js'
console.log(api)
// http://www.baidu.com
Copy the code
As you can see from all the examples above, the variable name output by export must be the same as the variable name input by import, otherwise the value will not be taken. Of course, you can also rename it using the AS keyword.
In order to avoid the normal flow of the entire input and output, the ESM rules that the import input variable is read-only and cannot be modified. The following will be an error!
// import.js
// import.js
import { $__API as api } from './export.js'
api = ' '
// TypeError: Assignment to constant variable.
Copy the code
But if theapi
It’s an object, you can modify the properties of the object. But it’s hard to get wrong, so it’s not recommended.
Whenever you import a file, you run it:
// export.js
consolel.log('run export. Js)
export let count = 1
Copy the code
// import.js
console.log('运行了 import.js')
import { count } from './export.js'
console.log(count)
Copy the code
Run import.js and the result is as follows:
Run export.js and run import.js 1Copy the code
Several conclusions can be drawn from this example:
-
Whenever you import a file, you run the file
-
The import command is automatically promoted to the top of the module. Causes the imported module to run first. So run export-.js for the first output.
If you just want to execute (run) your imported module, you can do this. It just executes export-js and does not output any values.
import 'export.js'
Copy the code
If the same module is executed repeatedly, it is executed only once:
// export.js
console.log('run export. Js)
Copy the code
// import.js
import './export.js'
import './export.js'
Copy the code
The running results are as follows:
Run the export. JsCopy the code
Due to theimport
It is statically executed, so expressions and variables cannot be used, and syntactic destructions that only get results at run time cannot be used.
The following syntax is incorrect:
let name = 'exe'
import { ` $__${name}` } from './name.js'
let url = '. /.. /name.js'
import url
if(...). {import './a.js'
} else {
import './b.js'
}
Copy the code
In addition to specifying to load an output value, we can use the * keyword to print all of them.
import * as all from './export.js'
console.log(all)
Copy the code
When you use * to output all, you cannot modify all, including attributes.
The following code will report an error.
import * as all from './export.js'
all.name = 'Luoxuehi'
TypeError: Cannot assign to read only property 'name' of object '[object Module]' TypeError: Cannot assign to read only property 'name' of object '[object Module]'
console.log(all)
Copy the code
In the ESM, there is also a default output, Export Default. Each module can have only one default output.
// export.js
export const name = 'xxx'
export const age = 28
export const sex = 'male'
export default {
name,
age,
sex
}
Copy the code
// import.js
import * as all from './export.js'
import info from './export.js'
import info1, { name } from './export.js'
console.log(info)
// {name: 'XXX ', age: 28, sex:' male '}
console.log(all)
// {
// age: 28,
// default: { name: 'xxx, age: 28, sex: '男' },
// name: 'xxx',
// sex: 'male'
// }
Copy the code
We can draw several conclusions from the above code:
-
Default output and normal output can be written together
-
Default inputs and different inputs can also be written together: import info1, {name} from ‘./export.js’
-
The default output is only one per module
-
The default output is actually output with default as the key value
-
The default is to output anonymous variables, meaning you can name them whatever you want when you import them
-
When you import info directly from ‘./export.js’ without curly braces, it is the default output.
Essentially, export default simply prints a variable or method called default, and the system allows you to call it whatever you want.
So the following two ways are the same:
const name = 'xxx'
export {
name as default
}
Copy the code
const name = 'xxx'
export default name
Copy the code
From the second conclusion above, the default output is only one per module, and the following is not allowed:
const name = 'xxx'
export {
name as default
}
// There can only be one default output
export default function () {
console.log('cc')}Copy the code
In addition to the output, the input can also be understood this way, so the following two lines are also consistent:
// import name from './export.js'
import { default as name } from './export.js'
Copy the code
If we need to write at the same time, please change the variable name as follows:
import name1 from './export.js'
import { default as name } from './export.js'
console.log(name)
console.log(name1)
Copy the code
When we write export and import in a module, we are forwarding the name and age in export-.js:
// import.js
export { name, age } from './export.js'
// is equal to the following
import { name, age } from './export.js'
export { name, age }
Copy the code
Look at the code below. At this time, it should be noted that when export and import are written on one line, name and age are not introduced into the current module, but are relative to the two interfaces of external forwarding, so the current module cannot directly use name and age.
// import.js
export { name, age } from './export.js'
ReferenceError: name is not defined
console.log(name)
Copy the code
Here are some other ways to write it:
// The interface was renamed
export { name as username, age } from './export.js'
// Total output
export * from './export.js'
// Specify the default interface of export.js as the default interface output of the current module
export { default } from './export.js'
// Outputs the name variable of export-.js as the default interface for the current module
export { name as default } from './export.js'
// Outputs the default interface of export.js as the info variable of the current module
export { default as info } from './export.js'
Copy the code
Based on the above knowledge, we can write the module and the previous module inheritance as follows:
// parent.js
export const a = 1
export const b = 2
export const c = 3
Copy the code
// chilren.js
// We will inherit all fields in parent.js
export * from './parent.js'
// Start adding attributes unique to chilren.js
export const d = 4
Copy the code
As mentioned earlier, import commands can only be at the top of a module, not in a block of code, and cannot be written using expressions. Because the import command is statically parsed by the JS engine, it is executed before any other statement in the module.
This design helps the compiler to be more efficient. But it also makes it impossible to load modules at run time. Syntactically, conditional loading is impossible. If the import command were to replace the require method in Node, this would not be possible. Because the require method loads modules at runtime, the import command cannot replace the dynamically loaded modules in the require method.
if (type === 'white') {
require('white-style.css')}else {
require('block-style.css')}Copy the code
The above is dynamic loading, which module to load, need to be determined at runtime. The import command does not do this.
The ES2020 proposal introduces the import() method to support dynamic loading.
/ / grammar
import(Module path)Copy the code
The import() method supports whatever arguments the import command supports. The only difference is that the import() method can be loaded dynamically.
import('./export.js').then(module= > {
console.log(module)})/ / {
// age: 28,
// default: {name: 'XXX ', age: 28, sex:' male '},
// name: 'xxx',
// sex: 'male'
// }
Copy the code
The import() method returns a Promise object, so the import() method is loaded asynchronously. The require method in Node is synchronous.
5. Run the ESM in a browser
<script type="module" src="./export.js"></script>
<script type="module" src='./import.js'></script>
Copy the code
We set the type attribute of the tag to module to run the ES6 module in the browser, and we tell the browser that the file is an ES6 module.
with type=”module” is loaded asynchronously by the browser. It does not block the browser, but executes the script after the entire page is rendered. This is equivalent to turning on the defer attribute. If the browser has more than one of type=”module”, they will be executed in the order they appear on the page.
For loading a file of type=”module”, we need to start a service to run the entire HTML, otherwise there will be cross-domain problems.
It is recommended to install a Live Server in VS Code to easily experiment with modularity.
6. Differences between ESM and CommonJS modules
Before we learn how to run ES6 modules in Node, we need to understand the differences between ESM and CommonJS modules.
There are three main differences between ESM and CommonJS modules:
-
- The CommonJS module prints a copy of a value, the ES6 module prints a reference to a value.
-
- The CommonJS module is run time loaded, and the ES6 module is compile time output interface
-
- CommonJS module
require()
The method is to load modules synchronously, while ES6 modulesimport
Commands are loaded asynchronously and have a separate module-dependent parsing phase.
- CommonJS module
The first difference is exemplified by the following code:
You can see that the CommonJS module outputs a copy of the value, meaning that once a value is output, changes within the module do not affect that value.
/ / CommonJS module
/ / output
let count = 0
setTimeout(() = > count++, 1000)
module.exports = {
count
}
/ / input
const m = require('./export.js')
console.log(m.count)
/ / 0
setTimeout(() = > {
console.log(m.count)
/ / 0
}, 1000)
Copy the code
But in ES6 module is different, ES6 module output is the reference of the value, the change of the internal value can affect the external.
// export.js
export let count = 1
setTimeout(() = > { count++ }, 500)
Copy the code
// import.js
import { count } from './export.js'
console.log(count)
setTimeout(() = > { console.log(count)}, 500)
/ / 1
// After a delay of 500ms, output 2
Copy the code
7. HowNode
To run the ES6 module
CommonJS is exclusive to Node.js and is not compatible with ES6 modules. The CommonJS module uses require() and module.exports, while ES6 modules use import and export.
Starting with Node.js v13.2, Support for ES6 modules is enabled by default.
If you want to use ES6 modules in Node.js, there are two ways to do it:
-
Change the file name from the.js suffix to.mjs, and Node.js will parse the module in ES6 mode. Strict mode is turned on by default.
-
If you don’t want to change the suffix, specify the type field as module in the package.json file in your project. Examples are as follows:
// package.json
{
"type": "module"
}
Copy the code
If you want to use the original CommonJS module in Node.js, there are also two ways to do the opposite:
-
Change the file name from.js to.cjs and Node.js will parse the module as CommonJS module.
-
If you don’t want to change the suffix, specify the type field as CommonJS in the package.json file in your project. Examples are as follows:
// package.json
{
"type": "commonjs"
}
Copy the code
The.mjs suffix can only identify ES6 modules, and the.cjs suffix can only identify CommonJS modules.
8. CommonJS module loads ES6 module
Sometimes we write Node.js applications using the default CommonJS module to handle modularity. Then we need to introduce a library that uses ES6 modules.
The CommonJS module require() command cannot load the ES6 module, causing an error. But we can use the import() method to load it.
// export.mjs
export const name = 'xxx'
export const age = 28
export const sex = 'male'
export default {
name,
age,
sex
}
Copy the code
// import.js
import('./export.mjs').then(m= > {
console.log(m)
})
Copy the code
9. ES6 module loads CommonJS module
Sometimes we use ES6 modules to handle modularity when we write Node.js programs. Then we need to introduce a library that uses the CommonJS module.
The import command for ES6 modules can load CommonJS modules, but only as a whole. Cannot be destructively loaded.
// export.cjs
module.exports = {
name: 'xxx'
}
Copy the code
// import.js
import info from './export.cjs'
/ / success
import { name } from './export.cjs'
// SyntaxError: The requested module './ ext. CJS 'does not provide an export named 'name'
console.log(info)
// { name: 'xxx' }
Copy the code
There is another way to load CommonJS modules. Use the ES6 module’s import() method. But if you look at the printed value, you see that it automatically mounts the module content on default.
// import.js
import('./export.cjs').then(m= > {
console.log(m)
// { default: { name: 'xxx' } }
})
Copy the code
References:
Ruan Yifeng es6 tutorial