這是underscore源碼剖析系列第三篇文章,主要介紹underscore中each、map、filter、every、reduce等咱們經常使用的一些遍歷數組的方法。css
在underscore中咱們最經常使用的就是each和map兩個方法了,這兩個方法通常接收三個參數,分別是數組/對象、函數、上下文。jquery
// iteratee函數有三個參數,分別是item、index、array或者value、key、obj _.each = _.forEach = function(obj, iteratee, context) { // 若是不傳context,那麼each方法裏面的this就會指向window iteratee = optimizeCb(iteratee, context); var i, length; // 若是是類數組,通常來講包括數組、arguments、DOM集合等等 if (isArrayLike(obj)) { for (i = 0, length = obj.length; i < length; i++) { iteratee(obj[i], i, obj); } // 通常是指對象 } else { var keys = _.keys(obj); for (i = 0, length = keys.length; i < length; i++) { iteratee(obj[keys[i]], keys[i], obj); } } return obj; };
each函數的源碼很簡單,函數內部會使用isArrayLike方法來判斷當前傳入的第一個參數是類數組或者對象,若是是類數組,直接使用訪問下標的方式來遍歷,並將數組的項和index傳給iteratee函數,若是是對象,則先獲取到對象的keys,再進行遍歷後將對象的value和key傳給iteratee函數ios
不過在這裏,咱們主要分析optimizeCb和isArrayLike兩個函數。git
// 這個函數主要是給傳進來的func函數綁定context做用域。 var optimizeCb = function (func, context, argCount) { // 若是沒有傳context,那就直接返回func函數 if (context === void 0) return func; // 若是沒有傳入argCount,那就默認是3。這裏是根據第二次傳入的參數個數來給call函數傳入不一樣數量的參數 switch (argCount == null ? 3 : argCount) { case 1: return function (value) { return func.call(context, value); }; case 2: return function (value, other) { return func.call(context, value, other); }; // 通常是each、map等 case 3: return function (value, index, collection) { return func.call(context, value, index, collection); }; // 通常是reduce等 case 4: return function (accumulator, value, index, collection) { return func.call(context, accumulator, value, index, collection); }; } // 若是參數數量大於4 return function () { return func.apply(context, arguments); }; };
其實咱們很容易就看出來optimizeCb函數只是幫func函數綁定context的,若是不存在context,那麼直接返回func,不然則會根據第二次傳給func函數的參數數量來判斷給call函數傳幾個值。
這裏有個重點,爲何要用這麼麻煩的方式,而不直接用apply來將arguments所有傳進去?
緣由是call方法的速度要比apply方法更快,由於apply會對數組參數進行檢驗和拷貝,因此這裏就對經常使用的幾種形式使用了call,其餘狀況下使用了apply,詳情能夠看這裏:call和applygithub
關於isArrayLike方法,咱們來看underscore的實現。(這個延伸比較多,若是沒興趣,能夠跳過)segmentfault
// 一個高階函數,返回對象上某個具體屬性的值 var property = function (key) { return function (obj) { return obj == null ? void 0 : obj[key]; }; }; // 這裏有個ios8上面的bug,會致使相似var pbj = {1: "a", 2: "b", 3: "c"}這種對象的obj.length = 4; jQuery中也有這個bug。 // https://github.com/jashkenas/underscore/issues/2081 // https://github.com/jquery/jquery/issues/2145 // MAX_SAFE_INTEGER is 9007199254740991 (Math.pow(2, 53) - 1). // http://ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; // 聽說用obj["length"]就能夠解決?我沒有ios8的環境,有興趣的能夠試試 var getLength = property('length'); // 判斷是不是類數組,若是有length屬性而且值爲number類型便可視做類數組 var isArrayLike = function (collection) { var length = getLength(collection); return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX; };
在underscore中,只要帶有length屬性,均可以被認爲是類數組,因此即便是{length: 10}這種狀況也會被歸爲類數組。
我我的感受這樣寫其實太過片面,我仍是更喜歡jQuery裏面isArrayLike方法的實現。數組
function isArrayLike(obj) { // Support: real iOS 8.2 only (not reproducible in simulator) // `in` check used to prevent JIT error (gh-2145) // hasOwn isn't used here due to false negatives // regarding Nodelist length in IE var length = !!obj && "length" in obj && obj.length, type = toType(obj); // 排除了obj爲function和全局中有length變量的狀況 if (isFunction(obj) || isWindow(obj)) { return false; } return type === "array" || length === 0 || typeof length === "number" && length > 0 && (length - 1) in obj; }
jQuery中使用in來解決ios8下面那個JIT的錯誤,同時還會排除obj是函數和window的狀況,由於若是obj是函數,那麼obj.length則是這個函數參數的個數,而若是obj是window,那麼我在全局中定義一個var length = 10,這個一樣也能獲取到length。app
最後的三個判斷分別是:ide
說完了each,咱們再來講說map,map函數其實和each的實現很相似,不過不同的一個地方在於,map函數的第二個參數不必定是函數,咱們能夠什麼都不傳,甚至還能夠傳個對象。函數
var arr = [{name:'Kevin'}, {name: 'Daisy', age: 18}] var result1 = _.map(arr); // [{name:'Kevin'}, {name: 'Daisy', age: 18}] var result2 = _.map(arr, {name: 'Daisy'}) // [false, true]
因此這裏就會對傳入map的第二個參數進行判斷,總體來講map函數的實現比each更加簡潔。
_.map = _.collect = function (obj, iteratee, context) { // 由於在map中,第二個參數可能不是函數,因此用cb,這點和each的實現不同。 iteratee = cb(iteratee, context); // 若是不是類數組(是對象),則獲取到keys var keys = !isArrayLike(obj) && _.keys(obj), length = (keys || obj).length, results = Array(length); // 這裏根據keys是否存在來判斷傳給iteratee是key仍是index for (var index = 0; index < length; index++) { var currentKey = keys ? keys[index] : index; results[index] = iteratee(obj[currentKey], currentKey, obj); } return results; };
咱們來看看map函數中這個cb函數究竟是什麼來歷?
_.identity = function (value) { return value; }; var cb = function (value, context, argCount) { // 若是value不存在 if (value == null) return _.identity; // 若是傳入的是個函數 if (_.isFunction(value)) return optimizeCb(value, context, argCount); // 若是傳入的是個對象 if (_.isObject(value)) return _.matcher(value); return _.property(value); };
cb函數在underscore中通常是用在遍歷方法中,大多數狀況下value都是一個函數,咱們結合上面map的源碼和例子來看。
那麼咱們再來看matcher函數,matcher函數內部對兩個對象作了淺比較。
_.matcher = _.matches = function (attrs) { // 將attrs和{}合併爲一個對象(避免attrs爲undefined) attrs = _.extendOwn({}, attrs); return function (obj) { return _.isMatch(obj, attrs); }; }; // isMatch方法會對接收到的attrs對象進行遍歷,同時比較obj中是否有這一項 _.isMatch = function (object, attrs) { var keys = _.keys(attrs), length = keys.length; // 若是object和attr都是空,那麼返回true,不然object爲空時返回false 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; };
matcher是個高階方法,他會將兩次接收到的對象傳給isMatch函數來進行判斷。首先是以attrs爲被遍歷的對象,經過對比obj[key]和attrs[key]的值,只要obj中的值和attrs中的不想等,就會返回false。
這裏還會排除一種狀況,若是attrs中對應key的value正好是undefined,並且obj中並無key這個屬性,這樣obj[key]和attrs[key]其實都是undefined,這裏使用!==來比較必然會返回false,實際上二者應該是不想等的。
因此使用in來判斷obj上到底有沒有key這個屬性,若是沒有,也會返回false。若是attrs上面全部屬性在obj中都能找到,而且二者的值正好相等,那麼就會返回true。
這也就是爲何_.map([{name:'Kevin'}, {name: 'Daisy', age: 18}], {name: 'Daisy'}); 會返回 [false, true]。
each和map實現原理基本上同樣,不過map更加簡潔,這裏能夠用map的形式重寫一下each
_.each = _.forEach = function (obj, iteratee, context) { iteratee = optimizeCb(iteratee, context); var keys = !isArrayLike(obj) && _.keys(obj), length = (keys || obj).length, results = Array(length); for (var index = 0; index < length; index++) { var currentKey = keys ? keys[index] : index; iteratee(obj[currentKey], currentKey, obj); } return obj; };
這幾種方法的實現和上面的each、map相似,這裏就很少作解釋了,有興趣的能夠本身去看一下。