僅看 cb 和 optimizeCb 兩個函數的名字,你可能想不到這是用來作什麼的,儘管你可能想到 cb 是 callback 的縮寫。javascript
若是直接講解源碼,你可能想不明白爲何要這麼寫,因此咱們從 _.map 函數開始講起。html
_.map 相似於 Array.prototype.map
,但更加健壯和完善。咱們看下 _.map 的源碼:java
// 簡化過,這裏僅假設 obj 是數組 _.map = function (obj, iteratee, context) { iteratee = cb(iteratee, context); var length = obj.length, results = Array(length); for (var index = 0; index < length; index++) { results[index] = iteratee(obj[index], index, obj); } return results; };
map 方法除了傳入要處理的數組以外,還有兩個參數 iteratee 和 context,相似於 Array.prototype.map
中的其餘兩個參數,其中 iteratee 表示處理函數,context 表示指定的執行上下文,即 this 的值。git
而後在源碼中,咱們看到,咱們將 iteratee 和 context 傳入一個 cb 函數,而後覆蓋掉 iteratee 函數,而後將這個函數用做最終的處理函數。github
實際上,須要這麼麻煩嗎?不就是使用 iteratee 函數處理每次迭代的值嗎?不就是經過 context 指定 this 的值嗎?咱們能夠直接這樣寫吶:數組
_.map = function (obj, iteratee, context) { var length = obj.length, results = Array(length); for (var index = 0; index < length; index++) { results[index] = iteratee.call(context, obj[index], index, obj); } return results; }; // [2, 3, 4] console.log(_.map([1, 2, 3], function(item){ return item + 1; })) // [2, 3, 4] console.log(_.map([1, 2, 3], function(item){ return item + this.value; }, {value: 1}))
你看看也沒有什麼問題吶,但是,萬一 iteratee 咱們不傳入一個函數呢?好比咱們什麼也不傳,或者傳入一個對象,又或者傳入一個字符串、數字呢?架構
若是用咱們的方法天然是會報錯的,那 underscore 呢?app
// 使用 underscore // 什麼也不傳 var result = _.map([1,2,3]); // [1, 2, 3] // 傳入一個對象 var result = _.map([{name:'Kevin'}, {name: 'Daisy', age: 18}], {name: 'Daisy'}); // [false, true] var result = _.map([{name: 'Kevin'}, {name: 'Daisy'}], 'name'); // ['Kevin', 'daisy']
咱們會發現,underscore 居然還能根據傳入的值的類型不一樣,實現的效果不一樣。咱們總結下:ide
由此,咱們能夠推測在 underscore 的 cb 函數中,有對 iteratee 值類型的判斷,而後根據不一樣的類型,返回不一樣的 iteratee 函數。函數
因此咱們來看看 cb 函數的源碼:
var cb = function(value, context, argCount) { if (_.iteratee !== builtinIteratee) return _.iteratee(value, context); if (value == null) return _.identity; if (_.isFunction(value)) return optimizeCb(value, context, argCount); if (_.isObject(value) && !_.isArray(value)) return _.matcher(value); return _.property(value); };
這一看就牽扯到了 8 個函數!不要懼怕,咱們一個一個看。
if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);
咱們看看 _.iteratee 的源碼:
_.iteratee = builtinIteratee = function(value, context) { return cb(value, context, Infinity); };
由於 _.iteratee = builtinIteratee
的緣故,_.iteratee !== builtinIteratee
值爲 false,因此正常狀況下 _.iteratee(value, context)
並不會執行。
可是若是咱們在外部修改了 _.iteratee 函數,結果便會爲 true,cb 函數直接返回 _.iteratee(value, context)
。
這個意思實際上是說用咱們自定義的 _.iteratee 函數來處理 value 和 context。
試想咱們並不須要如今 _.map 這麼強大的功能,我只但願當 value 是一個函數,就用該函數處理數組元素,若是不是函數,就直接返回當前元素,咱們能夠這樣修改:
<html> <head> <title>underscore map</title> </head> <body> <script src="../vender/underscore.js"></script> <script type="text/javascript"> _.iteratee = function(value, context) { if (typeof value === 'function') { return function(...rest) { return value.call(context, ...rest) }; } return function(value) { return value; }; }; // 若是 map 的第二個參數不是函數,就返回該元素 console.log(_.map([1, 2, 3], 'name')); // [1, 2, 3] // 若是 map 的第二個參數是函數,就使用該函數處理數組元素 var result = _.map([1, 2, 3], function(item) { return item + 1; }); console.log(result); // [2, 3, 4] </script> </body> </html>
固然更多的狀況是自定義對不一樣的 value 使用不一樣的處理函數,值得注意的是,underscore 中的多個函數都是用了 cb 函數,而由於 cb 函數使用了 _.iteratee 函數,若是你修改這個函數,其實會影響多個函數,這些函數基本都屬於集合函數,具體包括 map、find、filter、reject、every、some、max、min、sortBy、groupBy、indexBy、countBy、sortedIndex、partition、和 unique。
if (value == null) return _.identity;
讓咱們看看 _.identity 的源碼:
_.identity = function(value) { return value; };
這也就是爲何當 map 的第二個參數什麼都不傳的時候,結果會是一個相同數組的緣由。
_.map([1,2,3]); // [1, 2, 3]
若是直接看這個函數,可能以爲沒有什麼用,但用在這裏,卻又十分的合適。
if (_.isFunction(value)) return optimizeCb(value, context, argCount);
當 value 是一個函數的時候,就傳入 optimizeCb 函數,咱們來看看 optimizeCb 函數:
var optimizeCb = function(func, context, argCount) { // 若是沒有傳入 context,就返回 func 函數 if (context === void 0) return func; switch (argCount) { case 1: return function(value) { return func.call(context, value); }; case null: 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); }; };
也許你會好奇,爲何我要對 argCount 進行判斷呢?就不能直接返回嗎?好比這樣:
var optimizeCb = function(func, context) { // 若是沒有傳入 context,就返回 func 函數 if (context === void 0) return func; return function() { return func.apply(context, arguments); }; };
固然沒有問題,但爲何 underscore 要這樣作呢?其實就是爲了不使用 arguments,提升一點性能而已,若是不是寫一個庫,其實還真是沒有必要作到這點。
而爲何當參數是 3 個時候,參數名稱分別是 value, index, collection ,又爲何沒有參數爲 2 的狀況呢?其實這都是根據 underscore 函數用到的狀況,沒有函數用到兩個參數,因而就省略了,像 map 函數就會用到 3 個參數,就根據這三個參數的名字起了這裏的變量名啦。
if (_.isObject(value) && !_.isArray(value)) return _.matcher(value);
這段就是用來處理當 map 的第二個參數是對象的狀況:
// 傳入一個對象 var result = _.map([{name:'Kevin'}, {name: 'Daisy', age: 18}], {name: 'Daisy'}); // [false, true]
若是 value 是一個對象,而且不是數組,就使用 _.matcher 函數。看看各個函數的源碼:
var nativeIsArray = Array.isArray; _.isArray = nativeIsArray || function(obj) { return Object.prototype.toString.call(obj) === '[object Array]'; }; _.isObject = function(obj) { var type = typeof obj; return type === 'function' || type === 'object' && !!obj; }; // extend 函數能夠參考 《JavaScript 專題之手寫一個 jQuery 的 extend》 _.matcher = function(attrs) { attrs = _.extend({}, attrs); return function(obj) { return _.isMatch(obj, attrs); }; }; // 該函數判斷 attr 對象中的鍵值是否在 object 中有而且相等 // var stooge = {name: 'moe', age: 32}; // _.isMatch(stooge, {age: 32}); => true // 其中 _.keys 至關於 Object.keys _.isMatch = function(object, attrs) { var keys = _.keys(attrs), length = keys.length; if (object == null) return !length; var obj = Object(object); for (var i = 0; i < length; i++) { var key = keys[i]; if (attrs[key] !== obj[key] || !(key in obj)) return false; } return true; };
return _.property(value);
這個就是處理當 value 是基本類型的值的時候,返回元素對應的屬性值的狀況:
var result = _.map([{name: 'Kevin'}, {name: 'Daisy'}], 'name'); // ['Kevin', 'daisy']
咱們看下源碼:
_.property = function(path) { // 若是不是數組 if (!_.isArray(path)) { return shallowProperty(path); } return function(obj) { return deepGet(obj, path); }; }; var shallowProperty = function(key) { return function(obj) { return obj == null ? void 0 : obj[key]; }; }; // 根據路徑取出深層次的值 var deepGet = function(obj, path) { var length = path.length; for (var i = 0; i < length; i++) { if (obj == null) return void 0; obj = obj[path[i]]; } return length ? obj : void 0; };
咱們好像發現了新大陸,原來 value 還能夠傳一個數組,用來取深層次的值,舉個例子:
var person1 = { child: { nickName: 'Kevin' } } var person2 = { child: { nickName: 'Daisy' } } var result = _.map([person1, person2], ['child', 'nickName']); console.log(result) // ['Kevin', 'daisy']
若是你想學習 underscore 的源碼,在分析集合相關的函數時必定會接觸 cb 和 optimizeCb 函數,先掌握這兩個函數,會幫助你更好更快的解讀源碼。
underscore 系列目錄地址:https://github.com/mqyqingfeng/Blog。
underscore 系列預計寫八篇左右,重點介紹 underscore 中的代碼架構、鏈式調用、內部函數、模板引擎等內容,旨在幫助你們閱讀源碼,以及寫出本身的 undercore。
若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵。