本文本身借鉴他人文章知识点进行咀嚼,再次输出,方便自己进行吸收,同时也是为了后期回顾以及与他人共享。倘若侵权,可私下联系,我会立马删除!
CommonJS 与 ES Module 知识点 一
- 了解两者发展历程
- 讲述两者各自优缺点
- 总体去理解
了解两者发展历程
前期无模块化,js只是简单的一些作用。--》node.js出现(将js带入后端)CommonJS 最初是服务于服务端的,由于js是语言载体--》发展中就有了模块化 《--这主要是没有模块化导致了一些问题
没有模块化时,前端是什么样子
当项目较大(多人参与)--》导致全局变量污染 [《--没有模块系统,没有封闭作用域的概念](前端科普系列-CommonJS:不是前端却革命了前端 - 知乎 (zhihu.com))
// index.html
<script src="./mine.js"></script>
<script src="./a.js"></script>
<script src="./b.js"></script>
// mine.js
var name = 'james'
var age = 19
// a.js
var name = 'zs'
var age = 25
// b.js
var name = 'ww'
var age = 23
解决方案初期(模块化原型)
这块知识可以使我们理解模块化需要解决的问题的初步思路。
命名空间-》作用域-》依赖关系
- 就是在每个js中加入命名空间---》解决区别不同变量--》存在js文件中变量值可以随意更改
- 引入闭包解决变量随意更改的情况--》闭包(保护变量不被修改)--》由于js加载顺序,模块之间依赖关系就很重要,当系统十分复杂的时候;就会出错;for example: 加入A模块在前,B模块在后,则B可以取到A模块的信息,A模块取不到B模块的变量。
对上面的总结
综上所述,前端需要模块化,并且模块化不光要处理全局变量污染、数据保护的问题,还要很好的解决模块之间依赖关系的维护。看到这里可以停下来思考下如何解决模块依赖这个问题。
讲述两者各自优缺点
- CommonJS简介
- ES module简介
CommonJS简介
既然 JavaScript 需要模块化来解决上面的问题,那就需要制定模块化的规范,CommonJS 就是解决上面问题的模块化规范,规范就是规范,没有为什么,就和编程语言的语法一样。我们一起来看看。
Node.js 应用由模块组成,每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
// a.js
var name = 'morrain'
var age = 18
除了a.js其余文件都访问不了其中的变量
CommonJS 规范还规定,每个模块内部有两个变量可以使用,require 和 module。
require 用来加载某个模块
module 代表当前模块,是一个对象,保存了当前模块的信息。exports 是 module 上的一个属性,保存了当前模块要导出的接口或者变量,使用 require 加载的某个模块获取到的值就是那个模块使用 exports 导出的值
// a.js
var name = 'morrain'
var age = 18
module.exports.name = name
//可以写成exports.name = name
//但是不能写成exports = name --->这就表示成了将exports指向了name这个变量了
module.exports.getAge = function(){
return age
}
//b.js
var a = require('a.js')
console.log(a.name) // 'morrain'
console.log(a.getAge())// 18
CommonJS 之 exports --->就是将exports指向了module.exports
CommonJS 之 require---->
require 命令的基本功能是,读入并执行一个 js 文件,然后返回该模块的 exports 对象。如果没有发现指定模块,会报错。
CommonJS 实现
里面还是有蛮多东西的;如果我们向一个立即执行函数提供 require 、 exports 、 module 三个参数,模块代码放在这个立即执行函数里面。模块的导出值放在 module.exports 中,这样就实现了模块的加载。如下所示:
(function(module, exports, require) {
// b.js
var a = require("a.js")
console.log('a.name=', a.name)
console.log('a.age=', a.getAge())
var name = 'lilei'
var age = 15
exports.name = name
exports.getAge = function () {
return age
}
})(module, module.exports, require)
知道这个原理后,就很容易把符合 CommonJS 模块规范的项目代码,转化为浏览器支持的代码。很多工具都是这么实现的,从入口模块开始,把所有依赖的模块都放到各自的函数中,把所有模块打包成一个能在浏览器中运行的 js 文件。譬如 Browserify 、webpack 等等。
我们以 webpack 为例,看看如何实现对 CommonJS 规范的支持。我们使用 webpack 构建时,把各个模块的文件内容按照如下格式打包到一个 js 文件中,因为它是一个立即执行的匿名函数,所以可以在浏览器直接运行。
// bundle.js
(function (modules) {
// 模块管理的实现
})({
'a.js': function (module, exports, require) {
// a.js 文件内容
},
'b.js': function (module, exports, require) {
// b.js 文件内容
},
'index.js': function (module, exports, require) {
// index.js 文件内容
}
})
CommonJS 规范有说明,加载过的模块会被缓存,[对于缓存而言,这篇文章很好的诠释了]((64条消息) 理清楚Commonjs中的缓存_白纸一样的博客-CSDN博客_commonjs缓存)所以需要一个对象来缓存已经加载过的模块,然后需要一个 require 函数来加载模块,在加载时要生成一个 module,并且 module 上 要有一个 exports 属性,用来接收模块导出的内容。
还有一些细节,通过该链接去看,作者写得特别好,就不再补充了。(前端科普系列-CommonJS:不是前端却革命了前端 - 知乎 (zhihu.com))
前端化模块方案(解释为什么会产生版本的演变)
由于CommonJS是node.js后端的模块化使用,执行完js文件,返回exports对象就结束了,这在服务端是可行的,因为服务端加载并执行一个文件的时间消费是可以忽略的,模块的加载是运行时同步加载的,require 命令执行完后,文件就执行完了,并且成功拿到了模块导出的值。硬盘上读文件比网络请求快得多。<----> CommonJS规范<---->缺陷就是同步,不能用于浏览器,不然会出现阻塞页面渲染,页面假死状态等。
还有其他演变模块规范:前端模块化--AMD\CMD;前后端--ES6;
前端模块化不进行介绍,用的比较少;只介绍ES6了。
ES6 Module
实验
就是对于缓存而言,如果想要验证下,就先创建个文件夹;再进行npm init -y ; 然后 npm install express --save;搭建好环境后,自己再进行尝试。(D:code-->interview_code-->common)
对于引用数据而言,浅拷贝-----真的就是从数据类型所决定的吗?
//c.js
var o = {
name: 'zhangsan'
}
var rename = function() {
o.name = 'lisi'
return o //用作比较
}
function get () {
return o
}
module.exports = {o,rename,get}
//test_c.js
var cur = require('./c')
console.log(cur.o, '接收值 未操作前') // { name: 'zhangsan' }
let b = cur.rename() // 修改对象内部属性值
console.log(cur.o, '接收值 操作后') //{ name: 'lisi' }
console.log(cur.get(), '原文件数据') // { name: 'lisi' }
// 对象调用的基本属性与原文件的基本属性就是复制的操作;
console.log(b == cur.o) // true
console.log(b == cur.get()) // true
// 总结发现是浅拷贝,就是对象内部元素会随着一起变,共用的。
其实这样去解释就很苍白,真正的缓存需要我们从require加载的方式去理解
在node中,我们可以输出require.cache来查看当前缓存,require.cache 指向所有缓存的模块。第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的module.exports属性。 这也就解释了为什么我们操作的都是同一个导出的对象,即使多次require。
// 在上一段代码中,打印require.cache
console.log(require.cache)
// 输出结果
[Object: null prototype] {
'D:\\Code\\interview_code\\Common\\test_c.js': Module {
id: '.',
path: 'D:\\Code\\interview_code\\Common',
exports: {},
filename: 'D:\\Code\\interview_code\\Common\\test_c.js',
loaded: false,
children: [ [Module] ],
paths: [
'D:\\Code\\interview_code\\Common\\node_modules',
'D:\\Code\\interview_code\\node_modules',
'D:\\Code\\node_modules',
'D:\\node_modules'
]
},
'D:\\Code\\interview_code\\Common\\c.js': Module {
id: 'D:\\Code\\interview_code\\Common\\c.js',
path: 'D:\\Code\\interview_code\\Common',
exports: { o: [Object], rename: [Function: rename], get: [Function: get] },
filename: 'D:\\Code\\interview_code\\Common\\c.js',
loaded: true,
children: [],
paths: [
'D:\\Code\\interview_code\\Common\\node_modules',
'D:\\Code\\interview_code\\node_modules',
'D:\\Code\\node_modules',
'D:\\node_modules'
]
//可以在导入一次模块,判断是否相同
// 证明重新拿到的是其内的exports
var cur2 = require('./c')
console.log(require.cache[`D:\\Code\\interview_code\\Common\\c.js`].exports === cur2)
// true
其实说白了,就是在编译模块成功后把这个编译好的对象(exports)作为属性存在了一个用于记录缓存的对象上。之后再次require时,会从这个缓存对象上根据require内的路径查找对应的exports对象。
require这块知识还是介绍太少了,其中有很多细节
比如加载顺序,优先级等等
初步理解
对于模块化有了个初步的了解;[关于这方面](「万字进阶」深入浅出 Commonjs 和 Es Module - 掘金 (juejin.cn))这位作者写的很好,可以移步去欣赏!!!