Eloquent JavaScript 笔记 十: Modules
2017-06-12 16:46
471 查看
所谓模块化,就是把代码组织成一个个的模块,使各个代码块之间尽可能少的互相影响,即所谓 “高内聚、低耦合”,以便于后期的使用与维护。 namespace、class、function 等是一些常见的模块化的工具,它们会形成不同层次的作用域,把大的系统切分成小的模块。遗憾的是,js 没有提供namespace、class等层次的语法工具,只有 function才能构成独立的作用域,在function之外的变量都是全局变量。 所以,在其他语言中很简单、清晰的概念,在js中要实现相同的效果会显得有些怪异。
1. 全局变量局部化
给定一个index,得到星期几的英文单词:
很明显, names 这个变量是不必要的全局变量,把它局部化:
一开始,我觉得这么做有点小题大作,直接把 names 的定义放到 function 中就可以了,为啥还有再加一层function呢? 没错,但后面还会用到这个dayName,还会添加新的内容,所以,现在只看它的技术实现,先不考虑 “小题大做” 的问题。
这里的动作分城三步:
1. 定义一个匿名function;
2. 执行这个function;
3. 把返回值赋值给dayName。
返回值还是个function,功能与上面的代码相同。 外层的function只是创建了一个作用域,使names变成了局部变量。
再看一个例子:
计算100的平方,把结果打印到控制台。(这么少的代码做模块化没有实际意义,这里只是讲模块化方法。)
这段代码我看了好几遍才明白。 第一行和最后一行就是为了形成一个局部作用域,删掉它们,执行结果是一样的。
为什么要用匿名函数? 因为不需要函数名,我们只是为了把一段代码局部化,并不是真的需要创建一个函数。
为什么要把函数的定义用( )包起来? js语法要求必须这么写,反正不这么写就不行,执行会出错。 或者,可以问另外一个问题,单独定义一个匿名函数,不把它赋值给任何变量,会怎样?如下:
解释器报错,深层次的原因我至今没想明白,先记住就好了。
2. 对象作为接口
上面的dayName有一个接口:通过index得到星期几。我们再给他添加一个接口:通过星期几得到index。 当然,它自己也不能再叫dayName了,weekDay更精确。
1. 它是一个匿名对象;
2. 这个对象有两个属性:name,number;
3. 这两个属性都是function;
4. 使用方法:
weekDay.name(2);
weekDay.number("Sunday");
想一想,我们常用的Math模块,和它差不多。
如果输出的接口比较多,或者,函数代码比较长,这么写就不太好了,看下一种写法:
为了更精炼、显得更牛x,也为了给后面做铺垫,我们可以这么写:
1. 最后一行的 this,是指全局对象。这是js中的全局变量组织方式,所有的全局变量都是全局对象的一个属性。只要this不是写在某个对象的成员函数中,它就是指全局对象。
2. this.weekDay = {} 就是定义了一个叫weekDay的全局变量,并给它赋值一个空对象。 和这种写法一样: var weekDay = {} , 但这种写法不能放在函数参数中。
至此,模块化的基本原理就讲完了。着实的令人费解。这也可以看作是js的一个缺陷吧,如此简单的概念,实现起来却如此的别扭。
设想一下,如果有个程序员把上面的代码写到一个文件(weekday.js)中,提供给我,我的使用过程会是什么样的?
1. 在html中包含这个文件 <script src="weekday.js"></script>
2. 在我的js代码中调用 weekDay.number("Saturday");
weekDay 是在模块中定义的全局变量,如果有其他代码也用到了这个全局变量怎么办?如果有两个版本的weekDay同时在使用,怎么办? 那我只能去改动 weekday.js 了。 嗯,貌似有些问题。如果 weekday.js 是第三方提供的函数库呢? 作为使用者,去修改库文件,这肯定不是正途。接下来我们看看CommonJS是怎么做的。
3. 从js文件中加载模块
创建一个require函数,它的作用就是从js文件中加载模块。
假设我们有一个函数叫readFile(),它可以读取指定路径的js文件。后面的章节会讲解如何实现readFile(),这里我们先用着。
文件读出来之后是个字符串,如何把这个字符串当作代码来执行呢?
第一种方法,eval() :
第二种方法,构建一个Function对象:
好了,开始创建require函数:
使用方法:
4. 优化require()
上面的require函数有两个问题:
1. 一个模块可能重复加载,多次读取文件,多次创建Function对象,会导致性能问题;
2. 模块只能输出exports对象,如果该模块的接口只是一个函数呢?
优化后的require:
这里的cache很容易理解。 中间那个 module 有什么用呢?这涉及到函数参数传递的概念:值传递、引用传递。 比如,weekday.js 只需要输出一个函数接口,可以这么写:
而不能这么写:
这种方法就是大名鼎鼎的 CommonJS 。
最近在使用iScroll,看了一下源文件:
其中的 module.exports 看来就是用来支持CommonJS 加载方式的。
其中的 define.amd 是下一个小节要讲的。
5. Slow-loading Modules
CommonJS 从文件中加载模块,如果该文件在互联网上,下载速度比较慢,会导致运行require()的网页没有响应。要解决这个问题,需要使用AMD - Asynchronous Module Definition。 使用方法如下:
define()的第一个参数是个数组,包含所有需要加载的模块。当模块都加载完之后,执行第二个参数(回调函数)。
在我们的模块文件中(weekday.js),需要调用define函数:
下面我们看看define函数如何实现。
首先,我们需要一个 backgroundReadFile() 函数,它有两个参数:
1. filename, 需要加载的文件;
2. function, 加载完文件,立即调用该函数。
在第17章会讨论backgroundReadFile 的实现,现在先假设已经存在这个函数了。
然后,我们定义一个getModel函数,加载文件,并记录模块的加载状态:
这个过程太绕了,实在看不懂。先放一放,过几天回来再看。
1. 全局变量局部化
给定一个index,得到星期几的英文单词:
var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; var dayName = function(number) { return names[number]; };
很明显, names 这个变量是不必要的全局变量,把它局部化:
var dayName = function () { var names = ["Sunday", "Monday", "Tuesday", "Wednes 4000 day", "Thursday", "Friday", "Saturday"]; return function (number) { return names[number]; }; }();
一开始,我觉得这么做有点小题大作,直接把 names 的定义放到 function 中就可以了,为啥还有再加一层function呢? 没错,但后面还会用到这个dayName,还会添加新的内容,所以,现在只看它的技术实现,先不考虑 “小题大做” 的问题。
这里的动作分城三步:
1. 定义一个匿名function;
2. 执行这个function;
3. 把返回值赋值给dayName。
返回值还是个function,功能与上面的代码相同。 外层的function只是创建了一个作用域,使names变成了局部变量。
再看一个例子:
计算100的平方,把结果打印到控制台。(这么少的代码做模块化没有实际意义,这里只是讲模块化方法。)
(function() { function square(x) { return x * x; } var hundred = 100; console.log(square(hundred)); })();
这段代码我看了好几遍才明白。 第一行和最后一行就是为了形成一个局部作用域,删掉它们,执行结果是一样的。
为什么要用匿名函数? 因为不需要函数名,我们只是为了把一段代码局部化,并不是真的需要创建一个函数。
为什么要把函数的定义用( )包起来? js语法要求必须这么写,反正不这么写就不行,执行会出错。 或者,可以问另外一个问题,单独定义一个匿名函数,不把它赋值给任何变量,会怎样?如下:
function() { function square(x) { return x * x; } var hundred = 100; console.log(square(hundred)); }
解释器报错,深层次的原因我至今没想明白,先记住就好了。
2. 对象作为接口
上面的dayName有一个接口:通过index得到星期几。我们再给他添加一个接口:通过星期几得到index。 当然,它自己也不能再叫dayName了,weekDay更精确。
var weekDay = function() { var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; return { name: function(number) { return names[number]; }, number: function(name) { return names.indexOf(name); } }; }(); console.log(weekDay.name(weekDay.number("Sunday"))); // → Sunday怪异的语法,这是要把人逼疯了。 仔细看返回值:
1. 它是一个匿名对象;
2. 这个对象有两个属性:name,number;
3. 这两个属性都是function;
4. 使用方法:
weekDay.name(2);
weekDay.number("Sunday");
想一想,我们常用的Math模块,和它差不多。
如果输出的接口比较多,或者,函数代码比较长,这么写就不太好了,看下一种写法:
var weekDay = (function() { var exports = {}; var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; exports.name = function(number) { return names[number]; }; exports.number = function(name) { return names.indexOf(name); }; return exports; })();
为了更精炼、显得更牛x,也为了给后面做铺垫,我们可以这么写:
(function(exports) { var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; exports.name = function(number) { return names[number]; }; exports.number = function(name) { return names.indexOf(name); }; })(this.weekDay = {});执行效果是一样的。分析一下:
1. 最后一行的 this,是指全局对象。这是js中的全局变量组织方式,所有的全局变量都是全局对象的一个属性。只要this不是写在某个对象的成员函数中,它就是指全局对象。
2. this.weekDay = {} 就是定义了一个叫weekDay的全局变量,并给它赋值一个空对象。 和这种写法一样: var weekDay = {} , 但这种写法不能放在函数参数中。
至此,模块化的基本原理就讲完了。着实的令人费解。这也可以看作是js的一个缺陷吧,如此简单的概念,实现起来却如此的别扭。
设想一下,如果有个程序员把上面的代码写到一个文件(weekday.js)中,提供给我,我的使用过程会是什么样的?
1. 在html中包含这个文件 <script src="weekday.js"></script>
2. 在我的js代码中调用 weekDay.number("Saturday");
weekDay 是在模块中定义的全局变量,如果有其他代码也用到了这个全局变量怎么办?如果有两个版本的weekDay同时在使用,怎么办? 那我只能去改动 weekday.js 了。 嗯,貌似有些问题。如果 weekday.js 是第三方提供的函数库呢? 作为使用者,去修改库文件,这肯定不是正途。接下来我们看看CommonJS是怎么做的。
3. 从js文件中加载模块
创建一个require函数,它的作用就是从js文件中加载模块。
假设我们有一个函数叫readFile(),它可以读取指定路径的js文件。后面的章节会讲解如何实现readFile(),这里我们先用着。
文件读出来之后是个字符串,如何把这个字符串当作代码来执行呢?
第一种方法,eval() :
function evalAndReturnX(code) { eval(code); return x; } console.log(evalAndReturnX("var x = 2")); // → 2js本身提供了eval(),可以把执行字符串包含的代码。但这样有个缺点,我们无法预设字符串中包含了什么代码,是不是可能会有变量命名冲突?所以,最好是能把字符串包含的代码封闭在一个独立空间中,幸好js提供了Function对象。
第二种方法,构建一个Function对象:
var plusOne = new Function("n", "return n + 1;"); console.log(plusOne(4));使用Function构造函数可以创建一个普通的function,而function创建了一个独立的局部作用域。第一个参数是函数参数,如果有多个参数,用逗号分隔。第二个参数是函数体。
好了,开始创建require函数:
function require(name) { var code = new Function("exports", readFile(name)); var exports = {}; code(exports); return exports; } console.log(require("weekDay").name(1)); // → Monday脑补一下哈,readFile(name) 得到的就是上一小节的最后一段代码。好想不对,那段代码的最外层已经是function了,再加上 new Function 就有两层了。所以,weekday.js中的代码不用 (function() { }) () 包起来,如下:
var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; exports.name = function(number) { return names[number]; }; exports.number = function(name) { return names.indexOf(name); };这么写就可以了。
使用方法:
var weekDay = require("weekDay"); console.log(weekDay.name(today.dayNumber()));注意这里,require("weekDay") 中的weekDay 是文件名。 var weekDay = ... 中的weekDay是模块输出的对象名,我们可以任意给它命名,不需要去修改模块文件。
4. 优化require()
上面的require函数有两个问题:
1. 一个模块可能重复加载,多次读取文件,多次创建Function对象,会导致性能问题;
2. 模块只能输出exports对象,如果该模块的接口只是一个函数呢?
优化后的require:
function require(name) { if (name in require.cache) return require.cache[name]; var code = new Function("exports, module", readFile(name)); var exports = {}, module = {exports: exports}; code(exports, module); require.cache[name] = module.exports; return module.exports; } require.cache = Object.create(null);
这里的cache很容易理解。 中间那个 module 有什么用呢?这涉及到函数参数传递的概念:值传递、引用传递。 比如,weekday.js 只需要输出一个函数接口,可以这么写:
var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; module.exports = function(number) { return names[number]; };
而不能这么写:
exports = function(number) { return names[number]; };在require函数返回后,exports还是一个空对象。不能直接改变传入的对象exports,而改变了传入对象的属性,在reuire()返回之后还能带出来。
这种方法就是大名鼎鼎的 CommonJS 。
最近在使用iScroll,看了一下源文件:
(function (window, document, Math) { ... if ( typeof module != 'undefined' && module.exports ) { module.exports = IScroll; } else if ( typeof define == 'function' && define.amd ) { define( function () { return IScroll; } ); } else { window.IScroll = IScroll; } })(window, document, Math);
其中的 module.exports 看来就是用来支持CommonJS 加载方式的。
其中的 define.amd 是下一个小节要讲的。
5. Slow-loading Modules
CommonJS 从文件中加载模块,如果该文件在互联网上,下载速度比较慢,会导致运行require()的网页没有响应。要解决这个问题,需要使用AMD - Asynchronous Module Definition。 使用方法如下:
define(["weekDay", "today"], function(weekDay, today) { console.log(weekDay.name(today.dayNumber())); });相对于CommonJS 的require() ,AMD的加载函数是define()。
define()的第一个参数是个数组,包含所有需要加载的模块。当模块都加载完之后,执行第二个参数(回调函数)。
在我们的模块文件中(weekday.js),需要调用define函数:
define([], function() { var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; return { name: function(number) { return names[number]; }, number: function(name) { return names.indexOf(name); } }; });返回值就是该模块提供的接口。
下面我们看看define函数如何实现。
首先,我们需要一个 backgroundReadFile() 函数,它有两个参数:
1. filename, 需要加载的文件;
2. function, 加载完文件,立即调用该函数。
在第17章会讨论backgroundReadFile 的实现,现在先假设已经存在这个函数了。
然后,我们定义一个getModel函数,加载文件,并记录模块的加载状态:
var defineCache = Object.create(null); var currentMod = null; function getModule(name) { if (name in defineCache) return defineCache[name]; var module = {exports: null, loaded: false, onLoad: []}; defineCache[name] = module; backgroundReadFile(name, function(code) { currentMod = module; new Function("", code)(); }); return module; }
function define(depNames, moduleFunction) { var myMod = currentMod; var deps = depNames.map(getModule); deps.forEach(function(mod) { if (!mod.loaded) mod.onLoad.push(whenDepsLoaded); }); function whenDepsLoaded() { if (!deps.every(function(m) { return m.loaded; })) return; var args = deps.map(function(m) { return m.exports; }); var exports = moduleFunction.apply(null, args); if (myMod) { myMod.exports = exports; myMod.loaded = true; myMod.onLoad.forEach(function(f) { f(); }); } } whenDepsLoaded(); }
这个过程太绕了,实在看不懂。先放一放,过几天回来再看。
相关文章推荐
- Eloquent JavaScript 笔记 一、Values, Types, and Operators
- Eloquent JavaScript 笔记 三: Functions
- Eloquent JavaScript 阅读笔记一
- Eloquent JavaScript 笔记 二十:略有遗憾
- Eloquent JavaScript 笔记 四:Objects and Arrays
- Eloquent JavaScript 笔记 十一:A Programming Language
- Eloquent JavaScript 笔记 九: Regular Expressions(下)
- Eloquent JavaScript 笔记 十五:A Platform Game
- Eloquent JavaScript 笔记 五: High-Order Functions
- Eloquent JavaScript 笔记 八: Bugs and Error Handling
- Eloquent JavaScript 笔记 十三:DOM
- Eloquent JavaScript 笔记 七: Electronic Life
- Eloquent JavaScript 笔记 十四:Handling Event
- Eloquent JavaScript 笔记 前言:这是个艰难的决定
- Eloquent JavaScript 笔记 六:The Secret Life of Objects
- Eloquent JavaScript 笔记 十七:HTTP
- Eloquent JavaScript 笔记 十八:Forms and Form Fields
- Eloquent JavaScript 笔记 十二:Javascript and the Browser
- JavaScript中的正则表达式学习笔记
- JavaScript 学习笔记 2