深入了解模块原理
如果你想详细地了解CommonJS的模块实现原理,请继续往下阅读。如果不想了解,请直接跳到最后做练习。
当我们编写JavaScript代码时,我们可以申明全局变量:
var s = 'global';
在浏览器中,大量使用全局变量可不好。如果你在 a.js 中使用了全局变量 s ,那么,在 b.js 中也使用全局变量 s ,将造成冲突, b.js 中对 s 赋值会改变 a.js 的运行逻辑。
也就是说,JavaScript语言本身并没有一种模块机制来保证不同模块可以使用相同的变量名。
那Node.js是如何实现这一点的?
其实要实现“模块”这个功能,并不需要语法层面的支持。Node.js也并不会增加任何JavaScript语法。实现“模块”功能的奥妙就在于JavaScript是一种函数式编程语言,它支持闭包。如果我们把一段JavaScript代码用一个函数包装起来,这段代码的所有“全局”变量就变成了函数内部的局部变量。
请注意我们编写的 hello.js 代码是这样的:
var s = 'Hello';
var name = 'world';
console.log(s + ' ' + name + '!');
Node.js加载了 hello.js 后,它可以把代码包装一下,变成这样执行:
(function () {
// 读取的hello.js代码:
var s = 'Hello';
var name = 'world';
console.log(s + ' ' + name + '!');
// hello.js代码结束
})();
这样一来,原来的全局变量 s 现在变成了匿名函数内部的局部变量。如果Node.js继续加载其他模块,这些模块中定义的“全局”变量 s 也互不干扰。
所以,Node利用JavaScript的函数式编程的特性,轻而易举地实现了模块的隔离。
但是,模块的输出 module.exports 怎么实现?
这个也很容易实现,Node可以先准备一个对象 module :
// 准备module对象:
var module = {
id: 'hello',
exports: {}
};
var load = function (module) {
// 读取的hello.js代码:
function greet(name) {
console.log('Hello, ' + name + '!');
}
module.exports = greet;
// hello.js代码结束
return module.exports;
};
var exported = load(module);
// 保存module:
save(module, exported);
可见,变量 module 是Node在加载js文件前准备的一个变量,并将其传入加载函数,我们在 hello.js 中可以直接使用变量 module 原因就在于它实际上是函数的一个参数:
module.exports = greet;
通过把参数 module 传递给 load() 函数, hello.js 就顺利地把一个变量传递给了Node执行环境,Node会把 module 变量保存到某个地方。
由于Node保存了所有导入的 module ,当我们用 require() 获取module时,Node找到对应的 module ,把这个 module 的 exports 变量返回,这样,另一个模块就顺利拿到了模块的输出:
var greet = require('./hello');
以上是Node实现JavaScript模块的一个简单的原理介绍。