通過underscore系列的一和二,我們掌握了一些underscore框架設計的思想以及基礎的資料型別診斷,從這一節開始,我們將慢慢進入underscore的難點和核心。這個系列的三和四,我們會全方位的瞭解underscore中對迭代器的實現。
三, 迭代器
3.1 迭代器的基本概念
javascript的迭代器我們並不陌生。我們可以這樣理解,迭代器提供了一種方法按順序訪問集合中的每一項,而這種集合可以是陣列,也可以是物件。我們先回憶一下,在ES6之前,javascript有7種陣列的迭代器,分別是
- for 迴圈,它是最常規,也是最基礎的迭代
var arr = [1,2,4]
for(var i=0;i<arr.length;i++) {
console.log(i)
}
複製程式碼
- forEach 接收一個函式作為引數,對陣列中的每一個元素使用該函式。
function add(num) {console.log(num + 1)}
var arr = [1,2,3]
arr.forEach(add)
複製程式碼
- every 接收一個返回值為boolean 型別的函式,對陣列中的每一個元素使用該函式,當所有的元素使用該方法後都返回true, 則最終結果返回true
function judge(a) {return a > 1}
var arr = [1,23,4];
console.log(arr.every(judge)); // false
複製程式碼
- some 接收一個返回值為boolean 型別的函式,對陣列中的每一個元素使用該函式,只要有一個元素使用該函式後返回結果為true,則最終結果返回true。
function judge(a) {return a > 1}
var arr = [1,23,4];
console.log(arr.some(judge)); // true
複製程式碼
- filter 接收一個返回值為boolean 型別的函式,對陣列中的每一個元素使用該函式,返回使用該函式後返回值為true的新陣列集合。
function filterNum(a) {return a> 2}
var arr = [1,45,56,2,5]
console.log(arr.filter(filterNum)) // [45, 56, 5]
複製程式碼
- reduce 接收一個函式,返回一個值。該方法會從一個累加值開始,不斷對累加值和陣列中的後續元素呼叫該函式,直到陣列中的最後一個元素,最後返回得到的累加值。(reduceRight 遍歷的順序為倒序)
function add(a, b) {return a +b }
var arr = [1,3,4,5,6]
console.log(arr.reduce(add, 0)) //19
複製程式碼
- map 接收一個函式作為引數,對陣列中的每一個元素使用該函式,返回一個執行了該函式的陣列集合,該方法也不改變原陣列
function mapNum(a) {return a + 1 }
var arr = [2,4,56]
console.log(arr.map(mapNum)) // [3,5,57]
複製程式碼
針對物件 ,我們經常使用for in進行迭代, 也可以使用 Object.keys等方式進行迭代
var a = {b: 1, c: 2}
for(var i in a) {
console.log(i) //b, c
}
Object.keys(a) // b, c
複製程式碼
在設計underscore框架的迭代器時我們需要考慮的是, 如何擴充套件方法讓迭代器適用於陣列,物件,或者帶有length屬性的arguments類陣列以及字串。並且從實現角度講,為了相容低版本的瀏覽器,我們需要拋棄ES5規範下便捷的方法,使用最常規的for迴圈進行陣列,類陣列,字串的遍歷。同樣,也可以通過for迴圈來遍歷物件。
3.2 _.reduce - _.reduce(list, iteratee, [memo], [context])
首先從最複雜的reduce入手,reduce的基本功能前面在陣列的迭代器方法中已經介紹,我們只重點關注實現的細節。其中iteratee 為迭代器函式,該函式有四個引數memo,value 和 迭代的index(或者 key)和最後一個引用的整個 list。
reduce 和 reduceRight 唯一的區別在於遍歷順序,一個從左往右, 一個從右往左,因此可以用同一個函式來設計reduce。其中 context 改變this的指向我們稍後分析。
_.reduce = createReduce(1);
_.reduceRight = createReduce(-1);
var createReduce = function(dir) {
// dir 來區分
return function (obj, iteratee, meno, context) {
// 兩種型別,物件和類陣列需要區別處理方便for迴圈遍歷, 物件我們需要拿到所有的屬性集合,陣列,類陣列我們關注的是下標。
// 巧妙點:當為陣列,類陣列時 keys = false, 當為物件時 keys = 屬性陣列
var keys = !isArrayLike(obj) && _.keys(obj)
var lengths = (keys || obj).length;
// 處理遍歷方向,即引數dir的值
var index = dir > 0 ? 0 : length-1;
for (; index >= 0 && index < lengths; index += dir) {
// 如果是陣列,類陣列則取下標,如果是物件則取屬性值
var currentKey = keys ? keys[index] : index;
// 執行迭代器函式,並把返回值賦值給meno,繼續迴圈迭代
meno = iteratee(meno, obj[currentKey], currentKey, obj);
}
return meno
}
}
複製程式碼
reduce 函式在使用的時候,meno是可選項,如果沒有傳遞meno, 則自動會把list 中的第一個元素賦值給meno。因此我們可以將處理的核心程式碼抽離為一個獨立的函式,並將是否有meno的初始值做獨立判斷。
_.reduce = createReduce(1);
_.reduceRight = createReduce(-1);
var createReduce = function(dir) {
// dir 來區分
var reducer = function (obj, iteratee, meno, context, initial) {
···
// 增加meno 的初始值判斷賦值
if (!initial) {
memo = obj[keys ? keys[index] : index];
index += dir;
}
···
}
return function (obj, iteratee, meno, context) {
// 記錄是否有meno傳值
var initial = arguments.length >=3;
return reducer(obj, iteratee, meno, context, initial)
}
}
複製程式碼
reduce的實現已經基本完成,然而依然留著一個懸念,那就是context可以改變this的指向。underscore原始碼中單獨將context改變this指向的方法抽離成一個獨立的函式optimizeCb,該方法可以相容underscore中 所有需要改變this指向的過程, reduce 為其中一種情形,函式呼叫call方法改變this指向所傳遞的引數分別為 meno, value, index, list
var createReduce = function(dir) {
···
return function(obj, iteratee, meno, context) {
···
return reducer(obj, optimizeCb(iteratee, context, 4), initial) //optimizeCb優化
}
}
var optimizeCb = function(func, context, argCount) {
// 當沒有特定的this指向時, 返回原函式
if(context == void 0) return func;
switch(argCount) {
// 針對reduce函式的context指向
case 4: return function(context, meno, value, index, list) {
return func.call(context, meno, value, index, list);
}
}
}
複製程式碼
3.3 map - _.map(list, iteratee, [context])
underscore 中map 方法原理上會生成一個新的陣列,該陣列與目標源陣列,類陣列的length屬性相同,或者與物件的自身可列舉屬性個數相同,因此有了reduce 的基礎,我們可以簡單的實現map 方法
_.map = function (obj, iteratee, context) {
var keys = !isArrayLike(obj) && _.keys(obj)
var lengths = (keys || obj).length;
// 生成一個個數和目標源陣列個數相同,或者目標源物件自身可列舉屬性個數相同的陣列
var result = new Array(lengths);
for(var i=0; i<lengths;i++) {
var currentKey = keys ? keys[i] : i;
results = iteratee(obj[currentKey], currentKey, obj)
}
}
複製程式碼
對於map的使用,我們可以不傳遞iteratee迭代器,可以傳遞一個函式型別的迭代器,也可以傳遞一個物件型別作為迭代器,因此需要在進入迭代過程之前做一層過濾,根據不同的迭代器型別做不同的操作。
_.map = function(obj, iteratee, context) {
// 迭代器型別分類
iteratee = cb(iteratee, context);
···
}
複製程式碼
- 1.當不傳遞迭代器時,如_.map(obj) 會返回obj本身,因此需要定義另一個方法,該方法返回與傳入引數相等的值。
var cb = function(iteratee, context) {
if(iteratee == null) return _.identity
}
_.identity = function(value) {
return value
}
複製程式碼
- 2.當傳遞的迭代器為函式時,可以直接進入optimizeCb的迭代器優化過程。
var cb = function(iteratee, context) {
if( _.isFunction(iteratee) ) return optimizeCb(iteratee, context, 3) // 此時型別為3
}
// 完善optimizeCb 函式
var optimizeCb = function(func, context, argCount) {
// 當沒有特定的this指向時, 返回原函式
if(context == void 0) return func;
switch(argCount) {
case 3: return function(context, value, index, list) {
return func.call(context, value, index, list);
}
case 4: return function(context, meno, value, index, list) {
return func.call(context, meno, value, index, list);
}
}
}
複製程式碼
- 3.當傳遞的迭代器為物件時會返回一個斷言函式,這個具體的內容我們將在以後的篇幅分析。
var cb = function(iteratee, context) {
if (_.isObject(value) && !_.isArray(value)) return _.matcher(value); // 返回斷言函式
}
複製程式碼
3.4 _.times - _.times(n, iteratee, [context])
_.times也是underscore提供的迭代器,它會呼叫迭代器n次,每次呼叫時傳遞index作為引數,最終結果返回一個執行結果的陣列。例如:
console.log(_.times(n, function(i) {return i * 2 })) // [0, 1, 4]
複製程式碼
簡單的實現如下,關鍵點在註釋中說明:
_.times = function(n, iteratee, context) {
// 必須保證n的值是比0 大的數字
var n = Math.max(0, n);
// 建立一個個數為n的新陣列
var arr = new Array(n);
for(var i = 0; i<n;i++) {
arr[i] = iteratee(i)
}
return arr
}
複製程式碼
同樣,涉及context 改變this指向,我們同樣可以通過optimizeCb進行優化,此時完善optimizeCb函式
_.times = function(n, iteratee, context) {
···
iteratee = optimizeCb(iteratee, context, 1)
}
// 完善optimizeCb 函式
var optimizeCb = function(func, context, argCount) {
// 當沒有特定的this指向時, 返回原函式
if(context == void 0) return func;
switch(argCount) {
case 1: return function(context, value) {
return func.call(context, value)
}
case 3: return function(context, value, index, list) {
return func.call(context, value, index, list);
}
case 4: return function(context, meno, value, index, list) {
return func.call(context, meno, value, index, list);
}
}
}
複製程式碼
通過列舉三種迭代器的設計,我們不但掌握了underscore迭代器設計的基本思想,也對中間核心optimizeCb函式設計的可能進行了列舉,underscore中optimizeCb函式優化的型別只有三種,對應的數值分別為1,3,4(注意:並沒有2的類別)。我們也對迭代器的三種型別進行了列舉,在cb函式中分別區分了不傳遞迭代器,迭代器自身為陣列,物件時的處理。掌握了optimizeCb和cb兩個函式的設計思想,在設計剩餘的迭代器時難度便小了很多。 由於篇幅過長,underscore中其他迭代器的實現我們放到下一節闡述。