彻底搞清楚JS的模块化import/export/require...问题
彻底搞清楚JavaScript的模块化问题
在写这篇文章之前,请让我自己的脑袋放空一下。忘记什么是JavaScript,忘记什么是编程,把自己当成一个是什么都不会的白痴来看这篇文章。
1. 谈谈历史
我们无论讨论什么技术问题,都要开始从历史谈起,因为技术是为了解决问题的,问题出现都是有历史背景的,抛开这些历史背景去谈技术问题,很容易搞不清逻辑,时间线搞不明白就记的不深刻,记得不深刻就是学了就忘记。
历史背景
首先JS为什么要有模块?因为JavaScript的创始人在创立这个代码的时候其实并没有予以厚望,只是想在浏览器验证一下输入框检查这种很弱智的事情。所以创造语言的时候也就没有多想。
<script src="jsForDemoHtml.js"></script>
最初也就简单的引入1个JS,几十行的代码。就这样的而已,事实上没有考虑模块。没有考虑到你以后会写几千行,甚至几十万行的问题。
所以如果出现了下面的问题,比如不同的文件会有相同的变量名!
但是下面这种不同模块(也就是不同文件)的冲突问题就不能解决了,因为会覆盖变量!!
// a.js var foo = "ok" // b.js var foo = "ng" // 谁知道你最后用的是a.js的变量?还是b.js的变量呢?
解决历史问题
后来为了解决命名冲突等问题,产生了一些解决的方法
- IIFE (Immediately Invoked Function Expression) 函数立即执行表达式 → 通过匿名函数的来解决
因为函数是有作用域的。
function(){}() // 这样写前面不会被解析成一段代码块 (function(){})() // 要加上括号 // 为了避免一些解析上不会被看成一段完整代码的问题还要加上; ;(function(){ var foo = "okk" console.log(foo); })() // 这样你就不能随便的在外面拿到你内部的变量(数据)了 console.log(foo);
但是这样如何让别的文件也来用你内部的作用域呢?
通过给 IIFE 赋值,然后在 IIFE 内部返回你需要暴露的变量。
// a.js var moduleBar = (function () { var foo = "okk" var bar = "bar" // 这样就返回去了 return { foo, bar } })() // 以后谁想用a.js的变量,直接就可以这样获取 // b.js moduleBar.foo
但是上面有一个麻烦
- 谁用你的变量还要跑到你的地盘打开你的文件看看你咋命名的?
- 大量的匿名函数IIFE 太脏!
大家想着,每个公司如果都有一个类似于↑那种moduleBar的规范的话,那么就会造成混乱。
比如你的公司用的是moduleBar,万一我写的也正好冲突了呢?!
所以大家都受不了了!就开始想别的方法!! 然你后CommoJS的规范就出来了!
2. CommonJS模块化规范诞生
注意!!!这只是一个规范!!!这不是一个语法,只是一个规范。
规范是什么意思呢?就是我给你说了一个大方向,你自己去实现。
国家给了你一个政策,各个地方都因地制宜制定不同的细则的感觉!
比如模块化的规范在NodeJS里就是CommonJS,在ES6里就是ES module。
最初提出来是在浏览器以外的地方使用,并且当时被命名为ServerJS,后来为了 体现它的广泛性,修改为CommonJS,平时我们也会简称为CJS。
既然CommonJS只是一个规范,那么NodeJS是怎么实现这种规范的呢?
模块化最重要的就是解决导入和导出!
-
导入
require()
永远记住,这是一个函数!! -
导出 → 比较难记忆
-
exports → 本质是对象
- module.exports
require 和 exports 的本质
我来直接说一下导出本质吧!!
require() 本质是函数,返回对象
exports 本质就是一个对象,可以在里面添加属性,属性就是你想暴露的玩意儿
首先一个模块想导出谁,直接给对象exports
添加属性
// module.js const foo = "abc" const bar = "def" // exports是一个对象 // 其实就是给exports这个对象赋值一个属性,属性名可以随便取 exports.foo = foo; // exports.newfoo = foo; 也可以的
再来看导入
const moduleName = require("module.js") // 其实每次 require 都会返回一个对象 // 也就是 require 通过各种查找方式,最终找到了 exports 这个对象; // 并且将这个 exports 对象赋值给了 moduleName 变量; // moduleName变量就是 exports 对象了; // 所以就可以直接写 moduleName.foo // 就可以拿到了 // 所以按照es6语法也就可以写成 const { foo } = moduleName // 解构赋值
其实内存表现就是这样的 所以修改 exports.foo 里面改变的话,那么moduleName.foo 也会改变!
并且因为赋值顺序是
const bar = require("我就是在找exports在哪里,找啊找") // 最后把找到的exports的地址给 bar 就行 // 也就是本质 const bar = 0x100 // exports的地址
module.exports 和 exports 有什么关系
那么 module.exports 和 exports 有什么关系或者区别呢
我们追根溯源,通过维基百科中对CommonJS规范的解析:
CommonJS中是没有module.exports的概念的
但是为了实现模块的导出,Node中使用的是Module的类,每一个模块都是Module的一个实例,也就是 module
所以在Node中真正用于导出的其实根本不是exports,而是module.exports; 因为module才是导出的真正实现者;
new Module()
一个js就是一个 module 实例- 所有的文件都有一个module全局对象,不信可以在
console.log(module)
来看看 - 真正导出的人就是
module.exports
- 源码里面可以看看
module.exports = exports
其实就是exports只是个桥梁,只是个中间调节者,最后的大BOSS还是 module.exports
那么这俩有啥区别呢
module.exports = exports // exports 有对象的时候就用exports // 但是一旦module.exports自己有了啥,但就以module.exports为准 module.exports = { foo:"aaa" }
以上所有都是CJS,而不是ES规范,CJS规范本身是有缺点的。
那是什么缺点呢?
3. CommonJS规范的缺点
- CommonJS加载模块是同步的
- 同步的意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行
- 这个在服务器不会有什么问题,因为服务器加载的js文件都是本地文件,加载速度非常快
- 如果将它应用于浏览器呢
- 浏览器加载js文件需要先从服务器将文件下载下来,之后在加载运行
- 那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作
- 所以在浏览器中,我们通常不使用CommonJS规范
当然在webpack中使用CommonJS是另外一回事,因为它能帮我们把代码转换成浏览器可以直接执行的代码
4. ES module 横空出世
为了解决CJS规范的一些问题,ES6就横空出世了!!!
- 采用关键字
import
还有export
而不是函数require()
或者对象module.exports
- 采用严格模式 use strict
import export 是啥
首先自己要知道一个地方就是 这俩是特殊的关键字 不是对象更不是函数
import { } export { } // 上面看起来像是一个对象,但其实没有 export = {} 本质就是一个关键字 // 导出的是里面变量的引用
导出 export
- 导出(2个类)
- 有名字的导出 named export
- 默认导出 default export
// named export 导出方式三种 // 1.方式一: export const foo = 'foo'; export const bar = 'bar'; export const hoge = (foo) => {console.log(foo)} // 2.方式二: {}中统一导出 // {}大括号, 但是不是一个对象 // {放置要导出的变量的引用列表} const foo = 'foo'; const bar = 'bar'; const hoge = (foo) => {console.log(foo)} export { foo, bar, hoge } // 3.方式三: {} 导出时, 可以给变量起 别名 export { foo as Cfoo, bar as Cbar, hoge as Choge }
默认导出 default export
- 一个文件只能有一个 default export
// named export 导出方式三种 export default const foo = () => {}
导入 import
// 1. 方式一 // import {标识符列表} from '模块'; // 这里的{}也不是一个对象,里面只是存放导入的标识符列表内容; import { foo, bar, hoge} from "./module.js" // 2. 方式二 // 导入时给标识符起别名 import { foo as Cfoo, bar, hoge} from "./module.js" // Q:如果在导出的时候已经给取别名了咋办? // A: 可以在别名的基础上在命名一次 但一定要用export 别名的那个命名 import { Cfoo as CDfoo, bar, hoge} from "./module.js" // 3. 方式二
关于import其实她还是个函数,用来解决什么问题呢?
先想象一个场景,比如
// 当flag为true的时候才导入某个模块 let flag = true if (flag) { import time from "./module.js" // NG // 但是require()就可以 因为require是函数,运行阶段才执行 // 但是因为ES module不认require() ※ 除非你用的是webpack的模块化打包工具 }
NodeJS在解析阶段其实就会解析import time from "./module.js"
,所以直接像上面这么写,是会报错的。但require()
是一个函数,解析阶段并不会执行。
所以上面import错误的原因就是一段解析阶段的代码你放到了运行阶段去执行!!
那么怎么办呢?
import()
闪亮登场!!
// 当flag为true的时候才导入某个模块 let flag = true if (flag) { // 因为是异步加载 非同步 有个返回值 返回值是promise const promise = import("./module.js") .then((res)=>{ console.log(res) }) .catch() }
于是接下来验证大概是这样的
// 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" /> <script src="demo.js" type="module"></script> <title>Document</title> </head> <body></body> </html> // demo.js let flag = true; if (flag) { const promise = import('./module.js') .then((res) => { console.log('print in res'); console.log(res.obj); }) .catch((err) => { console.log(err); }); } // module.js const obj = { name: 'chin', age: 199, }; let foo = 'iiii'; export { obj, foo };
5. 关于ES导出的内存图
下面假如是 main.js 需要引入 a.js
// a.js let name = 'chin'; let age = 18; setTimeout(()=>{ name = "nnnn"; }) export { name, age }
这个时候有个main.js 需要导入
// main.js import { name, age } from './a.js'; console.log(name);
那么内存的表现是什么样子的?其实
1. export在导出一个变量时,js引擎会解析这个语法 2. 并且创建 模块环境记录(module environment record) 3. 模块环境记录会和变量进行绑定(binding) 4. 这个绑定是实时的,而在导入的地方,我们是可以实时的获取到绑定的最新值的
6. CommonJS和ES Module能互通吗
require和export混用
方式一
通常情况下,CommonJS不能加载ES Module → 因为CommonJS是同步加载的,但是ES Module必须经过静态分析等,无法在这个时候执行JavaScript代码; → 但是这个并非绝对的,某些平台在实现的时候可以对代码进行针对性的解析,也可能会支持 → Node当中是不支持的
// a.js const name = "chin" export { name } // main.js 导入文件a require("a.js") // 这样可以不?
方式二
多数情况下,ES Module可以加载 CommonJS → ES Module在加载CommonJS时,会将其module.exports导出的内容作为【default】导出方式来使用 → 这个依然需要看具体的实现,比如webpack中是支持的、Node最新的Current版本也是支持的
// a.js const age = 10; module.exports = { age } // main.js → 默认用的是default形式 import foo from "./a.js"
总结
其实也写了这么多了,如果实在觉得看着挺麻烦的。可以直接看下面这个。
- CommonJS规范用的是
require(),exports组合
require()
本质函数。需要谁,导入谁,返回值是对象。exports
本质对象。想暴露哪个变量,就给exports增加属性就行module.exports
是NodeJS实现的exports,module.exports = exports
,module.exports每个文件都有,本质是一个实例,所以最后还是以module.exports
为准
- ES module用的是
import
,export
组合
- import 本质是一个关键字。特殊情况下也可以当函数
import()
- export 本质是关键字。看起来装的样子是对象,因为可以
export{}
,但是仔细一看,人家对象可是export = {}
才对啊,所以这玩意儿就是个关键字。
如果有一天我又混淆了,一定要先分清,自己是两大阵营的哪一块,然后再来看一遍这篇文章吧。
共有评论(2)
感谢,这篇文章写得很棒
EuDs63:
Good!
hokyo: