由于 Js 起初定位的原因(刚开始没想到会应用在过于复杂的场景),所以它本身并没有提供模块系统,随着应用的复杂化,模块化成为了一个必须解决的问题。本着菲麦深入原理的原则,很有必要来揭开模块化的面纱
一、模块化需要解决的问题
要对一个东西进行深入的剖析,有必要带着目的去看。模块化所要解决的问题可以用一句话概括
在没有全局污染的情况下,更好的组织项目代码
举一个简单的栗子,我们现在有如下的代码:
function doSomething () {
const a = 10;
const b = 11;
const add = function (a + b) {
return a + b
}
add (a + b)
}
在现实的应用场景中,doSomething 可能需要做很多很多的事情,add 函数可能也更为复杂,并且可以复用,那么我们希望可以将 add 函数独立到一个单独的文件中,于是:
// doSomething.js 文件
const add = require('add.js');
const a = 10;
const b = 11;
add(a+ b);
// add.js 文件
function add (a, b) {
return a + b;
}
module.exports = add;
这样做的目的显而易见,更好的组织项目代码,注意到两个文件中的 require 和 module.exports,从现在的上帝视角来看,这出自 CommonJS 规范(后文会有一个章节来专门讲规范)中的关键字,分别代表导入和导出,抛开规范而言,这其实是我们模块化之路上需要解决的问题。另外,虽然 add 模块需要得到复用,但是我们并不希望在引入 add 的时候造成全局污染
二、引入的模块如何运行
在上述的例子中,我们已经将代码拆分到了两个模块文件当中,在不造成全局污染的情况下,如何实现 require,才能使得例子中的代码做到正常运行呢?
先不考虑模块文件代码的载入过程,假设 require 已经可以从模块文件中读取到代码字符串,那么 require 可以这样实现
function require (path) {
// lode 方法读取 path 对应的文件模块的代码字符串
// let code = load(path);
// 不考虑 load 的过程,直接获得模块 add 代码字符串
let code = 'function add(a, b) {return a+b}; module.exports = add';
// 封装成闭包
code = `(function(module) {$[code]})(context)`
// 相当于 exports,用于导出对象
let context = {};
// 运行代码,使得结果影响到 context
const run = new Function('context', code);
run(context, code);
//返回导出的结果
return context.exports;
}
这有几个要点:
1) 为了不造成全局污染,需要将代码字符串封装成闭包的形式,并且导出关键字 module.exports ,module 是与外界联系的唯一载体,需要作为闭包匿名函数的入参,与引用方传入的上下文 context 进行关联
