JS的模块化

185 阅读7分钟

一、模块化解决的问题

模块化解决的2个问题:

  • JS在页面的加载顺序(JS引擎在加载当前JS文件的时候,后边的JS文件是不执行的)

  • 全局污染(JS引入到页面之中,会共用一个作用域:全局作用域,有可能会产生变量重名导致的变量覆盖,污染全局作用域)

立即执行函数(解决全局污染问题)

ES5中利用立即执行函数来解决全局污染的问题

立即执行函数页面加载的时候自动执行,并且执行完毕之后立即释放(全局作用域找不到这个函数,所以不用写函数名)

立即执行函数可以有返回值,如果想获取返回值,就交给一个变量进行保存

//module_a.js
var moduleA = (function(){  
   //作用域 上下文
   //立即执行函数创建一个模块的独立作用域
   var a = [1,2,3,4].reverse()
   return { //return写成对象形式,通用做法,这里产生了闭包,函数的作用域被拉到了全局作用域上
       a:a
   }
})();
//module_b.js
var moduleB = (function(moduleA){ 
   var b = moduleA.a.concat([6,7,8]) //通过模块注入解决模块依赖
   return {
       b:b
   }
})(moduleA)

//立即执行函数不仅解决污染全局的问题,还通过模块注入解决模块之间的依赖问题
//index.js
//用括号包起来function,后面再加一个()再加一个分号,表示执行,约定俗成的写法
(function(moduleA,moduleB){
   console.log(moduleA.a)
   console.log(modeleB.b)
})();

但是ES5这种方法只能解决全局污染的问题,不能解决加载顺序的问题,在页面中必须按顺序引入

<body>
   <script src="./module_a.js"></script>
   <script src="./module_b.js"></script>
   <script src="./index.js"></script>
</body>

【补充】函数声明、函数表达式与立即执行函数

function test(){} //这是函数声明
var test = function(){} //这是函数表达式function(){}) //用括号包起来变成函数表达式

//只有是表达式的时候才能被执行符号执行,后面可以直接加执行符号(),表示立即执行function(){})();
var test = function(){}();//如果没有return 那么test为undefined,立即执行函数执行完立刻销毁

//函数声明变成函数表达式,前面 + - * / || && ,就可以接执行符号(),表示立即执行,并且忽略函数名
 + function test(){}()

一道面试题:

function test(a){
  console.log(a)
}(6,5)

//JS引擎会解析如下,并且逗号也是一种运算符,返回最后一个值,打印(6,5)结果为5
function test(a){console.log(a)}  //不执行函数,因为是函数声明,不是函数表达式65//括号里会被认为是表达式 

二、CommonJS

CommonJs是基于NodeJs上的模块化。不是通过HTML页面来导入模块,而是在JS文件中通过requiremodule.exports来实现导入导出模块

//moduleA.js
var a = (function(){  
   return [1,2,3,4,5].reverse()
})();

module.exports = {a}
//moduleB.js
var moduleA = require('./moduleA')

var b = (function(){ 
   return moduleA.a.concat([6,7,8])
})()

module.exports = {b}
//index.js
var moduleA = require('./moduleA')
var moduleB = require('./moduleB')

console.log(moduleA.a) 
console.log(moduleB.b)

然后页面只引用index.js即可

<body>
   <script src="./index.js"></script>
</body>

以上就是nodeJS的模块化规范 CommonJS

CommonJS特点

  • 通过require引入模块,通过module.exports导出模块(所有模块的加载都是同步进行的)
  • 使用缓存机制,引用一次模块,模块就会缓存,之后的每次引用都会比较文件内容是否修改,没有被修改就启用缓存
  • Node上运行 ,客户端运行不了(要搭建webpack才能使用CommonJs)
  • require、module并不是全局变量,引入进来相当于创建一个立即执行函数,创建一个独立的模块的作用域
(function(exports,require,module,__fireName,__dirName){})()

三 、AMD

因为ComonJS在客户端运行不了,客户端针对这个开发出AMD,通过RequireJS来实现AMD

定义模块用define,引入模块用require(要先配置路径)

//define第一个参数是模块名,第二个参数是引入其他的模块名(字符串数组,可传可不传),第三个参数是模块下要执行的函数
//moduleA.js
define('moduleA',function(){
   var a = [1,2,3]
   return {
       a:a.reverse()
   }
})
//moduleB.js
define('moduleB',['moduleA'],function(moduleA){
   var b = [1,2,3]
   return {
       b: moduleA.a.concat(b)
   }
})
//index.js
//先通过require.config配置路径,再通过require注入模块,第一个参数是要引入的模块(字符串数组),第二个参数是引入后要执行的函数
require.config({
  paths:{
     moduleA:'./module_a.js',
     moduleB:'./module_b.js'
  }
})

require(['moduleA','moduleB'],function(moduleA,moduleB){
    console.log(moduleA.a)
    console.log(moduleB.b)
})
<body>
   <script src="./require.js"></script> 
   <script src="./index.js"></script>
</body>

AMD特点

  • 基于CommonJS在客户端加载模块,定义模块用define,引入模块用require(要先配置路径)

define第一个参数是模块名,第二个参数是引入其他的模块名(字符串数组,可传可不传),第三个参数是模块下要执行的函数

  • 通过引用require.js来实现异步加载模块,并且要等所有模块执记载完毕之后,才执行require后面的回调函数(术语:依赖前置),这样就避免了浏览器中必须要顺序引入的问题,因为是要等所有模块加载完才执行函数

先通过require.config配置路径,再通过require注入模块,第一个参数是要引入的模块(字符串数组),第二个参数是引入后要执行的函数

四、CMD

与AMD类似,但本质上有所不同

用define定义模块,seajs.use使用模块

//module_a.js
define(function(require,exports,module){
  var a = [1,2,3,4]
  return {
      a: a.reverse()
  }
})
//module_b.js
define(function(require,exports,module){
  var moduleA = require('module_a')
  var b = [5,6,7]
  return {  //return 和 exports差不多 
      a: moduleA.a.concat(b)
  }
})
//index.js
//第一个参数是所需要的模块的路径,第二个参数是回调函数(依赖注入)
seajs.use(['module_a.js','module_b.js'],function(moduleA,moduleB){
   console.log(moduleA.a)
   console.log(moduleB.b)
})
<body>
   <script src="./sea.js"></script>
   <script src="./index.js"></script>
</body>

CMD和AMD的区别

  • CMD通过define定义模块,参数直接是一个函数,函数接受三个参数,require,exports,module,require是引入其他模块,exports(和return差不多)导出模块,也可以通过module.exports = xxx来导出模块

exports 仅仅是 module.exports 的一个引用。在 factory 内部给 exports 重新赋值时,并不会改变module.exports 的值。因此给 exports 赋值是无效的,不能用来更改模块接口。

AMDdefine定义模块,第一个参数是模块名,第二个参数是引入其他的模块名(字符串数组形式,可传可不传),第三个参数是模块下要执行的函数

  • 使用模块:

    • AMD使用模块的时候要配置模块的URL,先通过require.config配置路径,再通过require注入模块,第一个参数是要引入的模块(字符串数组),第二个参数是引入后要执行的函数;
    • CMD使用模块时seajs.use接收两个参数,第一个参数是模块路径(字符串数组),第二个参数是模块引入后执行的回调函数
  • 依赖加载:

    • AMD是依赖加载完毕之后执行回调函数
    • CMD是“依赖就近,按需加载”原则,当模块需要的才会加载,而不是模块加载的时候把所需要的全部模块加载完毕才执行回调函数

五、ES6

引入模块import ,导出模块export

//module_a.js
export default {
   a:[1,2,3,4].reverse()
}
//module_b.js
import moduleA from './module_a.js'
export default {
  b:moduleA.a.concat([5,6,7])
}
//index.js
import moduleA from './module_a.js'
import moduleB from './module_b.js'

console.log(moduleA.a)
console.log(moduleB.b)
<body>
   <script src="./index.js"></script>
</body>

CommonJS和ES6模块区别(2点)

  • CommonJS模块输出的是值的拷贝,而ES6模块输出的是值的引用
  • CommonJS模块是在运行时加载(CommonJS是运行在服务端,在程序运行的时候用require引进来的时候才会进行加载),ES6模块是编译时加载(本身是模块,但是浏览器不支持,需要webpack去编译)

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

// CommonJS模块
let { stat, exists, readfile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。

ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

// ES6模块
import { stat, exists, readFile } from 'fs';

上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。

参考资料:阮一峰ES6入门