Eloquent JavaScript 笔记 十: Modules

来源:互联网 发布:屏蔽一段ip的js代码 编辑:程序博客网 时间:2024/06/05 12:49

所谓模块化,就是把代码组织成一个个的模块,使各个代码块之间尽可能少的互相影响,即所谓 “高内聚、低耦合”,以便于后期的使用与维护。 namespace、class、function 等是一些常见的模块化的工具,它们会形成不同层次的作用域,把大的系统切分成小的模块。遗憾的是,js 没有提供namespace、class等层次的语法工具,只有 function才能构成独立的作用域,在function之外的变量都是全局变量。 所以,在其他语言中很简单、清晰的概念,在js中要实现相同的效果会显得有些怪异。


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", "Wednesday", "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"));// → 2
js本身提供了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();}

这个过程太绕了,实在看不懂。先放一放,过几天回来再看。




原创粉丝点击