Open Adamwu1992 opened 6 years ago
import和export目前尚未在任何浏览器中实现,通常是借用babel/rollup之类的工具将esm代码转为cjs代码,方便在nodejs端运行,转译的代码会加上
__esModule: true
的标识。
导出有两种不同的方式:
// 导出一个已经声明过的方法
export { myFunction }
// 导出一个变量
export const a = 'hello world';
// 导出函数
export default function() {}
// 导出类
export default class {}
// 导出已经声明的变量
const a = 100;
export default a;
命名导出可以导出多个值,但是在导入时必须使用相同的名称;默认导出一个模块只能有一个,但是在导出时可以指定任意名字。
以下方法会忽略默认导出:
export * from 'moduleA'
如果需要可以使用这种方式:
import foo from '[moduleA]';
export { foo }
export default function f() {}
export const a = 100;
let b = 200;
export { b, f }
如果把以上模块的全部内容导入,会是一个包含四个属性值的对象,使用babel-node打印如下:
{ default: [Function: f], a: 100, b: 200, f: [Function: f] }
可以猜测,不管在模块中导出多少次,都会将导出的对象合并到一个对象中,而且如果有默认导出的话也会在合并后的对象中增加一个default名字的对象,所以如果要导出默认模块,以下两种方法都是一样的:
// 将默认对象命名为fa
import fa form 'moduleA'
// 将默认对象命名为fb
import { default as fb } from 'moduleA'
import * as myModule form 'moduleA'
myModule是整个模块的命名空间,可以用myModule.foo的方式访问名称为foo的导出对象,如果模块中有默认导出的对下岗,可以用myModule.default访问
import { foo } from 'moduleA'
也可以用这样的方式导出多个有名字的对象
import { foo, bar } form 'moduleA'
import { foo as fooA, bar as barA } from 'moduleA';
import { foo as fooB } from 'moduleB';
import defaultFoo from 'moduleA'
// 同时导出默认对象和全部,此时默认对象必须在前
import defaultA, * as myModuleA from 'moduleA'
// 或者
import defaultA, { foo, bar } from 'moduleA'
Note:
{ xxx, xxx }
的语法和import
用在一起时,不是对象解构(object destruct),而是导入模块中对应的命名导出(named export)。
// lib.js
export const a = 1;
export const b = 2;
// index.js
import { a, b} from 'lib'
所以下面的模块在导入是可能会出现问题
// lib.js
export default {
a: 1,
b: 2
}
// index.js
// 错误导入
import { a, b } from 'lib'
// 正确导入
import lib from 'lib'
const { a, b } = lib
在babel5中,上面的错误写法可以正确到导入值,原因是babel在处理export default
时,会将默认导出的对象绑定到module.exports
上:
// lib.js
export default {
a: 1,
b: 2
}
// dist.js
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = {
a: 1,
b: 2
};
// babel5额外增加
module.exports = exports['default']
而导入的语法进过转译会变成:
const { a, b } = require('lib')
这样就可以取到绑定在module.exports
上的值了,但是这种转译破坏了esm的定义,所以babel6中取消了对module.exports
的额外绑定。如果我们要用cjs的方式导入esm模块中的默认导出,需要这样写:
const { a, b } = require('lib').default
为了兼容旧代码,add-module-exports会将代码按照babel5的方式处理。
早期的js并不是用来实现大型程序,没有模块化的需求,但是随着js需要处理的问题越来越复杂,js模块化的需求也越来越强烈,AMD和CMD都是为了适应前端模块化的需求而出现的。
RequireJS
是AMD规范的实现,SeaJS
是CMD规范的实现。
虽说都实现了前端js的模块化,但是RequireJS
和SeaJS
在加载模块的方式上还是有差异的,简单来说就是,RequireJS
对于模块是预加载的,SeaJS
对于模块是懒加载的。
有这样一个模块:
define(function(require, exports, module) {
console.log('require main');
var mod1 = require('./mod1');
mod1.hello();
var mod2 = require('./mod2');
mod2.hello();
return {
hello: function() {
console.log('hello main');
}
}
})
使用SeaJS加载的结果如下:
require main
require mod1
hello mod1
require mod2
hello mod2
hello main
使用RequireJS加载的结果如下:
require mod1
require mod2
require main
hello mod1
hello mod2
hello main
SeaJS
只有在模块需要使用的时候才去加载模块,也就是执行模块的代码;而RequireJS
的处理方式则是提升执行,所谓预加载就是检测到代码里存在require
就会提升到顶部执行,而且提升的顺序也和require
在代码中出现的顺序无关,有可能mod2
会在mod1
之前执行
正如当年为了统一 JavaScript 语言标准,人们制定了 ECMAScript 规范一样,如今为了统一 JavaScript 在浏览器之外的实现,CommonJS 诞生了。CommonJS 试图定义一套普通应用程序使用的 API,从而填补 JavaScript 标准库过于简单的不足。CommonJS 的终极目标是制定一个像 C++ 标准库一样的规范,使得基于 CommonJS API 的应用程序可以在不同的环境下运行,就像用 C++ 编写的应用程序可以使用不同的编译器和运行时函数库一样。为了保持中立,CommonJS 不参与标准库实现,其实现交给像 Node.js 之类的项目来完成。
CommonJS也是用来解决js的模块化的,但是只适用于服务端的js,Node.js就采用了CommonJS的规范来实现自身的模块系统的。 CommonJS采用同步的方式加载模块,使用简单。在服务端,性能的瓶颈是CPU和内存,而不是带宽,所以AMD和CMD要求异步加载模块,而CommonJS可以同步加载模块。 Node.js并没有完全遵守CommonJS的规范,但除非我们的程序需要兼容Node.js之外的CommonJS实现库,一般情况下不需要关心他们之前的区别。
在node中,每一个文件就是一个模块,模块中的变量和函数都是私有的,对其他文件不可见。如果需要定义其他文件可见的变量需要挂载在global对象上,但是这种写法是不推荐的。
每个模块内部都有一个module
变量,代表当前的模块,它有以下属性:
module.id
模块的唯一标识,通常是带有绝对路径的模块名。module.filename
模块的文件名,带有绝对路径。module.loaded
返回一个布尔值,表示模块是否已经完成加载。module.parent
返回一个对象,表示调用该模块的模块。module.children
返回一个数组,表示该模块要用到的其他模块。module.exports
表示模块对外输出的值。在node中module
代表当前模块,module.exports
是对外的接口,当我们加载一个模块的时候,实际上是加载这个模块的exports
上挂载的属性。
为了方便,node中还有一个exports
变量,指向module.exports
,相当于每个node文件都有一行隐藏的代码var exports = module.exports
。所以使用 exports
导出对象的时候,不能直接改变它的指向,只能用exports.xxx = function() {}
方式,而如果模块里只有一个方法需要导出,我们可以用这样的方式module.exports = function() {}
导出唯一的方法。
nodejs使用require命令来加载一个模块,它根据加载规则找到对应的模块执行,并且返回该模块的exports属性。
默认是加载后缀为.js
的文件,后缀可以省略。
/
开头,则当作绝对路径去查找文件;./
开头,则当作相对路径去查找文件;通常发布在npm上的模块,都会把相关的文件放在一个目录里,目录中有一个package.json
文件,用main
字段指定入口文件,如果找不到入口文件,就找index.js
或者index.json
文件。
// main.js
console.log('in main.js for base', require('./base').x);
console.log('in main.js for util', require('./utils').x);
// util.js
exports.x = 'b1';
console.log('in util.js', require('./base').x);
exports.x = 'b2';
// base.js
console.log('in base.js', require('./utils').x)
exports.x = 'a2';
运行main.js后打印的结果如下:
in util.js undefined
in base.js b2
in main.js for base a2
in main.js for util b2
从结果倒推,可以分析出以下加载流程:
CommonJS是在运行时加载模块,ES6 Module是在编译是加载模块,具体来讲的区别就是,require可以被当作一个普通的函数来使用,import只能出现在文件的最开头部分:
另一个差异也和两者的加载模式息息相关:CommonJS是简单的值传递或者引用传递,ES6 Module是强绑定的,包括基础类型, ES6中可以获取到模块内实时的值:
CommonJS中获取的是模块内的拷贝: