由于JavaScript缺乏模块体系,在ES6之前,社区指定了一些模块加载方案,比如node.js所遵循的commonJS规范。而在ES6在语言层面上实现了模块功能,完全可以取代现有的commonJS等规范,成为浏览器、服务器通用的模块解决方案。本篇博客目录如下:
1.如何使用exports
2.如何使用module.exports
3.exports与module.exports的区别
4.使环境支持module语法
5.export与export default的基本用法
1.如何使用exports
在es6还没有出现的日子里,通过commonJS实现模块加载会接触到两个方法,exports与module.exports。我们先来看exports,在node.js中,我们通常会以exports.name=something
的形式来使用exports
//导出文件mout.js
function outputA(){
console.log("this is output A");
}
function outputB(){
console.log("this is output B");
}
exports.outputA=outputA;
exports.outputB=outputB;
//导入文件module.js
let mod=require("./mout.js");
console.log(mod);
这时会输出
如果你想在module.js中使用outputA或outputB函数,就得
let {outputA,outputB}=require("./mout.js");
//{}中变量名必须与mout.js导出函数名相同
outputA();
outputB();
上面的写法也等同于
let _mout=require("./mout.js");
//变量a,b的变量名可以随意
let a=_mout.outputA;
let b=_mout.outputB;
a();
b();
2.如何使用module.exports
在commonJS中还有一种方法可以导出模块。我们通常以module.exports=something
的形式使用它
//还是那个导出文件
function outputA(){
console.log("this is output A");
}
function outputB(){
console.log("this is output B");
}
module.exports=outputA;
module.exports=outputB;
//还是那个导入文件
let mod=require("./mout.js");
console.log(mod);
这个时候我们会发现输出是这样子的
虽然我们在mout.js中module.exports两次,但输出却告诉我们只有最后一次module.exports是有效的,第一次module.exports被覆盖掉了。如果我们想在module.js中使用outputB只需mod()
即可。
所以,当我们需要将模块定义为一个类时,使用module.exports是唯一的选择。
3.exports与module.exports的区别
其实,module.exports 初始值为一个空对象 {},require() 返回的是 module.exports 而不是 exports,exports只不过是指向的 module.exports 的引用。
赋给exports的所有属性或方法最终都赋值给了module.exports。但,这是有前提条件的,那就是module.exports是空对象,如果module.exports具有了一些属性或方法,那么赋给exports的所有属性或方法都会被忽略掉,譬如
//还是TA
function outputA(){
console.log("this is output A");
}
function outputB(){
console.log("this is output B");
}
exports.outputA=outputA;
module.exports=outputB;
//是TA是TA就是TA
let mod=require("./mout.js");
console.log(mod);
这样子,输出的就是酱紫的
而且无论exports在module.exports前还是后,结果都是module.exports生效,而exports看起来并没有发挥什么卵用。
通过上面的推导,我们可以猜测给module.exports添加属性等同于于给exports添加属性
//。。。。。
function outputA(){
console.log("this is output A");
}
function outputB(){
console.log("this is output B");
}
module.exports.outputA=outputA;
module.exports.outputB=outputB;
//。。。。。
let mod=require("./mout.js");
console.log(mod);
直接看结果
一目了然
4.使用ES module语法
原文编写时,node版本才更新到9.3.0,并不能直接在node环境中使用ES module,浏览器对ES module的支持也不完善,所以当时是通过webpack编译来使用ES module。
随着时代的进步,node从13.2.0开始支持ES module。而在浏览器方面,尤其是chrome浏览器,对ES module的支持越来越完善。所以我们有多种方式使用ES module。
4.1 通过webpack编译
为了在不支持ES module的node版本中进行体验,我们需要用到babel或者类似于babel的工具对代码进行编译。下面以babel为主进行讲解。
首先需要说明的是,如果你通过babel-node等工具试图直接在命令行中运行es6代码可能会出现错误。这是因为
Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。而且像import和export这两个命令现在在任何浏览器中都是不支持的, 同时babel也无法将其转换为浏览器支持的ES5, 原因在于:babel只是个翻译,假设a.js 里 import 了 b.js, 对a.js进行转码,只是翻译了a.js,并不会把b.js的内容给读取合并进来, 如果想在最终的某一个js里,包含 a.js,b.js 的代码,那就需要用到打包工具
接下来就以webpack为例展示如何进行转码
首先,你得有个目录,并且你安装了node与npm
进入到目录中,运行npm init
init完了之后,执行npm install webpack --save-dev --no-optional
接下来,你需要安装babel-loader与babel-core
npm install --save-dev --no-optional babel-loader babel-core
然后根据你的需求选择转码规则,目前有这些
我们使用latest吧,npm install --save-dev --no-optional babel-preset-latest
之后我们还会用到path,所以一并安装上npm install --save-dev --no-optional path
目前为止,目录下的webpack.config.js应该是酱紫的
这一堆工作做完之后,我们还需要在根目录下创建一个webpack配置文件——webpack.config.js,具体内容如下
const path=require("path");
module.exports = {
entry: __dirname + "/app/import.js",//入口文件
output: {
path: __dirname + "/dist",//打包后的文件存放的地方
filename: "bundle.js"//打包后输出文件的文件名
},
module:{
loaders:[
{
test:/\.js$/,
loader:'babel-loader',//-loader不可省略
include:path.resolve(__dirname,'/app'),//babel只处理指定目录
options:{
presets:['latest']
}
}
]
}
}
其中,options字段值得我们注意。一般来说babel有三种配置方式:
通过webpack.config.js loaders中的options字段
通过package.json中的babel字段
通过.babelr文件
三种方式具体的配置内容是一模一样的,我比较喜欢第一种
这样子一通折腾下来后,我们在app下创建两个文件import.js与export.js。
//import.js
import {alias,outputB} from "./export.js"
console.log(alias);
console.log(outputB);
//export.js
function outputA(){
console.log("this is output A");
}
function outputB(){
console.log("this is output B");
}
export {outputA as alias,outputB}
之后在根目录下运行webpack
,等待打包完成之后,在dist目录下就会出现bundle.js,我们通过html引用也好,还是直接node运行都可以,输出结果是
4.2 通过babel-node
如果只是简单在本地node环境中使用ES module,其实有种简单的方法是使用babel-node执行文件。为此,我们需要安装三个包
npm i -g @babel/core @babel/node @babel/plugin-transform-modules-commonjs
然后就可以通过如下命令执行使用ES module的文件
babel-node [file path] --plugins @babel/plugin-transform-modules-commonjs
4.3 直接在node中使用
node从13.2.0开始支持ES module,可以通过以下两种方法使用
4.3.1 文件后缀名使用.mjs
举个例子,我们的文件目录可以是这样:
.
|____a.mjs
|____index.mjs
// a.mjs
export let foo = 'bar'
setTimeout(() => foo = 'baz', 500)
// index.mjs
import { foo } from './a.mjs'
console.log(foo)
setTimeout(() => console.log(foo), 500)
执行node index.mjs
,代码可以正确执行,并且按照ES module的特性,开始执行时输出bar,0.5秒后输出 baz。
4.3.2 在项目的package.json中设置type
如果不想使用.mjs后缀的文件格式,也可以在根路径添加package.json文件,配置type:module
即可
在上一个例子的基础上,修改.mjs后缀为js,添加package.json:
.
|____a.js
|____index.js
|____package.json
package.json如下:
{
...
"type": "module",
...
}
执行node index.js
,结果和之前一致。
4.4 在浏览器中使用
在浏览器中使用ES module感觉会更方便直观一点,在这里我们可以查看浏览器的支持程度,以及完整的例子。总的来说,在浏览器中使用ES module最关键的一步是为<script>
标签添加type="module"
,来声明这是一个模块。
比如如下目录:
.
|____a.js
|____index.js
|____main.js
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module" src=".index.js"></script>
</body>
</html>
index.js和a.js同4.3.1,在浏览器中打开index.html,结果应该是一致的。
5.export与export default的基本用法
普通的、老老实实的、朴实无华的操作
function outputA(){
console.log("this is output A");
}
function outputB(){
console.log("this is output B");
}
export {outputA,outputB}
import {outputA,outputB} from "./export.js"
outputA()
在引入文件中,如果需要使用export.js中的函数,用户必须先知道它的名字,如果用户不想翻文档或者怎么滴,这样子就不太方便,所以就有了export default
function outputA(){
console.log("this is output A");
}
export default outputB;
import alias from "./export.js"
console.log(alias);
alias();
注意,在import的时候,我们已经不用使用{}了
模块整体加载
如果我们需要加载模块的全部接口,那么只需
function outputA(){
console.log("this is output A");
}
function outputB(){
console.log("this is output B");
}
export {outputA,outputB}
import * as alias from "./export.js"
export时使用别名
function outputA(){
console.log("this is output A");
}
function outputB(){
console.log("this is output B");
}
export {outputA as alias,outputB}
接下来,我们进行一些比较骚气的操作
function outputA(){
console.log("this is output A");
}
function outputB(){
console.log("this is output B");
}
export {outputA as alias,outputB}
export default outputB;
这段代码既有export又有export default
我们先
import * as x from "./export.js"
console.log(x);
这时,console出来的x是
这里我们可以看出export default的本质是输出一个叫做default的方法,系统允许我们对它取任意名称
接下来,酱紫
import x from "./export.js"
console.log(x);
输出结果是
也就是说,如果import时变量只有一个,而且外面没有{},那么该变量接受的就是export default。如果import时变量放在{}里,那么就会拿到对应的export。我们可以用这种方式同时接受export与export default
import x,{alias,outputB} from "./export.js"
6.CommonJs和ES Module的区别
6.1 语法不同
commonjs是module.exports,exports导出,require导入
ES6则是export导出,import导入
6.2 是否存在提升
ES module在编译期间会将所有import提升到顶部,commonjs不会提升require。
foo()
import { foo } from 'module'
这代码并不会报错,因为import会被提升到文件顶部。
6.3 是否支持动态加载
commonjs是运行时加载模块,支持动态加载。
ES6是在静态编译期间就确定模块的依赖,不支持动态加载。
// 报错
import { 'f' + 'oo' } from 'module'
// 报错
let module = 'module'
import { foo } from module
// 报错
if (x === 1) {
import { foo } from 'module1'
} else {
import { foo } from 'module2'
}
由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
但ES2020提案引入import()
函数,支持动态加载模块。
import()
函数接受一个参数specifier
,为所要加载的模块的位置,返回一个 promise。
import()
类似于Node的require
方法,区别主要是前者是异步加载,后者是同步加载。
看个例子:
// main.js
function init() {
const canvas = 'canvas'
// 使用import函数
import(`./modules/${canvas}.js`).then((module) => {
console.log(module.default)
})
}
init()
// canvas.js
console.log('canvas.js开始执行')
export default 'canvas'
6.4 导出值
commonjs导出的是一个值拷贝,会对加载结果进行缓存,一旦内部再修改这个值,则不会同步到外部。比如:
// a.js
// 第一次require之后就缓存了done
const { done } = require('./b.js')
console.log(done)
setTimeout(() => {
console.log(done)
}, 500)
// b.js
var done = false
setTimeout(() => {
done = true
}, 500)
exports.done = done
执行node a.js
,两次打印结果应该都是false
ES6是导出的一个引用,内部修改可以同步到外部。看一个之前的例子:
// a.js
export var foo = 'bar'
setTimeout(() => foo = 'baz', 500)
// index.js
import { foo } from './a.js'
console.log(foo)
setTimeout(() => console.log(foo), 500)
根据ES module的特性,开始执行时输出bar,0.5秒后输出 baz。
6.5 this指向
commonjs中顶层的this指向这个模块本身,而ES6中顶层this指向undefined。
因为ES module采用严格模式,严格模式禁止this指向全局对象。
6.6 循环加载的处理
由于之前五个不同点,导致commonjs和es module在处理循环加载时的表现不同。可以参考这篇文章
6.6.1 CommonJs的表现
根据 6.4 ,在commonjs中,代码在require的时就会全部执行,然后在内存生成一个对象,内部修改不会同步到外部。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
比如可以运行如下例子:
// mian.js
var a = require('./a.js')
var b = require('./b.js')
// 在 main.js 之中, a.done=true, b.done=true
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done)
// a.js
exports.done = false
console.log('a 开始执行')
var b = require('./b.js')
// 在 a.js 之中,b.done = true
console.log('在 a.js 之中,b.done = %j', b.done)
exports.done = true
console.log('a.js 执行完毕')
// b.js
exports.done = false
// 返回a.js已经执行的部分,此时done为false
var a = require('./a.js')
// 在 b.js 之中,a.done = false
console.log('在 b.js 之中,a.done = %j', a.done)
exports.done = true
console.log('b.js 执行完毕')
6.6.2 ES module的表现
在ES module中,import存在提升,且导出值是只是一个引用,等到真的需要用到时,会再去模块里面去取值。比如导出一个变量,内部修改会影响外部。
可以运行这个例子:
// a.js
console.log('执行a.js')
// import会被提升到顶部
import { bar } from './b.js'
export function foo() {
bar()
console.log('执行完毕')
}
foo()
console.log('a.js执行完毕')
// b.js
console.log('执行b.js')
import { foo } from './a.js'
export function bar() {
if (Math.random() > 0.5) {
foo()
}
}
console.log('b.js执行完毕')
执行a.js,输出结果应该类型于
“执行b.js”
“b.js执行完毕”
“执行a.js”
“执行完毕”
“a.js执行完毕”
四种输出的顺序是固定的,但“执行完毕”
的输出次数不定