> 原文:[http://yalishizhude.github.io](http://yalishizhude.github.io/2015/09/22/underscore-source/)
> 作者:[亞里士朱德](http://yalishizhude.github.io/about/)
> underscore 源碼版本 1.8.2
## 起因
很多人向我推薦研究js,可以看看一些第三方js類庫的源碼,而源碼之中最好解讀也最簡短的就是underscore,它也是我平常比較喜歡的一個庫,因為它性價比高:體積小、能力強。打開一看,才1000多行,試著讀了一下,確實很值得一看,所以對精彩部分做了一下整理。
## 閉包
整個函數在一個閉包中,避免污染全局變量。通過傳入this(其實就是window對象)來改變函數的作用域。和jquery的自執行函數其實是異曲同工之妙。這種傳入全局變量的方式一方面有利于代碼閱讀,另一方面方便壓縮。
underscore寫法:
~~~
(function(){
...
}.call(this));
~~~
jquery寫法:
~~~
(function(window, undefined) {
...
})(window);
~~~
## 原型賦值
~~~
18 var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
~~~
Array,Object,Function這些本質都是函數,獲取函數原型屬性prototype也是為了便于壓縮。簡單解釋一下,如果代碼中要擴展屬性,可能這樣寫
~~~
Object.prototype.xxx = ...
~~~
而這種代碼是不可壓縮的,`Object`,`prototype`這些名字改了瀏覽器就不認得了。
但是上面的代碼中創建了`ObjProto`之后,源生代碼經過壓縮之后,`ObjProto`就可能命名成a變量,那么原來的代碼就壓縮成
~~~
a.xxx = ...
~~~
一個小建議就是凡事一段代碼被使用兩次以上都建議定義變量(函數),有利于修改和壓縮代碼。
## 格式
~~~
29 var
nativeIsArray = Array.isArray,
nativeKeys = Object.keys,
nativeBind = FuncProto.bind,
nativeCreate = Object.create;
~~~
這種定義的方式省略了多余的var,格式也美觀,讓我想到了sublime中的一個插件alignment。
## 數據判斷
~~~
1194 _.isElement = function(obj) {
return !!(obj && obj.nodeType === 1);
};
~~~
判斷是否為dom,dom的nodeType屬性值為1。這里用`!!`強轉為boolean值
~~~
1200 _.isArray = nativeIsArray || function(obj) {
return toString.call(obj) === '[object Array]';
};
~~~
判斷是否為數組。由于Array.isArray函數是ECMAScript 5新增函數,所以為了兼容之前的版本,在原生判斷函數不存在的情況下,后面重寫了一個判斷函數。用call函數來改變作用域可以避免當obj沒有toString函數報錯的情況。
~~~
1205 _.isObject = function(obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj;
};
~~~
判斷是否為對象。先用typeof判斷數據類型。函數也屬于對象,但是由于typeof null也是object,所以用!!obj來區分這種情況。
~~~
1219 if (!_.isArguments(arguments)) {
_.isArguments = function(obj) {
return _.has(obj, 'callee');
};
}
~~~
判斷是否為arguments,很簡單,arguments有個特有屬性callee。
~~~
1239 _.isNaN = function(obj) {
return _.isNumber(obj) && obj !== +obj;
};
~~~
NaN這個值有兩個特點:1.它是一個數;2.不等于它自己。
‘+’放在變量前面一般作用是把后面的變量變成一個數,在這里已經判斷為一個數仍加上’+’,是為了把`var num = new Number()`這種沒有值的數字也歸為NaN。
~~~
1244 _.isBoolean = function(obj) {
return obj === true || obj === false || toString.call(obj) === '[object Boolean]';
};
~~~
是不是以為如果是布爾值不是true就是false?還有第3中情況`var b = new Boolean()`。b也是布爾值。
~~~
1254 _.isUndefined = function(obj) {
return obj === void 0;
};
~~~
用void 0來表示undefined,非常有意思的小技巧。不過常用方式還是if(xxx)來判斷是不是undefined。
`eq`是underscore的一個內置函數,代碼太長,不粘貼了。isEmpty調用了這個函數。整個思路由易到難,先用===比較簡單數據,然后用toString來判斷是否相等,最后用遞歸處理復雜的Array、Function和Object對象。
~~~
1091 if (a === b) return a !== 0 || 1 / a === 1 / b;
~~~
這里為了區分’+0’和’-0’,因為這兩個數對計算結果是有影響的。
~~~
1098 var className = toString.call(a);
if (className !== toString.call(b)) return false;
switch (className) {
// Strings, numbers, regular expressions, dates, and booleans are compared by value.
case '[object RegExp]':
// RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')
case '[object String]':
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
// equivalent to `new String("5")`.
return '' + a === '' + b;
case '[object Number]':
// `NaN`s are equivalent, but non-reflexive.
// Object(NaN) is equivalent to NaN
if (+a !== +a) return +b !== +b;
// An `egal` comparison is performed for other numeric values.
return +a === 0 ? 1 / +a === 1 / b : +a === +b;
case '[object Date]':
case '[object Boolean]':
// Coerce dates and booleans to numeric primitive values. Dates are compared by their
// millisecond representations. Note that invalid dates with millisecond representations
// of `NaN` are not equivalent.
return +a === +b;
}
~~~
這里是對簡單對象進行判斷,分為兩類,一類是`String`和`RegExp`,這種數據直接`toString`然后判斷。另一類是`Number`、`Date`和`Boolean`,通過轉換成數字判斷。
~~~
1150 aStack.push(a);
bStack.push(b);
if (areArrays) {
length = a.length;
if (length !== b.length) return false;
while (length--) {
if (!eq(a[length], b[length], aStack, bStack)) return false;
}
} else {
var keys = _.keys(a), key;
length = keys.length;
if (_.keys(b).length !== length) return false;
while (length--) {
key = keys[length];
if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;
}
}
aStack.pop();
bStack.pop();
~~~
對于數組和對象只能用遞歸了,同時用aStack和bStack來暫存遞歸中的子對象。這里一個小技巧的就是先判斷數組/屬性的長度,如果不相等可以有效地減少遞歸。