目錄
說(shuō)明
underscore是一個(gè)很有用的javaScript工具庫(kù)丁寄,對(duì)函數(shù)式編程提供很多方法涮帘,所以讀源碼很適合了解函數(shù)式編程顶瞳,筆者讀的是1.8.3版独令,代碼和注釋都為手寫摔桦,又不懂得也參考了網(wǎng)上資料社付,然后用了es6的let代替了var,等讀完會(huì)將封裝過(guò)的js文件放到github上:
_.VERSION = '1.8.3';
立即執(zhí)行函數(shù)
underscore的最外層是一個(gè)立即執(zhí)行函數(shù)酣溃,所有內(nèi)容都放在函數(shù)內(nèi)部瘦穆,利用的思想是閉包。
(function ( ) {...}) ()
形成一個(gè)獨(dú)立的作用域赊豌,這樣的好處是可以不污染全局扛或,也可以防止其他因素對(duì)內(nèi)部函數(shù)的影響
全局變量聲明
let root = (
(typeof self == 'object') &&
// self表示window窗口自身,這是瀏覽器環(huán)境下的全局命名空間
(self.self === self) &&
// 如果存在self碘饼,判斷self是否是自身引用熙兔,即window這一對(duì)象
(self)
// 如果以上都滿足,說(shuō)明全局對(duì)象是window艾恼,并返回window作為root住涉,這里self即window
) ||
(
(typeof global == 'object') &&
// global表示node環(huán)境下全局的命名空間
(global.global === global) &&
// 如果存在gloabl,判斷global是否是自身引用
(global)
// 如果以上都滿足钠绍,說(shuō)明全局對(duì)象是global舆声,并返回global作為root
) ||
(this);
// 如果以上兩者都不是,直接返回this,這里應(yīng)該處理既不是window這一瀏覽器環(huán)境,也不是global這一node環(huán)境的
該段代碼主要用于確認(rèn)環(huán)境的全局命名空間媳握,并賦值給變量root碱屁。
值得注意的是,網(wǎng)上很多源碼分析給出的是
var root = this;
但在1.8.3版本中查看了github上的源碼蛾找,發(fā)現(xiàn)它是最上面的封裝方式娩脾,這樣的好處有:
1、向前兼容嚴(yán)格模式打毛,在嚴(yán)格模式下直接使用this會(huì)得到undefined柿赊。這是因?yàn)閑cma262第5版中,為了防止人們誤將非new出來(lái)的對(duì)象的this指向全局命名空間幻枉,特地將之設(shè)置為undefined碰声。
2、用來(lái)支持WebWorker展辞,在WebWorker里可以使用self但不能使用window.
另一個(gè)值得注意的地方是:
(typeof self == 'object')
這里用的是==奥邮,而不是===。==和===的區(qū)別是一個(gè)不完全等罗珍,一個(gè)是全等洽腺,用==的好處是可以進(jìn)行轉(zhuǎn)義,因?yàn)槿置臻g不一定完全等價(jià)為對(duì)象覆旱,這跟瀏覽器的實(shí)現(xiàn)有關(guān)蘸朋。
避免命名沖突
let previousUnderscore = root._; // 將全局變量中的變量
_賦值給變量previousUnderscore進(jìn)行緩存
previousUnderscore,從字面上理解就是“以前的 underscore”扣唱,最開始不明白這條語(yǔ)句的用意藕坯,但是從頭到尾只有一個(gè)地方用到了 previousUnderscore,即(1352行):
_.noConflict = function() {
root._ = previousUnderscore;
return this;
};
// 使用noConflict方法返回自身
如果發(fā)生沖突噪沙,可以用例如
var underscore_cache = _.noConflict();
用來(lái)重新定義 underscore 命名.
_(初始化)
let _ = function (obj) {
if (obj instanceof _){
return obj;
}
// 如果obj已經(jīng)是·_·函數(shù)的實(shí)例炼彪,則直接返回obj
if (!(this instanceof _)) {
return new _(obj);
}
// 如果不是`_`函數(shù)的實(shí)例
// 則調(diào)用new運(yùn)算符,返回實(shí)例化對(duì)象
this._wrapped = obj;
// 將obj賦值給this._wrapped屬性
};
其實(shí)核心函數(shù)——
_
其實(shí)是一個(gè)構(gòu)造函數(shù)正歼,支持無(wú)new調(diào)用的構(gòu)造函數(shù)辐马。當(dāng)使用函數(shù)式風(fēng)格的代碼時(shí)并不會(huì)有太大影響,但使用面向?qū)ο箫L(fēng)格的代碼時(shí)局义,這里省去了使用者調(diào)用new的麻煩喜爷。
node服務(wù)端中:
if (typeof exports != 'undefined' && !exports.nodeType) {
if (typeof module != 'undefined' && !module.nodeType && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root._ = _;
}
這是 Node.js 中對(duì)通用模塊的封裝方法,通過(guò)對(duì)判斷 exports 是否存在來(lái)決定將局部變量 _ 賦值給exports萄唇,順便說(shuō)一下 AMD 規(guī)范檩帐、CMD規(guī)范和 UMD規(guī)范,Underscore.js 是支持 AMD 的另萤,在源碼尾部有定義湃密,這里簡(jiǎn)單敘述一下:
amd:AMDJS
define(['underscore'], function (_) {
//todo
});
var _ = require('underscore');
module.exports = _;
值得一提的是使用nodeType來(lái)確保exports和module并不是HTML的元素。
保存原型
let ArrayProto = Array.prototype,
ObjProto = Object.prototype,
SymbolProto = ((typeof(Symbol)) !== ("undefined")) ? (Symbol.prototype) : (null);
// 緩存變量泛源,減少代碼量揍障,便于壓縮代碼
let push = ArrayProto.push,
slice = ArrayProto.slice,
// slice方法,可以從數(shù)組中返回選定的元素
toString = ObjProto.toString,
hasOwnProperty = ObjProto.hasOwnProperty;
// 緩存變量俩由,減少代碼量,便于壓縮代碼
// 同時(shí)可減少在原型鏈中的查找次數(shù)(提高代碼效率)
let nativeIsArray = Array.isArray,
nativeKeys = Object.keys,
// 返回一個(gè)給定對(duì)象的所有可枚舉屬性的字符串?dāng)?shù)組
nativeCreate = Object.create;
// 可以調(diào)用create方法來(lái)創(chuàng)建對(duì)象癌蚁,對(duì)象的原型就是create方法的第一個(gè)參數(shù)
//ES5原生方法幻梯,如果瀏覽器支持,則underscore中會(huì)優(yōu)先使用
值得一提的是:
var SymbolProto = typeof Symbol !== 'undefined' ? Symbol.prototype : null;
2009年的 ES5 規(guī)定了六種語(yǔ)言類型:Null Undefined Number Boolean String Object努释,詳見ES5/類型 和 ES5/類型轉(zhuǎn)換與測(cè)試碘梢。新出臺(tái)的 ES6 則規(guī)定,包括六種原始類型:Null Undefined Number Boolean String 和 Symbol伐蒂,還有一種 Object煞躬,詳見JavaScript 數(shù)據(jù)類型和數(shù)據(jù)結(jié)構(gòu)。新增加的 Symbol 很早就已經(jīng)提出逸邦,其具體概念這里不再?gòu)?fù)述請(qǐng)移步參考 Symbol 恩沛,得益于 ES6 的漸漸普及,客戶端瀏覽器也有很多已經(jīng)支持 Symbol缕减,比如 Firefox v36+ 和 Chrome v38+ 等雷客,具體參考 ES6 支持情況,如果大家對(duì) ES6 想要深入了解可以看 ES6 In Depth 這篇文章和 ES6草案桥狡。
對(duì)象創(chuàng)建的特殊處理
let Ctor = function () {}; //用于代理轉(zhuǎn)換的空函數(shù)
let baseCraate = function (prototype) {
if (!_.isObject(prototype)) {
return {};
}
// 如果參數(shù)不是對(duì)象搅裙,直接返回空對(duì)象
if (nativeCreate) {
return nativeCreate(prototype);
}
//如果原生的對(duì)象創(chuàng)建可以使用,返回該方法根據(jù)原型創(chuàng)建的對(duì)象裹芝。
//處理沒有原生對(duì)象創(chuàng)建的情況
Ctor.prototype = prototype;
//將空函數(shù)的原型指向要使用的原型
let result = new Ctor();
//創(chuàng)建一個(gè)實(shí)例
Ctor.prototype = null;
// 恢復(fù)Ctor的原型給下次使用
return result;
//返回該實(shí)例
};
為了處理Object.create的跨瀏覽器的兼容性部逮,underscore進(jìn)行了特殊的處理。原型是無(wú)法直接實(shí)例化的嫂易,因此我們先創(chuàng)建一個(gè)空對(duì)象兄朋,然后將其原型指向這個(gè)我們想要實(shí)例化的原型,最后返回該對(duì)象其一個(gè)實(shí)例炬搭。
回調(diào)處理
在看這個(gè)之前蜈漓,先解釋一下void 0是什么含義? 引文來(lái)源
在 JavaScript 中宫盔,假設(shè)我們想判斷一個(gè)是否是 undefined融虽,那么我們通常會(huì)這樣寫:
if(a === undefined){}
但是,JavaScript 中的 undefined 并不可靠灼芭,我們?cè)囍鴮戇@樣一個(gè)函數(shù):
function test(a) {
var undefined = 1;
console.log(undefined); // => 1
if(a===undefined) {
// ...
}
}
現(xiàn)在我們能夠明確的有额,標(biāo)識(shí)符 undefined 并不能真正反映 “未定義”,所以我們得通過(guò)其他手段獲得這一語(yǔ)義。幸好 JavaScript 還提供了 void 運(yùn)算符巍佑,該運(yùn)算符會(huì)對(duì)指定的表達(dá)式求值茴迁,并返回受信的 undefined:
void expression
最常見的用法是通過(guò)以下運(yùn)算來(lái)獲得 undefined,表達(dá)式為 0 時(shí)的運(yùn)算開銷最杏┧ァ:
void 0;
// or
void(0);
在 underscore 中堕义,所有需要獲得 undefined 地方,都通過(guò) void 0 進(jìn)行了替代脆栋。
當(dāng)然倦卖,曲線救國(guó)的方式不只一種,我們看到包裹 jquery 的立即執(zhí)行函數(shù):
(function(window,undefined) {
// ...
})(window)
在這個(gè)函數(shù)中椿争,我們沒有向其傳遞第二參數(shù)(形參名叫 undefined)怕膛,那么第二個(gè)參數(shù)的值就會(huì)被傳遞上 “未定義”,因此秦踪,通過(guò)這種方式褐捻,在該函數(shù)的作用域中所有的 undefined 都為受信的 undefined。
// underscore內(nèi)部方法
// 根據(jù)this指向(context參數(shù))以及argCount參數(shù)二次操作返回一些回調(diào)椅邓、迭代方法
let optimizeCb = function (func, context, argCount) {
if (context === void 0) {
return func;
}
// void 0返回undefined柠逞,即未傳入上下文信息時(shí)直接返回相應(yīng)的函數(shù)
switch (argCount) {
// 如果傳入了argCount,那么參數(shù)數(shù)量為argCount希坚,如果傳入等價(jià)為null边苹,則為3,包括未傳值得情況
// 1個(gè)參數(shù)的時(shí)候裁僧,只需要傳遞當(dāng)前值
case 1: return function (value) {
return func.call(context, value);
};
// 并沒有2個(gè)參數(shù)的時(shí)候个束,因?yàn)槟壳安]有用到2個(gè)參數(shù)的時(shí)候
// 3個(gè)參數(shù)的時(shí)候,分別是當(dāng)前值聊疲、當(dāng)前索引以及整個(gè)集合
// _.each茬底、_.map
case 3:return function (value, index, collection) {
return func.call(context, value, index, collection)
};
// 4個(gè)參數(shù)的時(shí)候,分別是累計(jì)值获洲、當(dāng)前值阱表、當(dāng)前索引以及整個(gè)集合
//_.reduce、_reduceRight
case 4:return function (accumulator, value, index, collection) {
return func.call(context, accumulator, value, index, collection);
};
}
// 如果都不符合上述的任一條件贡珊,直接使用apply調(diào)用相關(guān)函數(shù)
return function () {
return func.apply(context, arguments)
}
其實(shí)不用上面的switch-case語(yǔ)句
直接執(zhí)行下面的return語(yǔ)句最爬,一樣的效果
不這樣做的原因call比apply快很多
apply在運(yùn)行前要對(duì)作為參數(shù)的數(shù)組進(jìn)行一系列的檢驗(yàn)和深拷貝,.call則沒有這些步驟
https://segmentfault.com/q/1010000007894513
http://www.ecma-international.org/ecma-262/5.1/#sec-15.3.4.3
http://www.ecma-international.org/ecma-262/5.1/#sec-15.3.4.4
接下來(lái)门岔,是針對(duì)集合迭代的回調(diào)處理
var builtinIteratee;
// 設(shè)置變量保存內(nèi)置迭代
var cb = function(value, context, argCount) {
if (_.iteratee !== builtinIteratee) {return _.iteratee(value, context);}
// 如果用戶修改了迭代器爱致,則使用新的迭代器
if (value == null) {return _.identity;}
// 如果不傳value,表示返回等價(jià)的自身
if (_.isFunction(value)) {return optimizeCb(value, context, argCount);}
// 如果傳入函數(shù)寒随,返回該函數(shù)的回調(diào)
if (_.isObject(value) && !_.isArray(value)) return _.matcher(value);
// 如果傳入對(duì)象糠悯,尋找匹配的屬性值
return _.property(value);
// 如果都不是帮坚,返回相應(yīng)的屬性訪問器
};
// 默認(rèn)的迭代器,是以無(wú)窮argCount為參數(shù)調(diào)用cb函數(shù)互艾。用戶可以自行修改试和。
_.iteratee = builtinIteratee = function(value, context) {
return cb(value, context, Infinity);
};
剩余的處理
// 屬性訪問器生成。
// 通過(guò)傳入鍵名纫普,返回可以訪問以傳入對(duì)象為參數(shù)阅悍,并可以獲取該對(duì)象相應(yīng)鍵值對(duì)的函數(shù)
let shallowProperty = function(key) {
return function(obj) {
return obj == null ? void 0 : obj[key];
};
};
// 額外參數(shù)。
// 等價(jià)于ES6的rest參數(shù)昨稼。它將起始索引后的參數(shù)放入一個(gè)數(shù)組中溉箕。
let restArgs = function(func, startIndex) {
startIndex = startIndex == null ? func.length - 1 : +startIndex;
// startIndex為null時(shí),為函數(shù)聲明時(shí)的參數(shù)數(shù)量減1悦昵,即除了第一個(gè)參數(shù)后的其他參數(shù),否則為傳入的startIndex
// 返回一個(gè)支持rest參數(shù)的函數(shù)
return function() {
// 校正參數(shù), 以免出現(xiàn)負(fù)值情況
let length = Math.max(arguments.length - startIndex, 0),
// 剩余參數(shù)是一個(gè)length長(zhǎng)度的數(shù)組
rest = Array(length),
index = 0;
// 假設(shè)參數(shù)從2個(gè)開始: func(a,b,*rest)
// 調(diào)用: func(1,2,3,4,5); 實(shí)際的調(diào)用是:func.call(this, 1,2, [3,4,5]);
for (; index < length; index++) {
rest[index] = arguments[index + startIndex];
}
// 根據(jù)rest參數(shù)不同, 分情況調(diào)用函數(shù), 需要注意的是, rest參數(shù)總是最后一個(gè)參數(shù), 否則會(huì)有歧義
switch (startIndex) {
case 0: return func.call(this, rest);
// 起始索引為0晌畅,表示全部參數(shù)作為一個(gè)數(shù)組調(diào)用
case 1: return func.call(this, arguments[0], rest);
// 表示將第一個(gè)參數(shù)以及后續(xù)參數(shù)的數(shù)組但指,作為兩個(gè)參數(shù)進(jìn)行調(diào)用
case 2: return func.call(this, arguments[0], arguments[1], rest);
// 將第一個(gè)參數(shù)、第二個(gè)參數(shù)以及后續(xù)參數(shù)的數(shù)組作為三個(gè)參數(shù)進(jìn)行調(diào)用
}
// 如果不是上面三種情況, 而是更通用的apply
let args = Array(startIndex + 1);
// 先拿到前面參數(shù)
for (index = 0; index < startIndex; index++) {
args[index] = arguments[index];
}
// 拼接上剩余參數(shù)
args[startIndex] = rest;
return func.apply(this, args);
};
};
// 處理類數(shù)組對(duì)象抗楔。
// 類數(shù)組對(duì)象的長(zhǎng)度應(yīng)當(dāng)是一個(gè)非負(fù)整數(shù)
let MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
// 表示正無(wú)窮+∞
let getLength = shallowProperty('length');
// 獲取length屬性訪問器
let isArrayLike = function(collection) {
let length = getLength(collection);
return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};