暑假打算研究一下 Underscore 源码,我会对一些我觉得比较有意思的点拿出来讨论下,不过我不会过多介绍,也不会去分析 Underscore 的各个方法,但我会附上一些相关的不错的参考资料。由于我也是初学阶段,所以如果有说的不正确的地方望指出。

要点1:立即执行函数

Underscore 的内容都用这么一个东西包装起来了。

1
(function(){..}());

其实也可以这样写

1
(function(){})();

Underscore 把全部内容封装在立即执行函数里面,就形成了一个独立的作用域,与外部隔离,并且这样做还形成了闭包,可以模拟私有方法。
推荐阅读:

要点2:兼容浏览器和 Node 环境

浏览器和服务端的一个主要区别是全局对象命名的不同,在浏览器全局变量是 window,在服务端即 Node 环境则是 global。

1
2
3
var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global ||
this;

这个地方之前的写法是这样的

1
var root = this;

我认为之所以改成前面那种写法,可能是为了确保 root 指向 global 或者 window(self) 。大部分框架和库都采用这种做法,这种做法更加安全。

1
2
3
4
5
6
7
// 这一步确保self是一个object,这样self.self才不会出错
typeof self == 'object'
// 这一步确保self.self严格等于自身,貌似只有window具备这个特性
// 即window === window.window.window
self.self === self
// 为什么还要进行这一步?
self

推荐阅读:

要点3:提供命名冲突解决方法

Underscore 在给 root 赋值前,先保存了原先 root 的 _ 对象。之所以这样做,是因为可能我们用的其他库也使用了 _ 这个作为命名空间。

1
var previousUnderscore = root._;

我们结合 Underscore 最下面的这个方法来看。

1
2
3
4
_.noConflict = function() {
root._ = previousUnderscore;
return this;
};

如果 _ 出现了冲突,可以使用下面方法

1
var _new = _.noConflict();

这样一来应该很明显了,noConflictpreviousUnderscore 即原先的 root._ 重新放回去,然后重新定义 Underscore 命名给 _new,这样就解决了 _ 冲突问题。

要点4:考虑压缩问题

1
2
3
4
5
6
var ArrayProto = Array.prototype, ObjProto = Object.prototype;
var SymbolProto = typeof Symbol !== 'undefined' ? Symbol.prototype : null;
var push = ArrayProto.push,
slice = ArrayProto.slice,
toString = ObjProto.toString,
hasOwnProperty = ObjProto.hasOwnProperty;

这里首先说一下Symbol,ES5 规定了六种语言类型即 Null, Undefined, Number, Boolean, String, Object,而新出台的 ES6 则新增了 Symbol。关于 Symbol 查看推荐阅读。
这里把 Array 和 Object 和 Symbol 的原型都用变量来引用的原因是变量可以进行压缩,如果有使用过代码压缩工具的话,一个常见的压缩技巧就是用短变量名代替长变量名,而对于一些出现频率高的方法我们可以用变量来进行引用来便于压缩。
而 push, slice, toString, hasOwnProperty 这些引用不仅便于压缩,还可以减少在原型链中的查找次数,提高速度。即直接在原型上操作,避免原型链查找。关于原型链是个大问题了,这里不打算过多讲解(其实是我也不怎么懂),下一篇再进行介绍。
还有一个地方顺便提一下的是,Underscore 的源码不使用 undefined 而是使用 void 0。这个也是代码压缩的时候会做的事情。虽然从 ES5 开始 undefined 是全局对象的只读属性不能重写,但是在局部作用域中仍然可以被重写,而 void 运算符能对给定的表达式进行求值,然后返回 undefined,可以保证返回的是 undefined,void不能重写。再者,之所以跟的是0,只是因为0短并且习惯问题而已。
推荐阅读:

要点5:区别apply, call和bind

在 Underscore 源码中我们会经常看到 apply 和 call 的应用。例如:

1
2
3
4
5
6
7
8
9
10
11
switch (startIndex) {
case 0: return func.call(this, rest);
case 1: return func.call(this, arguments[0], rest);
case 2: return func.call(this, arguments[0], arguments[1], rest);
}
var args = Array(startIndex + 1);
for (index = 0; index < startIndex; index++) {
args[index] = arguments[index];
}
args[startIndex] = rest;
return func.apply(this, args);

apply 和 call 都是为了改变某个函数运行时的 context 即上下文而存在的,即改变函数提内部 this 的指向,他们的功能是相同的,只是用法稍有不同。

1
2
3
4
var func1 = function(arg1, arg2) {};
// 可以通过下面两个方法来调用
func1.call(this, arg1, arg2);
func1.apply(this, [arg1, arg2]);

即 call 传递的是参数列表,而 apply 传递的是数组,当我们知道参数的数量时使用 call 方法,不知道参数数量时可以把参数放到一个数组然后使用 apply 方法调用。bind 也可以更改函数执行的上下文但是不同的是,bind 只进行绑定不会立即调用。
再看看上面 Underscore 源码中的一部分,对于startIndex小于3的情况,他分别使用了 call 方法调用,而当startIndex大于3的时候,则将参数转为数组形式使用 apply 方式调用。为什么不直接用 apply 方法呢?
简而言之,apply 比 call 慢。
对于 apply 方法,Function 会检查传入的参数的类型是否符合要求,还要进行解构操作等等。所以应该尽量使用 call 方法。
Underscore 源码关于这段代码还有个地方值得注意

1
startIndex = startIndex == null ? func.length - 1 : +startIndex;

为什么使用 +startIndex
+运算符尝试将后面的数转为数字,例如将字符串(“123”)转为数(123),对数字不会产生影响,如果传入(“123NASD”),会得到 NaN 。
推荐阅读: