Eloquent JavaScript 笔记 十: Modules

所谓模块化,就是把代码组织成一个个的模块,使各个代码块之间尽可能少的互相影响,即所谓 “高内聚、低耦合”,以便于后期的使用与维护。 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();
}

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




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值