ES6之模块你知吗?
历史缘由
混乱时期 ---- 社区规范
CommonJs 分为两个派系
* 服务端
modules/1.x
modules/async
* browserify
AMD\UMD 代表有requireJs和seaJs
ES6时期
苦于之前社区混乱无秩序,又鉴于 node 都支持模块加载了,前端还不支持,所以前端也加速了进程。其设计思想是尽量前端静态化,编译时就能确定模块的依赖关系,以及输入和输出的变量。而 CommonJs 和 AMD 模块,却只能在运行时确定这些东西。所以浏览器采用了 export 和 import 方式。而 Node 则是CommonJS 规范的。
ES6时期
import
import 后面的 from 指定模块文件的位置,可以是相对的路径,也可以是绝对的,.js 后缀可以省略。如果只是模块名,不带路径,那么必须要有配置文件,告诉 JavaScript 引擎该模块的位置。 在 ES6 模块中,顶层的 this 指向 undefined,即不应该在顶层代码中使用 this。 import 命令具有提升效果,会提升到整个模块的头部,首先执行。
以下是一些要注意的用法
import { f as F } from './a' // 导入时起别名
import * as All from './a' // 导入模块中的所有成员, 绑定在 All 上
import myFunction from './a' // 不加{}, 导入默认
⚠️:import 加载模块的接口 import 命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块对外接口保持一致。import 命令是只读的,因为它的本质是输入接口,不过如果是个对象,我们则相当于修改了属性。
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
import {a} from './xxx.js'
a.foo = 'hello'; // 合法操作
// 因为 a 是一个对象,修改 a 的属性是允许的。
import 是静态执行,不能使用表达式和变量。因为这些只有运行时才能得到结果的语法解构。以下我们来看 import 的错误示例:
// 报错
import { 'f' + 'oo' } from 'my_module';
// 报错
let module = 'my_module';
import { foo } from module;
// 报错
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
为了解决从模块把所有一切导入到一个单独命名空间,而不是向作用域直接导入独立的成员。 命名空间导入。
import * as foo from './export.js'
foo.bar()
foo.x;
foo.baz();
注意点: 命名空间要么全有要么全无。
import { bar, baz } as foo from 'foo'
export
exports 用来导出模块 包括标识符(identify)和模块内容(contents)。
//default
export default {}
export default class{}
export default class foo{}
export default function foo{}
//variables exports
export var foo = 1;
export var foo = function(){};
export var bar;
export let foo = 2;
export const foo = 3;
// name exports
export {foo}
export {foo, bar}
export {foo as bar}
export {foo as default}
export {foo as default, bar}
export default{
foo(){},
bar(){}
}
这个模式,js 引擎无法静态分析拼接对象的内容,这意味着它无法对静态 import 进行性能优化。让每个成员独一且显示的导出的优点是引擎可以对其进行静态分析和优化。
我们可以采用
export default function foo(){}
export function bar(){}
export function baz(){}
或者
function foo(){}
function bar(){}
function baz(){}
export{ foo as default, bar, baz, ... } from ‘..’
没有用 export 标示的一切都在模块作用域内部保持私有。
//默认导出,无需函数名
export default function(){
console.log('I am default')
}
⚠️:export 对外输出的是接口。
export 1
var m = 1;
export m
上面两种没有提供对外的接口,都是直接输出值 1。 而不是输出接口。
它们的实质是,在接口名与模块内部变量之间,建议一一对应关系. export 语句输出的接口, 与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。 CommonJS 模块输出的是值的缓存,不存在动态更新。
CommonJS 规范的核心内容
- 通过 require 关键字引用模块
- module.exports 定义模块 其中我们要理清楚的点: export 是 module.exports的引用。
require
浏览器端还不支持require require 是一个函数 参数是模块表示 require 函数的返回值是module.exports对象
// CommonJS模块
let { stat, exists, readFile } = require("fs");
// 等同于
let fs = require("fs");
let stat = fs.stat;
let exists = fs.exists;
let readfile = fs.readfile;
加载的是整个 fs 对象,再从这个对象上读取 3 个方法。 这种加载称为“运行时加载”,只有运行时才能得到这个对象,没有办法做到编译时“静态优化”。
益处:
- 编译加载,能够静态分析。 能够引入宏 (marco) 和类型检查 (type system)。
- 不在需要 umd 模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。
- 不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。
弊处:
没法引用 ES6 模块本身,因为他不是对象。
module.exports
先说结论:
exports 是 module.exports的引用。
exports是一个特殊的对象,它的任何输出都将对作为一个对外暴露的公共api.
module.exports = {} //那么顶级的会覆盖次级的导出。 导出方式:
- module.exports对外导出的对象只能有1个
- exports.xxx 对外导出的值可以有多个
两者用法区别:
exports 多用于编写对外暴露多个api的工具类代码。
module.exports 用于编写对外暴露同一个对象 api 的代码。
// 导出多个变量
exports.c= 3;
exports.d = 4;
模块依赖环
require 还有按需加载的含义,当多次引用一个模块的时候,该模块只会被加载一次,其他情况下都在缓存中加载,不需要重新加载。模块间的循环引用,require返回的对象必须至少包含此外部模块在调用require。函数之前就已经准备完毕的输出。
module A
import bar from 'B'
export default function foo(x){
if(x > 10) return bar(x-1)
return x*2
}
module B
import foo from 'A'
export default function bar(y){
if(y >5) return foo(y/2)
return y*3
}
如果先加载模块 A,第一步是扫描这个文件分析所有导出,注册所有可以导入的绑定。然后处理 import .. from 'B'。
引擎加载”B“之后,会对它的导出绑定进行同样的分析。当看到import .. from "A",它已经了解"A"的api,所以可以验证import是否有效。现在它了解'B'的api, 就可以验证等待的“A”模块中import .. from 'b'的有效性。
模块加载
第一步肯定是解析路径,系统解析出一个绝对路径。如果是核心模块,比如 fs,就直接返回模块。如果是带有路径的 /、./等,则进行拼接,然后先读取缓存require.cache,在读取文件。在加载的过程中,按照依赖关系依次执行,结果保存到 cache, 下次在运行时,遇到则不执行。 我们将 path 设置为 id, 下面是相关的伪代码
var cache = {},
loadings = {},
queue = [],
scripts = doc.getElementsTagName('script'),
root = scripts[scripts.length-1].src,
baseUrl = root.slice(0, root.lastIndexOf('/') + 1);
function Module(path, deps, factory){
this.id = path;
uitls.addLoading(this.deps);
cache[path] = this;
}
var utils = {
addLoading(){
for(var i = 0; i< deps.length; i++){
var id = deps[i],
stat = loadings[id];
loadings[id] = sta ? stat : 0
}
},
loadDeps(){
for (var i = 0; i < deps.length; i++) {
var m = deps[i];
if (m.state < STATUS.LOADED) {
m.load();
}
}
},
run(){
queue.push(path);
utils.addLoading([path]);
utils.loadDeps();
}
}
Module.prototype = {
compile:function(){
return this.factory(utils._r)
}
}
const run = utils.run()
补充
浏览器对于带有type="module"的"script",都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了"script"标签的defer属性。 以下选自阮一峰老师的《Module加载实现》 ES6模块
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
第二个差异是因为 CommonJs 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
第一个差异 CommonJs 输出的是值拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。注意:要是原始类型的值,才会被缓存。
ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。 export通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例。
script一个type="module"表明这里面是ES6的模块,而且这个标签默认是异步加载. 测试代码
let count=0
class nub{
addCount(){
count=count+2
}
}
export { nub , count }
<script type="module">
import {count, nub} from './xxx.js';
//count=4//不可修改,会报错
nub.addCount()
console.log(count);
</script>
后记
欢迎关注微信公众号!,进行更好的交流。