underscore源码解读之模块化

大家在阅读一些源码的过程中,经常会遇到类似这样的结构代码

;(function(){
   // ...
   function MyModule() {
      // ...
  }
  // ...
  if(typeof module !== `undefined` && typeof exports === `object`) {
      module.exports = MyModule;
  }
  // ...
  if(typeof define === `function` && define.amd) {
      define(function() { return MyModule; });
  } 
   // ...
})()

想必大家或多或少,无非就是一些什么闭包、模块化之类的作用,下面就此展开解读。

首先介绍什么是模块化

在封装插件之前,需要考虑什么问题?

  1. 如何安全的包装一个模块的代码?(不污染模块外的任何代码)
  2. 如何唯一标识一个模块?
  3. 如何优雅的把模块的API暴漏出去?(不能增加全局变量)
  4. 如何方便的使用所依赖的模块?

解决这些问题的同时,产生了CommonJS和AMD、CMD等,其中规范以前两种为主。下面简单介绍用法及区别。

CommonJS

Node中采用了CommonJS,模块引用使用require,如下:

var http = require('http');

AMD

AMD 即Asynchronous Module Definition,中文名是“异步模块定义”的意思。它是一个在浏览器端模块化开发的规范,服务器端的规范是CommonJS。

模块将被异步加载,模块加载不影响后面语句的运行。所有依赖某些模块的语句均放置在回调函数中。
AMD 是 RequireJS 在推广过程中对模块定义的规范化的产出。

其模块引用方式如下:

define(id?,dependencies?,factory);

其中,id及依赖是可选的。其与CommonJS方式相似的地方在于factory的内容就是实际代码的内容,下面是一个简单的例子:

define(function(){
  var exports = {};
  exports.say = function(){
    alert('hello');
  };
  return exports;
});

CMD

CMD规范,与AMD类似,区别主要在于定义模块和依赖引入的地方。主要实现比如: SeaJS。

define(['dep1','dep2'],function(dep1,dep2){
  return function(){};
});

require、exports、module通过形参传递给模块,在需要依赖模块时,随时调用require引入即可,实现按需加载。

AMD:提前执行(异步加载:依赖先执行)+延迟执行
CMD:延迟执行(运行到需加载,根据顺序执行)

CMD 推崇依赖就近,AMD 推崇依赖前置。看如下代码:

// CMD
define(function(require, exports, module) {
  var a = require('./a')
  a.doSomething()
  // ...
  var b = require('./b') // 依赖可以就近书写
  b.doSomething()
  // ... 
})
 
// AMD 
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
  a.doSomething()
  // ...
  b.doSomething()
  // ...
})

下面分析underscore源码中加载模块化相关部分

(function() {
  var _ = function(obj) {
    if (obj instanceof _) return obj;
    if (!(this instanceof _)) return new _(obj);
    this._wrapped = obj;
  };
  // ...
  if (typeof exports !== 'undefined') {
    if (typeof module !== 'undefined' && module.exports) {
      exports = module.exports = _;
    }
    exports._ = _;
  } else {
    root._ = _;
  }
  // ...
  if (typeof define === 'function' && define.amd) {
    define('underscore', [], function() {
        return _;
    });
  }
}).call(this);

1. 通过function定义构建了一个闭包,call(this)是将function在this对象下调用,以避免内部变量污染到全局作用域。浏览器中,this指向的是全局对象(window对象),将“”变量赋在全局对象上“root.”,以供外部调用。

2. 另外还有一个常用知识点,在这里没有出现。

;(function (){}())
!(function (){})()

立即执行函数前面放一个";"或者"!"是干嘛用的?看到下面这个例子你就知道了

var aa = function (){console.log('我是无辜的')}
(function(){console.log("立即输出")}())

// 看下面代码与上面代码不同之处
var aa = function (){console.log('我是无辜的')};
(function(){console.log("立即输出")}())

看到运行结果你有什么想法?发现,上面的代码被当做了一条语句运行了,也就是
var aa = function (){console.log('我是无辜的')}(function(){console.log("立即输出")}())

这就是为什么要加";"或者"!"的原因,防止上条执行语句没有分号影响下面语句执行

3. 如下代码:

if (typeof exports !== 'undefined') {
    if (typeof module !== 'undefined' && module.exports) {
      exports = module.exports = _;
    }
    exports._ = _;
  } else {
    root._ = _;
  }

首先判断是否支持exports导入方式,typeof module !== 'undefined' && module.exports这句代码判断CommentJS与CMD,如果加上define.cmd则只支持CMD
对于exports = module.exports = _;exports._ = _;这两句我是这么理解的。exports._ = _;是为了兼容老的写法。对于那些进入if条件已经执行完exports = module.exports = _;的情况,当执行到exports._ = _;时,并不会生效。

下面介绍下exportsmodule.exports的区别:
1. exports是module.exports的辅助方法。模块最终返回module.exports给调用者,而不是exports。
2. exports所做的事情是收集属性,如果module.exports当前没有任何属性的话,exports会把这些属性赋予module.exports。
3. 如果module.exports已经存在一些属性的话,那么exports中所用的东西都会被忽略。

// aa.js
module.exports = '测试';
exports.name = function() {
    console.log('My name is XX');
};

// 下面引入aa.js文件
var aa= require('./aa.js');
aa.name(); // TypeError: Object 测试 has no method 'name'

aa模块完全忽略了exports.name,然后返回了一个字符串'测试'。通过上面的例子,你可能认识到你的模块不一定非得是模块实例(module instances)。可以是任何合法的JavaScript对象 - boolean,number,date,JSON, string,function,array和其他。模块可以是任何你赋予module.exports的值。如果你没有明确的给module.exports设置任何值,那么exports中的属性会被赋给module.exports中,然后并返回它。

4. 如下代码

if (typeof define === 'function' && define.amd) {
        define('underscore', [], function() {
            return _;
        });
    }

只是为了兼容加载AMD 规范,几乎每个插件源码都是这样引入,固定格式

结尾

以上只是我对对underscore源码阅读的一点见解,或许也会有理解错误的地方,欢迎指正。在doT.js的源码中同样的模块功能中发现了一些不同之处,很有意思,大家可以思考下这种使用方式。代码如下:

(function(){
   // ...
   (function(){ return this || (0,eval)('this'); }()).doT = doT;

}())

1. 这里的(0,eval)("this")什么意思?为什么不用window.doT或者eval('this')
2. 另外(function(){}())(function(){})()又有什么区别呢

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容