ES6模块
1. ES6模块是什么?
ES6在语言层级上出现了“模块”的概念。
javascript中一个文件就是一个模块,如果模块中使用了ES6的语法import或者export,
这个文件就是一个ES6模块。
另外,其实在html文件还可以通过设置script脚本的类型type="module"。这个脚本也是ES6模块。
2. ES6模块的特性
ES6模块区别于一般文件模块的特性如下:
1. 默认使用严格模式
ES6模块默认使用"use strict"。代码按照严格模式运行。
所以模块中,顶级this是undefined,不允许是window。
2. 模块级作用域
普通的模块一般通过IIFE(立即执行函数表达式)来生成模块作用域。
ES6模块默认模块文件的代码处于“模块作用域”,模块内的内容外部无法访问。
外部代码只能通过import访问模块export出的内容。
需要注意的是,如果在html文件中通过type="module"的script标签外部引入js,文件之间彼此也不能访问。
// html--按照es6Module引入的文件遵循ES6Module的标准 <script src="1.js" type="module"></script> <script src="2.js" type="module"></script> // 1.js let user = 'lyra'; // 2.js alert(user); // 报错,访问不到
3. 模块代码仅在第一次导入时解析执行
即不论模块代码被import多少次,都只在第一次被import的时候解析执行;
import语句是单例模式。
然后将第一次的导出结果供给所有需要导入的文件,即所有引用文件共享一个输出结果。
所以如果导出的是对象,如果一个文件修改了对象的属性,则所有文件的引入结果都会改变。
// obj.js文件export一个对象 export var obj={ name: 'nobody' } // 1.js引入--假设这个文件先执行 import {obj} from ./'obj.js'; console.log(obj.name); // "nobody" obj.name = 'lyra'; // 2.js引入 import {obj} from './obj.js'; console.log(obj.name); // "lyra"--obj的属性被修改,证明使用的一个导出结果
4. <script type="module">默认异步加载(defer属性)
普通脚本是同步加载文件,文件加载完成后执行,会产生页面阻塞。如果想异步,可以加上defer,async属性。
其中defer属性是“等页面渲染完再执行”,如果含有多个使用defer属性的标签,按照书写顺序执行。
async是“下载完就执行”,如果含有多个使用async的标签,执行顺序随机,按照下载完成的先后顺序。
ES6模块默认使用defer属性。即不管是下面的哪种方式都是等页面渲染完再执行代码。
也可以使用async属性,会覆盖defer属性。
<script>标签作为模块,有两种方式:
1. 外部脚本导入
如果src是从其他域名获取文件,且设置了type="module",如果跨域需要外部服务器设置 Access-Control-Allow-Origin
// 1. 使用了type="module", 则引入的文件代码只执行一次 <script type="module" src="./outside.js"></script>
2.内联脚本
<script type="module" async> import {outsideObj} from './outside.js' // 等import执行完成就执行后续代码,async不用等渲染完成 ... </script>
3. export命令
1. export命令特征
1. export命令可以输出变量,函数,类。
❗️输出函数和类的时候,后面不要加分号(;), 因为导出的是函数和类的声明。
2. export命令只能位于块级作用域的顶层,不能位于块级或者函数作用域中,否则无法进行静态优化。
2. export的写法
因为ES6模块的模块级作用域,外部代码无法访问内部的变量或者方法等。
export命令本身就是为了给外部提供一个可以访问模块内部变量或者方法的接口。
接口可以动态的获取模块内导出的变量或者方法。
1.逐个导出(不推荐)
export var a = 5; export function test() {} // 没有分号 export class{} // 没有分号
2. 大括号导出(推荐)
var a = 5; function test() {} // 没有分号 class example{} // 没有分号 export { a, test, example}; // ⚠️注意千万不要export m;相当于直接输出一个常量,和模块无关,不是一个外部访问模块内部的接口,不能通过接口从模块内部获取值
// 同理,也不能直接export test;因为模块代码只执行一次,相当于直接输入一个固定的函数,如果函数声明变化,输出的值不会随之变化
3. 默认导出
一个模块中只能使用一次默认导出,相当于导出一个变量名的default的值。
// 1.js var a = 1; export default a; // 相当于导出一个变量名为default的变量,值是a;所以不能写成export default a =1;
// 但是可以写成export default 1;
// 对应的导入写法 import a from './1.js'; // 没有大括号
3. export重命名--as
可以使用as对导出的变量或者函数等进行重命名
var a = 5; function test() {} // 没有分号 class example{} // 没有分号 export { a as aValue, test as testFunction, example as exampleClass };
可以通过as在大括号导出方法中导出default
var a = 5; export { a as default } ; // 相当于export default a;
4. import命令
1. import命令特征
1. import命令会执行所加载的模块
所以写代码时如果没有用到的模块,就不要引入,因为会执行。
// 1.js function test(){console.log('hehe')} test(); export var a = 1; //2.js import {a} from './1.js'; // 会打印,因为import执行模块代码,test执行
3.❗️import是静态执行(编译阶段执行),所以后面不能跟变量或者表达式。
// 报错 import { 'f' + 'oo' } from 'my_module';
4. import除了导入默认的名称可以随机,其他的名称必须和export导出的接口名称一致
5. import 导入的接口是只读属性,特例是接口如果是对象,可以更改属性(不建议)
6. import只能位于模块的顶层,不能位于块级或者函数作用域,否则无法静态分析。
7. import命令可以变量提升
foo();//可以正常执行 import { foo } from 'xxxx'
8.from后面原则上只能跟绝对或者相对路径,但使用webpack解析,可以不是路径形式。
2. import 写法
1. 大括号引入普通接口
import { foo } from 'xxxx'
2.引入默认接口
import anyName from 'xxxx'; // 不用大括号;名字可以是任意名称 // 还可以使用as将默认接口转成其他名称 import { default as foo } from 'xxxx';
3.普通接口和默认接口同时
import defaultName, { x1, x2 } from 'xxx'; // 注意只能默认值在前
4.整体加载*
import * as obj from 'xxx'; // 可以通过obj获取所有的输出接口 obj.default; // 默认输出 obj.xxx; // 其他变量或方法
3.import重命名
1. 普通转
import {x as y} from 'xxxx'
2.默认转
import { default as foo } from 'xxxx';
5. import和export复合写法
其实相当于在一个模块中转发另一个模块的接口。语法如下:
export xxxx from 'xxxx';
意义是可以将多个文件,用一个入口文件管理。
1. 全部转发的写法(*)
其实相当于模块的继承,转发的模块继承了被转发的模块。
// 2.js --- 相当于继承1.js的接口 export * from '1.js';// 注意,不会导出1.js中的默认接口,默认接口被忽略 export {default as foo} from '1.js'; // 转发默认接口 // 两句合在一起才完成输出1.js的接口
2. 非全部转发的写法({})
这种写法需要注意的是: 只是转发,test.js中是无法使用foo的
// test.js
export { foo } from 'xxxx';
3. 转发的同时定义别名
// 普通 export {foo as bar} from 'xxx'; // 默认转别名 export {default as bar } from 'xxx'; // 别名为默认 export {foo as default} from 'xxx';// 相当于输出默认值
4. 三种特殊形式复合写法的提案
export * as someIdentifier from "someModule"; export someIdentifier from "someModule"; export someIdentifier, { namedIdentifier } from "someModule";
6. 应用--跨模块常量或者方法
1.公共模块
实际开发中,项目中经常会有需要多个模块都使用的常量,或者通用的方法,组件等。
一般将常量和方法归类为公共模块,统一放到common文件夹下。
common下设constants文件夹放置常量文件,文件名可以根据类型区分,然后设一个index.js作为统一出口。
common下设utils文件夹放置公共方法文件,文件名可以根据功能区分,然后设一个index.js作为统一出口。
设置统一的出口,可以不考虑具体的文件。
2. 统一出口
// add.js export function() { //add函数 } // sum.js export function() { //求和函数 } //index.js export {add } from './add.js'; export {sum } from './sum.js';
7. 动态导入--import(modulePath)函数
1.import()函数特性
1. import(模块位置)函数可以在任何地方(import和export只能在顶层)调用,返回一个Promise对象,解析结果是模块对象。
语法如下:
import('./' + fileName).then(moduleObj => {// 可以使用参数解构.then({exp1, exp2} => { // 导入成功 }).then(err => { // 导入失败 })
如果想要同时加载多个模块
Promise.all([ import('./module1.js'), import('./module2.js'), import('./module3.js'), ]) .then(([module1, module2, module3]) => { ··· });
2. import()函数是在运行时加载
3.import(path)中的参数可以是变量或者表达式。不同的运行结果加载不同的模块。
2. 使用场合
1. 动态加载
动态加载就是参数是变量或者表达式,可以在运行时根据不同的结果动态加载内容。
import(`./${filename}`).then().then()
2. 按需加载
如在监听函数的回调函数中使用,那么只有当触发监听函数才会执行。
button.addEventListener('click', event=> { import('./').then(module=>{}).then(err=>{})// })
3. 条件加载
if(...) { import('./....').then().then() }else { import('./....').then().then() }
4. 用在async函数中
async function test() { let module = await import('./1.js'); }
8. ES6模块和CommonJS模块
1. 输出的内容形式不同
CommonJS输出的是值的拷贝,ES6输出的是值的引用(接口)。
1. CommonJS如果输出一个原始类型的值,模块内变化不会影响到这个值。
因为值会被缓存。
// lib.js var counter = 3; function incCounter() { counter++; } module.exports = { counter: counter, // 如果想要获取动态的值,可以使用取值函数 incCounter: incCounter, }; // main.js var mod = require('./lib'); console.log(mod.counter); // 3 mod.incCounter(); console.log(mod.counter); // 3,
2. ES6输出的是值的引用,运行时会到模块内实时取值。值不会被缓存。
变量总是绑定其所在的模块。
上面的示例如果用ES6实现,最后会打印出4。
2. 加载时期不同
CommonJS是运行时加载,ES6是编译时加载
3. 顶层this指向不同
CommonJS中顶层this执行当前模块;
ES6中顶层this指向undefined;
4.ES6模块加载CommonJS模块
CommonJS模块的输出形式如下:
var a = 5; function test(){} module.exports = { a:a, test: test }
如果使用import加载CommonJS模块,会将输出解析成
var a = 5; function test(){} export default { a:a, test:test }
注意: import不能使用{}加载CommonJS模块,如 "fs"模块
import {readFile} from 'fs'; //❌ // CommonJS模块运行时加载,import编辑时执行,取不到模块内容
5. CommonJS模块加载ES6模块
不能使用require方法加载,只能使用import()方法
6. 处理循环加载(彼此依赖)的方式不同
1. CommonJS的循环加载只会输出已经执行的部分
require的返回结果其实就是exports的值。
// a.js exports.done = false; var b = require('./b.js'); console.log('在 a.js 之中,b.done = %j', b.done); exports.done = true; console.log('a.js 执行完毕'); // b.js exports.done = false; var a = require('./a.js'); // a.js加载该模块时,只执行到export.done=false,返回{done: false},继续执行 console.log('在 b.js 之中,a.done = %j', a.done); exports.done = true; console.log('b.js 执行完毕'); // main.js var a = require('./a.js'); var b = require('./b.js'); // 运行到这里,因为a.js中已经运行过require(b.js),所以不再执行,直接返回缓存的结果 console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
运行结果如下:
$ node main.js 在 b.js 之中,a.done = false b.js 执行完毕 在 a.js 之中,b.done = true a.js 执行完毕 在 main.js 之中, a.done=true, b.done=true
2. ES6会默认加载的接口的已经存在
// a.mjs import {bar} from './b'; console.log('a.mjs'); console.log(bar); export let foo = 'foo'; // b.mjs import {foo} from './a'; console.log('b.mjs'); console.log(foo); export let bar = 'bar';
运行结果如下:
$ node node --experimental-modules a.mjs // 注意这是个试验性方案 b.mjs ReferenceError: foo is not defined
如果想要有正确的运行结果
// a.js修改如下 import {bar} from './b'; console.log('a.mjs'); console.log(bar); var foo = 'foo'; //变量提升,只提升变量声明,值不会 export {foo}; // b.js保持不变 //运行结果如下: b.mjs undefined a.mjs bar
9. ES6模块用于Node
node中本身有CommonJS,ES6和其不兼容,各自处理各自的加载。
1. 使用.mjs文件名
node要求ES6模块的文件使用.mjs后缀名,只能被import命令加载。
运行该类型文件语法如下:
$ node --experimental-modules my-app.mjs
node中import命令只能加载本地文件(file:协议)。语法如下
import XXXXX
1.如果模块不含路径,去node_modules下寻找模块
import 'foo/bar'; //前面不含路径,./ 或者/
2.如果是路径,根据路径寻找
import './foo/bar.js';
3.如果是路径,并且省略了后缀
import './foo/bar'
1)先按照后缀.mjs->.js->.json->.node寻找,否则继续
2)按照./foo/bar/package.json中的main字段,否则继续
3)按照./foo/bar/index.mjs->index.js->index.json->index.node,否则报错
2. ES6模块内不能使用CommonJS的特有变量
arguments,require,module,exports,__dirname,__filename
10. ES6模块的转码
为了实现兼容,需要将ES6转为ES5,一般都使用babel进行转码。
将ES6转为AMD或者CommonJS模块,还有两种方式:
1. es6-module-transpiler转码器
1)安装
npm install -g es6-module-transpiler
2)转化
compile-modules convert file1.js file2.js
2. SystemJS垫片库
1)引入
<script src="system.js"></script>
2)使用
<script> System.import('app/es6-file').then(function(m) { console.log(new m.q().es6); // hello }); </script>