浅析JS模块化

221 阅读2分钟

一、起源

1. 模块化是什么?有什么好处?

简单来说,就是将代码分割为多个部分,类似于函数对于逻辑的封装。我们将数据和方法私有化,只暴露部分属性的方法给外部调用的做法,称为模块化。
模块化可以提高代码的复用性和可维护性。

2. 基本的模块化

(1) 命名空间

var m1 = {
    m1_data: xxx,
    m1_fn: yyy,
}
var m2 = {
    m2_data: xxx,
    m2_fn: yyy,
}

可以看到通过命名空间虽然实现了基础的模块化,会但其副作用明显: 全局变量污染、模块内部的属性可以被篡改。

(2) IIFE

// module1.js
(function modular() {
    var data = 1;
    function fn() {
        // ...
    }
    function _fn() {
        // ...
    }
    window.module1 = {
        data,
        fn,
    }
})()
<!--html-->

<script src="./module1"></script>
<script>
    module1.fn();
</script>

通过IIFE的闭包来实现模块化是个不错的注意,保护了内部变量不被随意修改。
但是还是必须依赖全局变量,且脚本之间的依赖决定了维护script标签的顺序也是必要的,这在大型应用中(模块之间互相依赖的关系复杂)简直是灾难。
下面我们就来看看几个现在流行的模块化规范:

二、模块化规范

CJS

思想与特点

CJS将每个文件作为一个模块,有自己的模块作用域(IIFE)。

  1. CJS是运行时加载,Node环境不像浏览器环境有编译的过程。
  2. CJS模块的引入其实是其导出的浅拷贝,模块首次加载时会在当前module变量的exports挂载暴露的内容,对该模块的依赖,其实都是在访问该模块的module.exports属性值。

语法

模块通过exports属性挂载需要暴露的属性/方法
其他模块通过require方法引入,方法参数是模块路径

// module1.js
let primitiveData = 1;
let refData = {
  val: 1,
};
function handlePrimitiveData() {
  primitiveData++;
}
function handleRefData() { 
  refData.val++;
}
module.exports = {
  primitiveData,
  refData,
  handlePrimitiveData,
  handleRefData,
};

// main.js
const module1 = require('./module1.js');
module1.handlePrimitiveData();
module1.handleRefData();
console.log('=======>', module1.primitiveData); // 1
console.log(module1.refData, '<======='); // { val:2 }

module1模块首次被加载时,除了执行module1模块的代码,也会将module1暴露的内容挂载到module1的module.exports属性上,因为primitiveData是基本数据类型,所以实现的是值拷贝,即使我们通过handlePrimitiveData改变了模块内部的值,但是我们引入的永远只会是那份拷贝值。
从另一个角度来说,正因为refData是引用拷贝,所以我们通过handleRefData改变了模块内部的refData值后,还是可以通过引入的refData观察到。

ESM

思想与特点

ESM希望尽量的静态化,在编译时确定模块之间的依赖关系。

  1. ESM也用文件划分模块,模块之间基于引用完成依赖

语法

ESM模块通过export命令暴露数据,通过export.default暴露默认数据。
ESM通过import命令引入数据

// module1.js
export default {
    name: 'module1.js'
}
// main.js
import m1 from './module1';
console.log(m1.name); // 'module1.js'