JavaScript 中的模块化

将程序划分成一个个小的结构,在每个结构中编写属于自己的逻辑代码。每个结构有自己独立的作用域,定义变量名不会影响到其他的结构;可以暴露变量、函数、对象等给其他结构使用;也可以导入其他结构的变量、函数、对象等。这种结构就是模块。

按照这种结构划分程序进行开发的过程,就是模块化开发。

之前的 JavaScript 没有模块化:

之前的 JavaScript 没有模块化。

例如:在 index1.js 中定义一个变量 name 为 Lee,在 index2.js 中定义一个变量 name 为 Mary,将它们都引入到 index.html 文件中,变量名就会互相影响。因为只是将 JavaScript 代码分开到了不同的 JavaScript 文件中,但这些 JavaScript 文件是没有独立的作用域的,它们其实还是都在全局作用域下。

// index1.js
var name = 'Lee'
// index2.js
var name = 'Mary'
// index.html
<script src="../src/js/index1.js"></script>
<script src="../src/js/index2.js"></script>
console.log(name) // Mary。变量名相互影响,因为 index2.js 在 index1.js 后面引入,因此打印 Mary

之前是使用立即执行函数来解决这个问题的。

// index1.js
// 强制给  JavaScript 文件中的变量增加了一个函数作用域
var module1 = (function(){
  var name = 'Lee'
  
  // 将变量返回出去
  return {
    name
  }
})()
// index2.js
// 强制给  JavaScript 文件中的变量增加了一个函数作用域
var module1 = (function(){
  var name = 'Mary'
  
  // 将变量返回出去
  return {
    name
  }
})()
// index.html
<script src="../src/js/index1.js"></script>
<script src="../src/js/index2.js"></script>
// 变量名不会相互影响
console.log(module1.name) // Lee
console.log(module2.name) // Mary

但这种解决方案,也有一定的问题:在没有合适的规范的情况下,可能会对模块任意命名,甚至出现模块名称相同的情况;而且必须得记住每一个模块名,才能在使用的时候正确使用;代码写起来混乱不看,每个文件中的代码都需要包裹在一个匿名函数中来编写。

AMD:

AMD 只是一种规范,它采用异步方式加载模块,模块的加载不影响它后面语句的运行。AMD 规范应用在浏览器端。

实现了 AMD 规范的比较常用的库有:require.jscurl.js

CommonJS:

CommonJS 只是一种规范,主要应用在服务器端。

Node 是 CommonJS 在服务器端的一个具有代表性的实现,因此可以直接在 Node 中运行 CommonJS;Browserify (一个浏览器端代码模块化的工具)是 CommonJS 在浏览器中的一个实现,浏览器自身并不支持 CommonJS,因此不能直接在浏览器上运行 CommonJS。

CommonJS 的缺点:CommonJS 加载模块是同步的,这意味着只有等到引入的模块加载完毕,当前模块中的内容才能运行。这在服务器端不会有什么问题,因为服务器加载的 JS 文件都是本地文件,加载速度非常快;但如果是在浏览器端,将会导致很长时间的阻塞。

导出模块:

导出模块使用 exports 和 module.exports

  1. exports:是一个对象,在这个对象上添加的属性会被导出。这种方式在开发中很少使用。
    // utils.js
    const name = 'Lee'
    exports.name = name
    
  2. module.exports:module 对象有一个 exports 属性,指向的是一个对象,在这个对象上添加的属性会被导出。Node 导出的本质就是在导出 module.exports 对象。

    CommonJS 规范中是没有 module.exports 的,只有 exports。
    在 Node 中使用的是 Module 的类,每一个模块都是 Module 类的一个实例 module,因此在 Node 中真正用于导出的其实是 module.exports
    为了兼容 CommonJS 规范,Node 中才保留了 exports 这个关键字,让 module.exports 默认指向 exports。

    // 可以简单地理解为,CommonJS 在每个模块的头部默认添加了以下代码:
    var module = {
    	exports:{}
    }
    var exports = module.exports;
    
    //  Node 导出的本质就是在导出 module.exports 对象。module.exports 和 exports 默认指向的对象是同一个对象,因此 exports 导出才有效
    
    // utils.js
    exports.name = 'Lee'
    module.exports.name = 'Mary'
    // main.js
    const utils = require('./utils.js')
    console.log(utils.name) // Mary
    
    // utils.js
    module.exports.name = 'Mary'
    exports.name = 'Lee'
    // main.js
    const utils = require('./utils.js')
    console.log(utils.name) // Lee
    
    // Node 导出的本质就是在导出 module.exports 对象。此时,module.exports 指向的是一个新的对象,和 exports 指向的对象不再是同一个对象,此时 export 导出无效。这种方式在开发中最常用
    // utils.js
    module.exports = {
      name: 'Mary',
    }
    exports.name = 'Lee'
    // main.js
    const utils = require('./utils.js')
    console.log(utils.name) // Mary
    

导入模块:

导入模块使用 require() 函数,可以引入一个模块中导出的对象。

// utils.js
const name = 'Lee'
module.exports.name = name
// main.js
const utils = require('./utils.js')
console.log(utils.name)

在 Node 中,每个 JavaScript 文件就是一个模块,并且 Node 已经实现了 CommonJS,因此在命令行中输入 node main.js 即可看到打印结果。

导入模块时查找文件的规则:

导入模块时,require(X) 查找文件的常见规则:

  1. 如果 X 是 Node 内置模块(例如:path、http 等),那么停止查找,直接返回核心模块。
  2. 如果 X 以 ./ 或者 ../ 或者 / 等开头:
    • 首先会将 X 当做一个文件在对应的目录下查找。
      • 如果有后缀名:直接按照后缀名的格式查找对应的文件。
      • 如果没有后缀名,按照以下顺序查找:直接查找文件 X;查找 x.js 文件;查找 X.json 文件;查找 x.node 文件。
    • 如果没有找到对应的文件,会将 X 当做一个文件夹。
      • 查找目录下的 index 文件:查找 X/index.js 文件;查找 X/index.json 文件;查找 X/index.node 文件。
      • 如果还没有找到,报错。
  3. 如果 X 不以 ./ 或者 ../ 或者 / 等开头,直接就是一个非 Node 内置模块的名称 X:
    • 首先会在当前目录的 node_modules 中,查找 X 文件夹下的 index.js
    • 如果没有找到,会去上层目录的 node_modules 中查找,以此类推,直到找到根目录。
导入模块的加载过程:
  1. 模块在第一次被导入时,其中的 JavaScript 代码会被从上到下执行一次。

  2. 模块被多次导入时,会缓存,其中的 JavaScript 也只会执行一次。

    原理是:每个模块模块对象 module 都有有一个 loaded 属性,默认为 false,只要被导入执行过一次,就变为 true,如果再次导入,发现 loaded 为 true,就不会再执行了。

    // utils.js
    console.log('utils')
    
    // main.js
    require('./utils.js')
    console.log('main')
    require('./utils.js')
    
    // 执行 node main.js,会打印
    utils
    main
    
  3. 如果出现循环引入,Node 采用的是深度优先算法。
    在这里插入图片描述
    如上图,加载顺序是:main --> aaa --> bbb --> ccc --> ddd --> eee --> bbb

CommonJS 在 Node 中实现的导出和导入的本质:

CommonJS 在 Node 中实现的导出和导入的本质其实就是引用赋值,通过 require()找到 nodule.exports 对象,然后将其赋值给变量。

// utils.js
const name = 'Lee'
exports.name = name

setTimeout(() => {
  exports.name = 'Mary'
}, 1000)
// main.js
const utils = require('./utils.js')
console.log(utils.name) // 最开始打印 Lee

setTimeout(() => {
  console.log(utils.name) // 两秒钟后打印 Mary。因为 utils 和 exports 指向的是同一个对象
}, 2000) 

CMD 规范:

CMD 只是一种规范,也是采用异步方式加载模块,但是它将 CommonJS 的优点也吸收了过来。CMD 规范是应用于浏览器端的。

实现了 AMD 规范的比较常用的库有:sea.js

ES6 中的 ES Module:

直到 ES6,ECMAScript 官方才推出了 JavaScript 的模块化方案 ES Module。浏览器自身就支持 ES Module。

在此之前,为了让 JavaScript 支持模块化,社区涌现出了很多不同的模块化规范:AMD、CommonJS、CMD 等。但是目前,AMD 和 CMD 已经很少使用了。

Webpack 支持对 CommonJS 和 ES Module 的转化。开发中使用 CommonJS 和 ES Module 进行开发,最后经由 Webpack 打包后就只是普通的没有使用模块化的 JS 文件,就样的话,即使浏览器不支持 CommonJS 和 ES Module ,也可以在上面运行了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值