由于 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 进行关联