Module
1. 概述
JavaScript一直没有模块体系,无法将一个大程序拆分成相互依赖的小文件,再使用简单的方法拼装起来。
ES6之前,社区制定了一些模块加载方案,主要有CommonJS和AMD两种。CommonJS用于服务器,AMD用于浏览器。
ES6在语言标准层面上实现了模块功能,可以取代CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案。
ES6模块化的设计思想:尽量静态化,使得编译时就能确定模块的依赖关系,以及输入输出的变量。
CommonJS和AMD的设计思想:在运行时确定模块的依赖关系和输入输出的变量。
// CommonJS模块其实就是对象,输入时必须查找对象属性。
let {stat,exists,readfile} = require('fs');
// 相当于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
// 此处代码实质:整体加载fs模块(加载fs的所有方法),生成一个对象(_fs),然后从对象上面读取方法。
// 这种加载称为运行时加载,只有运行时才能的到这个对象,导致完全没办法在编译时做静态优化。
ES6模块不是对象,而是通过export命令显示指定输出的代码,再通过import命令输入。
// ES6模块
import {stat,exists,readFile} from 'fs';
// 此处代码实质:从fs模块加载3个方法,其他方法不加载。这种加载称为编译时加载或者静态加载,即ES6可以在编译时就完成模块加载,效率要比CommonJS模块的加载方式高。但是,这也导致了没法引用ES6模块本身,因为它不是对象。
2.严格模式
ES6的模块自动采用严格模式,不管是否使用‘use strict’关键字。
3.export命令
模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果希望外部能够读取模块内部的变量,就必须使用export关键字输出该变量。
// profile.js
export var firstName = 'Nick';
export var secondName = 'Tony';
export var year = 1999;
// 另一种写法,与前一种是等价的,应该优先考虑这种写法,这样很清晰的看出在脚本末尾输出哪些变量。
var firstName = 'Tony';
var secondName = 'Nick';
var year = 1999;
export {firstName,secondName,year};
export命令除了输出变量,也可以输出函数或类(class)。
// 对外输出multiply函数。
export function multiply(x,y) {
return x*y
}
可以使用as关键字对输出的变量重命名。
function f1(){}
function f2(){}
export {
f1 as f3,
f2 as f4,
f2 as foo
}
// f2就可以使用不同的名字输出两次。
export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
// 报错
export 1;
// 报错
var m =1;
export m;
// 两种写法都报错,因为没有提供对外的接口。两种写法都直接输出1。1只是一个值,不是接口
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};
// function和class的输出也遵守这样的规则。
export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。而CommonJS模块输出的是值的缓存,不存在动态更新。
export var foo ='bar'
setTimeout(() => foo = 'baz',500)
// 代码输出的是变量foo,值为bar,500毫秒后变成baz。
export命令可以出现在模块的任何位置,只要是模块顶层就可以。import命令也是如此。因为处于条件代码块中,就没法做静态优化了(静态优化是什么呢?)
function foo (){
export default 'bar' // SyntaxError
}
foo()
4.import 命令
export命令定义模块对外接口,import命令加载模块。
import 命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,与被导入模块对外接口的名称相同。
可以使用as重命名。
// main.js
import {firstName,secondName,year} from './profile.js';
import {secondName as newName} from './profile.js';
import命令输入的变量都是只读的,因为它的本质是输入接口,故,不允许在加载模块的脚本里改写接口。
import {a} from './profile.js'
a = {} // 报错
但如果a是一个对象的话,改写a的属性是允许的。
但是这样的写法很难查错,建议输入的变量,都当做完全只读,不要轻易改变它的属性。
import {a} from './profile.js'
a.name = 'hello'
console.log(a.name)
from关键字后指定模块文件位置,可以是相对路径,可以是绝对路径。若不带路径,只是一个模块名,那么必须有配置文件,告诉JS引擎该模块的位置。
import命令具有提升效果,会提升到整个模块的头部,首先执行。
console.log(a)
import {a} from './profile.js'
// 代码不会报错,import的执行会早于a。
// 本质:import命令是编译阶段执行的,在代码运行之前。
import是静态执行(静态执行是什么呢?),所以不能使用表达式和变量。(即只有在运行时才能得到结果的语法结构。)
// 报错 使用了表达式
import { 'f' + 'oo' } from 'my_module';
// 报错 使用了变量
let module = 'my_module';
import { foo } from module;
// 报错 使用了if语句
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
import语句会执行所加载的模块。
import 'lodash'; // 仅仅执行lodash模块,但是不输入任何值。
// 多次重复执行同一句import语句,只会执行一次。
import 'lodash';
import 'lodash';
// import语句是Singleton模式。
import {foo} from 'my_module';
import {bar} from 'my_module';
// 等同于
import {foo,bar} from 'my_module';
// foo和bar是在两个语句中加载,但是它们对应的是同一个my_module模块。
5.模块的整体加载
除了指定加载某个输出,也可以使用星号*,指定一个对象,使所有输出值都加载在这个对象上面。(整体加载。)
// circle.js 输出
export function area(radius){
return Math.PI*radius*radius;
}
export function circumference(radius){
return 2*Math.PI*radius;
}
// main.js 引入(逐一指定要加载的方法。)
import {area,circumference} from './circle.js';
// 整体加载的写法
import * as circle from './circle.js';
// 注意:模块整体加载所在的那个对象,是可以静态分析的,所以不允许运行时改变。
circle.foo = {}; // 报错:object is not extensible
circle.area = {}; // 报错:circle.area is read only
6.export default 命令
从前面的代码可以看出,使用import命令的时候,用户需要知道所加载的变量名或者函数名,否则是无法加载的。
为了提供方便,让用户不用了解变量名或者函数名。就可以使用export default 命令,为模块指定默认的输出(也就是没有名字的输出??)。
// default.js
export default function (){
console.log('function');
}
// 模块默认输出的是一个函数。
其他模块加载该模块时,import命令可以为此匿名函数指定任意名字。且这时的import命令后面不需要跟大括号。
// main.js
import aName from './default.js';
aName(); // function
export default命令可以用在非匿名函数前。此时函数的名字在模块外是无效的,加载的时候,与匿名函数一样。
// default.js
export default function foo() {
console.log("object")
}
// 或者写成
function foo(){
console.log("object")
}
export default foo
export default命令用于指定模块的默认输出,一个模块只能有一个默认输出,export default命令只能使用一次。所以,import命令后才不用加大括号,因为只可能唯一对应export default命令。
本质上,export default命令就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。
// module.js
function mul(x, y) {
return x * y
}
export { mul as default }
// 等同于
export default mul
// main.js
import {default as foo} from './profile.js'
// 等同于
import foo from './profile.js'
因为export default命令输出的是一个叫做default的变量,所以它后面不能跟变量声明语句。
// ok
export var a = 'hello'
// ok
var a = 'hello'
export a
// no
export default var a ='hello'
// export default a 的含义是将变量a的值赋给变量default,所以最后一句代码会报错。
也可以直接将一个值写在export default后面。
// ok
export default 222;
// error
export 23;
可以在一条import语句中同时输入默认方法和其他接口。
import def,{each,forEach} from './profile.js'
// 对应的export模块
export default function (){}
export function each (){}
export {each as forEach} // 暴露出forEach接口,默认指向each接口,故forEach和each指向同一个方法。
export default可以用于输出类。
export default class{
constructor(x,y){
this.x = x
this.y = y
}
pri(){
console.log(this.x,this.x)
}
}
7.export和import的复合写法
如果在一个模块之中,先输入后输出同一个模块,import语句可以与export写在一起。
export {foo,bar} from './profile.js'
// 可以理解为
import {foo,bar} from './profile.js'
export {foo,bar}
export和import语句结合在一起,写在一行。但是foo和bar实际上并没有被导入到当前模块中,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo和bar。
模块的接口改名和整体输出,也可以使用这种写法。
// 接口改名
export {foo as myFoo} from './profile.js'
// 整体输出
export * from './profile.js'
// 默认接口的写法
export {default} from './profile.js'
// 具名接口改为默认接口的写法
export {es6 as default} from './profile.js'
// 等同于
import {es6} from './profile.js'
export default es6
// 同样,默认接口也可改为具名接口。
export {default as es6} from './profile.js'
在ES2020之前,有一种import语句,没有对应的复合写法。
import * as someIdentifier from './profile.js'
// 在ES2020之后补上了这个写法。
export * as someIdentifier from './profile.js'
// 即
import * as someIdentifier from './profile.js'
export {someIdentifier}
8.模块的继承
模块之间可以继承。 circleplus模块继承circle模块。
// circleplus.js
export * from './circle.js'
export var e = 2.71828182846
export default function (x){
return Math.exp(x)
}
// export *:输出circle模块的所有属性和方法。此命令忽略circle模块的default方法。
// 之后,输出了e变量和默认方法。
// 可以将circle的属性和方法改名后再输出。
export {area as circleArea} from './circle.js'
// 加载circleplus.js模块的写法。
import * as math from './circleplus.js'
import exp from 'circleplus.js' // 将circleplus模块的默认方法加载为exp方法
console.log(exp(math.e))
9.跨模块常量
const声明的常量只在当前代码块有效,如果想设置跨模块(跨多个文件)的常量,或者说一个值被多个模块共享,采用以下的写法。
// constants.js
export const A = 1
export const B = 3
export const C = 4
// test1.js
import * as constants from './constants.js'
console.log(constants.A)
console.log(constants.B)
console.log(constants.C)
// test2.js
import {A,B} from './constants.js'
console.log(A)
console.log(B)
建立一个专门的constants目录,将各种常量存放在不同的文件里面,保存在这个目录下面。
// constants/db.js
export const db = {
url:'XXX',
admin_username:'admin',
admin_password:'password'
}
// constants/users.js
export const users = ['root','admin','staff','coo','ceo','chief']
// 将这些文件输出的常量合并在index.js里面
export {db} from './db.js'
export {users} from './users.js'
// 使用的时候,直接从index.js中取
import {db,users} from './constants/index.js'
10.import()
简介
import命令会被JS引擎静态解析,先于模块内的其他语句执行。 下面的代码会报错:
// 报错:Unexpected identifier
if(x === 2 ){
import A from './circle.js'
}
// 引擎处理import语句是在编译时,不会去分析或执行if语句,故import语句放在if代码块中毫无意义,会报句法错误,而不是执行时错误。
// 故,import和export语句只能在模块顶层,不能在代码块之中。(例如:if语句,函数)
// 这样的设计,有利于编译器提高效率,但无法导致在运行时加载模块。故,条件加载就无法实现。
// 因此,import命令无法取代require的动态加载功能。(require是运行时加载模块。)
// const path ='./'+fileName
// const myModule = require(path)
ES2020引入了import()函数,支持动态加载模块。
import(specifier)
// specifier:指定要加载的模块的位置。import命令能够接受什么参数,import()函数就能接受什么参数,主要是后者为动态加载。
// import()函数返回一个Promise对象。
// 原例子
const main = document.querySelector('main');
import(`./section-modules/${someVariable}.js`)
.then(module => {
module.loadPageInto(main);
})
.catch(err => {
main.textContent = err.message;
});
// import()函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。是运行时执行。
// import()函数与所加载的模块没有静态连接关系,这点与import语句不同。
// import()函数类似Node的require方法,但前者是异步加载,后者是同步加载。
适用场合
1.按需加载
在需要的时候,再加载某个模块。
button.addEventListener('click', event => {
import('./dialogBox.js')
.then(dialogBox => {
dialogBox.open();
})
.catch(error => {
/* Error handling */
})
});
// import()方法放在click事件监听函数中,点击了按钮之后,才会加载。
2.条件加载
放在if代码块中,根据不同的情况,加载不同的模块。
if(condition){
import('moduleA').then(...);
}else{
import('moduleB').then(...);
}
// 满足条件,加载模块A,否则加载模块B
3.动态的模块路径
import允许模块路径动态生成。
import(f()).then(); // 根据函数f的返回结果,加载不同的模块。
注意点
import()加载模块成功以后,此模块作为一个对象,作为then方法的参数。
使用对象解构赋值的语法,获取输出接口。
import('./myModule.js').then(({export1,export2}) = > {// dosomething})
// 如果对象有default输出接口,可以使用参数直接获得。
import('./myModule.js').then((myDefault) => {// dosomething})
// 可以使用具名输入的形式。
import('./myModule.js').then(({default:theDefault}) => {//dosomething})
可以同时加载多个模块。
Promise.all([import('./module1.js'),import('./module2.js'),import('./module3.js')])
.then(([module1,module2,module3]) => {});
import用在async函数中。
async function main() {
const myModule = await import('./myModule.js');
const {export1, export2} = await import('./myModule.js');
const [module1, module2, module3] =
await Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
]);
}
main();
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!