一直想写一篇这样的文章,于是心动不如行动,这里选择的是 Underscore.js 1.8.3 版本,源码注释加在一起1625行。
Underscore.js 1.8.3
(c) 2009-2016 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors Underscore may be freely distributed under the MIT license.
这里我们首先看到的是一个闭包,概念不再熬述,诸君有意详勘闭包的概念,请移步 Closures。源码如下:
(function() {
这里如果这里有 this 那么一定是指向 window,即:
Window {external: Object, chrome: Object, document: document, speechSynthesis: SpeechSynthesis, caches: CacheStorage…}
window 具有的众多属性中就包含了 self 引用其自身,根据javascript的运算符执行顺序:
. [] () 字段访问、数组下标、函数调用以及表达式分组
++ -- - ~ ! delete new typeof void 一元运算符、返回数据类型、对象创建、未定义值
* / % 乘法、除法、取模
+ - + 加法、减法、字符串连接
<< >> >>> 移位
< <= > >= instanceof 小于、小于等于、大于、大于等于、instanceof
== != === !== 等于、不等于、严格相等、非严格相等
& 按位与
^ 按位异或
| 按位或
&& 逻辑与
|| 逻辑或
?: 条件
= 赋值、运算赋值
, 多重求值
var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global ||
this;
这里首先判断的是存在 self 或者 node 环境下的全局变量 global,然后复制给 root,作为根对象。
var previousUnderscore = root._;
previousUnderscore,从字面上理解就是“以前的 underscore”,说实话我并没理解这个赋值的用意,最开始以为是用来做判断全局 window是否已经存在 window._ 这个对象,然后通过判断 previousUnderscore 用来避免 window._ 污染 underscore 引起命名冲突,但是从头到尾只有一个地方用到了 previousUnderscore,即(1352行):
_.noConflict = function() {
root._ = previousUnderscore;
return this;
};
在外部可执行 var underscore_cache = _.noConflict();
用来重新定义 underscore 命名,很简单也很巧妙,noConflict 方法内将 root._
也就是 window._
重新定义为 previousUnderscore (previousUnderscore = undefined),而 noConflict 是_
的一个属性方法,所以 this 指向其自身(41行),即将 _
赋值给了 underscore_cache。
var _ = function(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};
var ArrayProto = Array.prototype, ObjProto = Object.prototype;
这两句很简单,就是将原生 JAVASCRIPT 的 Array 和 Object 对象的 prototype 缓存,这样做的好处是使用 push、slice、toString等方法的代码行数会减少、减少 JAVASCRIPT 遍历等等,更具体的介绍会在下面讲解,不要心急。
var SymbolProto = typeof Symbol !== 'undefined' ? Symbol.prototype : null;
2009年的 ES5 规定了六种语言类型:Null Undefined Number Boolean String Object,详见ES5/类型 和 ES5/类型转换与测试。新出台的 ES6 则规定,包括六种原始类型:Null Undefined Number Boolean String 和 Symbol,还有一种 Object,详见JavaScript 数据类型和数据结构。新增加的 Symbol 很早就已经提出,其具体概念这里不再复述请移步参考 Symbol ,得益于 ES6 的渐渐普及,客户端浏览器也有很多已经支持 Symbol,比如 Firefox v36+ 和 Chrome v38+ 等,具体参考 ES6 支持情况,如果大家对 ES6 想要深入了解可以看 ES6 In Depth 这篇文章和 ES6草案,说实话我的水平有限这份草案还没有读懂(+﹏+),如果想要进一步为 ES6 普及贡献自己的力量 ES6 WIKI 的编写是一个蛮好的选择。
回归正题,上述代码的目的显而易见就是判断客户端是否支持 Symbol,支持则缓存 Symbol.prototype 原型链,不支持则赋值为 Null,三元运算符的灵活运用是判断一个人语言到达一个阶段的标识,这句话有点武断,但是算的上肺腑之言,要熟悉且灵活运用它。
var push = ArrayProto.push,
slice = ArrayProto.slice,
toString = ObjProto.toString,
hasOwnProperty = ObjProto.hasOwnProperty;
这里是简单缓存了 push、slice、toString、hasOwnProperty 四个方法。
var nativeIsArray = Array.isArray,
nativeKeys = Object.keys,
nativeCreate = Object.create;
这里就比较有意思了,Array.isArray(element) 是 ES5 后来新增的静态函数,用来判断一个对象是不是数组,具体描述可见 Array.isArray() 和 Array.isArray 函数 (JavaScript):https://msdn.microsoft.com/zh-cn/library/ff848265(v=vs.94).aspx
,我一点都不喜欢微软,就比如现在我想粘一个微软的网址,但是它的网址里面居然有()
,以至于我必须把网址贴到代码框里才能保证不出现错误ヽ(ˋДˊ)ノ。Object.keys 用于返回一个由给定对象的所有可枚举自身属性的属性名组成的数组,Object.keys()。Object.create 用于创建一个拥有指定原型和若干个指定属性的对象,这一系列的函数方法都可以在 Object 处了解详情。同时这里面有些内容可以参考 Annotated ECMAScript 5.1,有兴趣的同学可以看一看,雾里探花,蛮有趣的。
var Ctor = function(){};
ctor 英文译为男星,或者我的百度翻译打开方式不对,翻译错了???,实际上就是一个空的方法,这种写法很常见,一般用于和 call、apply、argument 等配合使用,在 Underscore.js 中作者并没有上述的用法,只是用 Ctor 这个函数扩展了自身的 prototype,将一些函数方法绑定到自身作为一个 return function,具体细节后面接触到再详述。
var _ = function(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};
定义 _
对象,作者的备注是”Create a safe reference to the Underscore object for use below.“,这里我们了解到 _
本身是一个函数,而在 JAVASCRIPT 中函数本身就是对象的一种,所以 Underscore.js 的一系列函数都是作为对象函数绑定到 _
这个函数对象上面的,上面这个函数默认传入一个 obj 参数,可以通过 _(obj)
用来校验 _
是否是 obj 的父类型以此判断继承关系,instanceof的用法详见 JavaScript instanceof 运算符深入剖析,至于 _wrapped
涉及到后面的链式操作,在(887行)一起讲。
if (typeof exports != 'undefined' && !exports.nodeType) {
if (typeof module != 'undefined' && !module.nodeType && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root._ = _;
}
这是 Node.js 中对通用模块的封装方法,通过对判断 exports 是否存在来决定将局部变量 _ 赋值给exports,顺便说一下 AMD 规范、CMD规范和 UMD规范,Underscore.js 是支持 AMD 的,在源码尾部有定义,这里简单叙述一下:
amd:AMDJS
define(['underscore'], function (_) {
//todo
});
cmd:Common Module Definition / draft、CMD 模块定义规范
var _ = require('underscore');
module.exports = _;
另一种常见的写法:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(['underscore'], factory);
} else if (typeof exports === 'object') {
module.exports = factory(require('underscore'));
} else {
root.returnExports = factory(root._);
}
}(this, function ($) {
//todo
}));
_.VERSION = '1.8.3';
underscore 版本为 '1.8.3'。
var optimizeCb = function(func, context, argCount) {
if (context === void 0) return func;
switch (argCount == null ? 3 : argCount) {
case 1: return function(value) {
return func.call(context, value);
};
case 3: return function(value, index, collection) {
return func.call(context, value, index, collection);
};
case 4: return function(accumulator, value, index, collection) {
return func.call(context, accumulator, value, index, collection);
};
}
return function() {
return func.apply(context, arguments);
};
};
optimizeCb 翻译成汉语就是优化回调(optimize callback),那么 optimizeCb 是如何优化的呢,我们可以首先看到它传入了三个参数,分别为:func、context、argCount,语义化可知一个是将要优化的 callback function,一个是 context 上下文函数,最后 argCount 是一个 number 类型的数字。void 0
的用法很巧妙,这里用 context === void 0
判断是否存在上下文环境,也就是第二个参数,其他的一些关于 void 的用法详见 谈谈Javascript中的void操作符。接下来判断 argCount 数字进行相应的操作,其中有 call 和 apply 两个方法,详见 Function.prototype.apply() 和 Function.prototype.call()。