UNDERSCORE.js 源码解析(一)

920 阅读8分钟

【写在前面】

最近萌生了写一个“源码系列”的想法,一则让自己静心下来做点事情,二是借由这个过程,在积累专业知识的同时,也把整个技术框架系统的扎起来。

之所以选择了一个工具库作为切入点,也并没有什么特殊的原因,只是因为它的复杂度适中,源码也只有不到2000行,容易下手而已。嗯,希望这个系列可以不断丰满下去。

毕竟,分享是一件让人快乐的事情。

【看这里】

阅读源码的过程中,如果有任何不容易理解的地方,可以和使用文档[underscorejs.org/]对比来看,会容易理解…

一、我们先来看一下UnderScore的整体结构

(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  typeof define === 'function' && define.amd ? define('underscore', factory) :
  (global = global || self, (function () {
    var current = global._;
    var exports = global._ = factory();
    exports.noConflict = function () { global._ = current; return exports; };
  }()));
}(this, (function () {
    ...
})));
//# sourceMappingURL=underscore.js.map

1、最外层是一个立即执行函数,接收两个参数

// wrapper部分
(function (global, factory)){}(A,B))

A=this, 对应形式参数global,在浏览器侧对应window对象,在服务端对应global对象;B=(function(){}), 是一个工厂方法,对应形式参数factory。

2、函数内部2~8行,定义了不同模块规范下,该模块的导出方式。

// 判断是否是commonJs
typeof exports === 'object' && typeof module !== 'undefined'
// 判断是否是amd
typeof define === 'function' && define.amd
// 其它情况
(global = global || self, (function () {
    var current = global._;
    var exports = global._ = factory();
    exports.noConflict = function () { global._ = current; return exports; };
}()) 

(1)前两行分别判断了运行环境是否为commonJS或者AMD,关于各种规范的区别和判断语句的原理,参看下面的【扩展阅读】章节。

(2)剩下的部分是一个最外层用()包裹的代码块,其中包含多条语句,语句间直接使用逗号间隔。也就是说括号内部的部分都会执行。立即执行函数内部,global.其实是在全局对象上声明了一个属性, 属性值即为factory方法返回的结果,也就是内部定义的方法集合。所以我们可以通过global._访问underscore内部的任意方法。

(3)exports.noConflict方法是将global._属性重置了,只返回了导出的方法集合,是为了应对多个库挂载冲突的问题。比如有个DH模块也把导出的方法挂载到了global._上,此时就和underscore产生了冲突。可以通过以下方法解决冲突:

global.__ = underscore.noConflict();   // 将underscore挂载到新的字段上

3、最后一行,用于指定sourceMap位置。可以关注webpack的打包方式,不同的的配置,产生的sourceMap方式不同。

二、再来看一下方法是怎样组织导出的

// Named Exports

var allExports = {
    __proto__: null,
    VERSION: VERSION,
    restArguments: restArguments,
    isObject: isObject,
    isNull: isNull,
    ...
    mixin: mixin,
    'default': _
};

// Default Export

// Add all of the Underscore functions to the wrapper object.
var _$1 = mixin(allExports);
// Legacy Node.js API.
_$1._ = _$1;

return _$1;

allExports包含了需要导出的全部方法,以k:v的形式指定了字段命名和映射关系。所有的方法经过mixin方法处理之后,返回一个对象,该对象的_属性指向自己,并作为factory方法的结果返回出去。

// Start chaining a wrapped Underscore object.
function chain(obj) {
    var instance = _(obj);
    instance._chain = true;
    return instance;
}

// Helper function to continue chaining intermediate results.
function chainResult(instance, obj) {
    return instance._chain ? _(obj).chain() : obj;
}

// Add your own custom functions to the Underscore object.
function mixin(obj) {
    each(functions(obj), function(name) {
      var func = _[name] = obj[name];
      _.prototype[name] = function() {
        var args = [this._wrapped];
        push.apply(args, arguments);
        return chainResult(this, func.apply(_, args));
      };
    });
    return _;
}

chain方法将传入的对象包装为链式调用的对象,并设置一个_chain的标志位,表示该对象已经被包装过。该方法是为了支持链式调用的场景。链式调用的原理也是比较简单的,一般是返回this本身, 或该对象的实例。

_.chain().each().map()

mixin方法是Underscore提供的可以扩展自定义方法的接口。很多工具库都提供了扩展自身的接口,类似JQuery的extend等。扩展的方法也基本都是mixin模式,即将一个对象的方法属性拷贝到另一个对象上。

这里,mixin方法做了几件事情。第一,将obj的方法拷贝到_对象上,同名的会被覆盖。第二,将obj的方法映射到_的原型对象上。之所以说是映射,并非是完全的拷贝,是因为设置在原型对象上的,是加了chainResult处理步骤的方法。chainResult其实就是提供了一个对链式调用的判断,如果当前对象被链式包装过,就继续调用chain方法进行链式调用的包装,否则返回当前对象。

// If Underscore is called as a function, it returns a wrapped object that can
// be used OO-style. This wrapper holds altered versions of all functions added
// through `_.mixin`. Wrapped objects may be chained.
function _(obj) {
    if (obj instanceof _) return obj;
    if (!(this instanceof _)) return new _(obj);
    this._wrapped = obj;
}

chain方法中的第一句,将obj传入了_方法进行了处理。_方法中,通过new的方式,生成了一个_的实例对象,而_的原型对象上又映射了所有的方法集合。所以返回的新对象可以通过链式调用的方式来进行方法调用。

// Add all of the Underscore functions to the wrapper object.
var _$1 = mixin(allExports);
// Legacy Node.js API.
_$1._ = _$1;

return _$1;

Underscore自己也是使用了mixin的方法,将所有内部方法都导出暴露在_对象和其原型上。

总结:Underscore的整体结构还是比较简单的,我们将在下一篇《UNDERSCORE.js 源码解析(二)》中来看一些工具库内部有意思的片段。

【扩展阅读】

一、AMD/CMD/CommonJS/UMD的区别

1、AMD(Asynchronous Module Definition,异步模块定义) [1]

define([module-name?], [array-of-dependencies?], [module-factory-or-object]);

只有一个define函数的范式定义。前两个参数是可选的,分别标识了模块名称和依赖关系。最后一个参数是必选项,可以是一个具体的对象,或者是一个函数方法。它的使用方式如下:

define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {
       exports.verb = function() {
           return beta.verb();
           //Or:
           return require("beta").verb();
       }
});

同时,规范还定义了每个遵循AMD规范的模块实现都应该提供了一个属性字段'amd',用来明确标识该模块是基于AMD规范实现的。因此,在一些工具库的实现中,我们常常看到使用这样的语句来判断当前环境是否支持AMD规范:

typeof define === 'function' && define.amd ?

当define函数中,没传入moudle-name参数时,该模块为一个匿名模块。此时,默认使用文件名作为模块名称。以大家较为熟悉的RequireJS为例,引入一个模块的方式如下:

// Consider "foo" and "bar" are two external modules
// In this example, the "exports" from the two modules
// loaded are passed as function arguments to the
// callback (foo and bar) so that they can similarly be accessed

require(["foo", "bar"], function ( foo, bar ) {
        // rest of your code here
        foo.doSomething();
});

require第一个参数声明了依赖的其它模块。当然,也可以动态的加载依赖:

define(function ( require ) {
    var isReady = false, foobar;

    // note the inline require within our module definition
    require(["foo", "bar"], function ( foo, bar ) {
        isReady = true;
        foobar = foo() + bar();
    });

    // we can still return a module
    return {
        isReady: isReady,
        foobar: foobar
    };
});

2、CMD(Common Module Definition, 通用模块定义)[4]

CMD是国内前端大佬玉伯在推广Sea.js时,提出的一种模块规范。AMD的实践者RequireJS曾经只支持依赖前置,CMD则支持依赖就近,这曾经被认为是AMD和CMD最明显的差异。但后来RequireJS也支持了依赖就近的写法。关于其它的一些对比,可以参考玉伯本人关于这个问题的回答[5]。CMD规范在后来的前端技术发展中逐渐没落,其具体内容参见[4],此处不再赘述。

3、CommonJS

正如CommonJS官方站点首页的Slogan(javascript: not just for browsers any more!)写到的那样,自从CommonJS和NodeJS诞生之后,JS在服务端和本地应用编程中开始大放异彩。

NodeJS作为基于CommonJS的优秀实现,已经成为服务端JS框架的事实标准。这里以它为例,简单介绍一下CommonJS的内容。NodeJS中定义了一个全局变量global,类似BOM中的window对象。在global上挂载了多个字段属性。其中module、require、exports与模块化相关,可以直接全局访问。

通常,module.exports用于制定模块导出的内容,可以使用require来进行访问。exports是对module.exports的更简短的引用形式。开发者可以在其上挂载任意方法和属性,但不可以对其进行赋值。一旦赋值,exports和module.exports之间的联系也就被人为阻断了。所以可以通过判断exports和module对象是否存在,来判断是否符合CommonJS规范:

typeof exports === 'object' && typeof module !== 'undefined'

4、UMD(Universal Module Definition,通用模块定义)

UMD[8]是一种兼容AMD和CommonJS的方式,通过检测运行环境对不同规范的支持,来使用不同的方式导出模块内容。正如文章开头时候展示的片段,Underscore.js以及许多工具库都是使用UMD的方式对外导出的。

二、关于Source Map

Source Map用来指定编译压缩之后的文件和源文件的对应关系,在开发调试和错误trace中有很大的帮助。一般可以使用两种方式启用source map:

// 1、在文件的最后一行,插入如下语句,一般是编译器工具自动插入的
//# sourceMappingURL=/path/xxxx.js.map

// 2、在压缩文件的请求响应中添加header字段
X-SourceMap: /path/to/script.js.map

在Webpack、UglifyJS等打包编译工具中,都暴露了一些选项用于调整source map的生成规则,来平衡压缩时间和调试需求。具体见[11]。

WX公众号同步更新

【相关链接】

[1] AMD规范定义: github.com/amdjs/amdjs…

[2] AMD解读: www.w3cschool.cn/zobyhd/fcyo…

[3] SeaJS: github.com/seajs/seajs

[4] CMD规范定义: github.com/cmdjs/speci…

[5] AMD和CMD的区别:github.com/seajs/seajs…

[6] CommonJS:www.commonjs.org/

[7] CommonJS Modules/1.1: wiki.commonjs.org/wiki/Module…

[8] UMD规范定义:github.com/umdjs/umd

[9] Source Map介绍:blog.teamtreehouse.com/introductio…

[10] Source Map MDN: developer.mozilla.org/en-US/docs/…

[11] Webpack Source Map: www.webpackjs.com/configurati…

[12] Function MDN: developer.mozilla.org/en-US/docs/…