JavaScript 設計模式學習第二十二篇-迭代器模式

迭代器模式(Iterator Pattern)用於順序地訪問聚合對象內部的元素,又無需知道對象內部結構。使用了迭代器之後,使用者不需要關心對象的內部構造,就可以按序訪問其中的每個元素。

1. 什麼是迭代器

銀行裏的點鈔機就是一個迭代器,放入點鈔機的鈔票裏有不同版次的人民幣,每張鈔票的冠字號也不一樣,但當一沓鈔票被放入點鈔機中,使用者並不關心這些差別,只關心鈔票的數量,以及是否有假幣。

這裏我們使用 JavaScript 的方式來點一下鈔:

var bills = ['MCK013840031', 'MCK013840032', 'MCK013840033', 'MCK013840034', 'MCK013840035'];

bills.forEach(function(bill) {
    console.log('當前鈔票的冠字號爲 ' + bill)
})

是不是很簡單,這是因爲 JavaScript 已經內置了迭代器的實現,在某些個很老的語言中,使用者可能會爲了實現迭代器而煩惱,但是在 JavaScript 中則完全不用擔心。

 

2. 迭代器的簡單實現

前面的 forEach 方法是在 IE9 之後才原生提供的,那麼在 IE9 之前的時代裏,如何實現一個迭代器呢,我們可以使用 for 循環自己實現一個 forEach:

var forEach = function(arr, cb) {
    for (var i = 0; i < arr.length; i++) {
        cb.call(arr[i], arr[i], i, arr)
    }
}

forEach(['hello', 'world', '!'], function(currValue, idx, arr) {
    console.log('當前值 ' + currValue + ',索引爲 ' + idx)
})

// 當前值 hello,索引爲 0
// 當前值 world,索引爲 1
// 當前值 !,索引爲 2

2.1. JQuery 源碼中迭代器實現

JQuery 也提供了一個 $.each的遍歷方法:

// jquery 源碼 
each: function (obj, callback) {
    var i = 0;
    // obj 爲數組時
    if (isArrayLike(obj)) {
        for (; i < obj.length; i++) {
            if (callback.call(obj[i], i, obj[i]) === false) {
                break
            }
        }
    // obj 爲對象時
    } else {
        for (i in obj) {
            if (callback.call(obj[i], i, obj[i]) === false) {
                break
            }
        }
    }
    return obj
}

// 使用
$.each(['hello', 'world', '!'], function(idx, currValue){
    console.log('當前值 ' + currValue + ',索引爲 ' + idx)
})

這裏的源碼分爲兩個部分,前一個部分是形參 obj 爲數組情況下的處理,使用 for 循環,以數組下標依次使用 call/apply傳入回調中執行,第二部分是形參 obj爲對象情況下的處理,是使用 for-in 循環來獲取對象上的屬性。另外可以看到如果 callback.call返回的結果是 false 的話,這個循環會被 break。

源碼位於: jquery/src/core.js#L246-L265

由於處理對象時使用的是 for-in,所以原型上的變量也會被遍歷出來:

var foo = { paramProto: '原型上的變量' };

var bar = Object.create(foo, {
    paramPrivate: {
        configurable: true,
        enumerable: true,
        value: '自有屬性',
        writable: true
    }
});

$.each(bar, function(key, currValue) {
    console.log('當前值爲 「' + currValue + '」,鍵爲 ' + key);
})

// 當前值爲 「自有屬性」,鍵爲 paramPrivate
// 當前值爲 「原型上的屬性」,鍵爲 paramProto

因此可以使用 hasOwnProperty 來判斷鍵是否是在原型鏈上還是對象的自有屬性。

我們還可以利用如果 callback.call 返回的結果是 false 則 break 的特點,來進行一些操作:

$.each([1, 2, 3, 4, 5], function (idx, currValue) {
    if (currValue > 3){
        return false
    }
    console.log('當前值爲 ' + currValue)
})
// 當前值爲 1
// 當前值爲 2
// 當前值爲 3

2.2. Underscore 源碼中的迭代器實現

// underscore 源碼
_.each = function (obj, iteratee) {

    var i, length

    // obj 爲數組時
    if (isArrayLike(obj)) {
        for (i = 0, length = obj.length; i < length; i++) {
            iteratee(obj[i], i, obj);
        }

    // 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(['hello', 'world', '!'], function (currValue, idx, arr) {
    console.log('當前值 ' + currValue + ',索引爲 ' + idx)
})

underscore 迭代器部分的實現跟 jQuery 的差不多,只是回調 iteratee 的執行是直接調用,而不是像 jQuery 是使用 call,也不像 jQuery 那樣提供了迭代終止 break 的支持,所以總的來說還是 jQuery 的實現更優。

另外,這裏 iteratee 變量的命名也可以看出來迭代器的含義。

源碼位於: underscore.js#L181-L195

 

3. JavaScript 原生支持

隨着 JavaScript 的 ECMAScript 標準每年的發展,給越來越多好用的 API 提供了支持,比如 Array 上的 filter、forEach、reduce、flat 等,還有 Map、Set、String 等數據結構,也提供了原生的迭代器支持,給我們的開發提供了很多便利,也讓 underscore 這些工具庫漸漸淡出歷史舞臺。

另外,JavaScript 中還有很多類數組結構,比如:

1. arguments:函數接受的所有參數構成的類數組對象;

2. NodeList:是 querySelector接口族返回的數據結構;

3. HTMLCollection:是 getElementsBy 接口族返回的數據結構;

對於這些類數組結構,我們可以通過一些方式來轉換成普通數組結構,以 arguments爲例:

// 方法一
var args = Array.prototype.slice.call(arguments)

// 方法二
var args = [].slice.call(arguments)

// 方法三 ES6提供
const args = Array.from(arguments)

// 方法四 ES6提供
const args = [...arguments];

轉換成數組之後,就可以快樂使用 JavaScript 在 Array 上提供的各種方法了。

 

4. ES6 中的迭代器

ES6 規定,默認的迭代器部署在對應數據結構的 Symbol.iterator 屬性上,如果一個數據結構具有 Symbol.iterator 屬性,就被視爲可遍歷的,就可以用 for...of 循環遍歷它的成員。也就是說,for...of 循環內部調用的是數據結構的Symbol.iterator 方法。

for-of 循環可以使用的範圍包括 Array、Set、Map 結構、上文提到的類數組結構、Generator 對象,以及字符串。

通過 for-of 可以使用 Symbol.iterator 這個屬性提供的迭代器可以遍歷對應數據結構,如果對沒有提供 Symbol.iterator 的目標使用 for-of 則會拋錯:

var foo = { a: 1 }

for (var key of foo) {
    console.log(key)
}

// Uncaught TypeError: foo is not iterable

我們可以給一個對象設置一個迭代器,讓一個對象也可以使用 for-of 循環:

var bar = {
    a: 1,
    [Symbol.iterator]: function() {
        var valArr = [
            { value: 'hello', done: false },
            { value: 'world', done: false },
            { value: '!', done: false },
            { value: undefined, done: true }
        ]
        return {
            next: function() {
                return valArr.shift()
            }
        }
    }
}

for (var key of bar) {
    console.log(key)
}

// hello
// world
// !

可以看到 for-of 循環連 bar 對象自己的屬性都不遍歷了,遍歷獲取的值只和 Symbol.iterator 方法實現有關。

 

5. 迭代器模式總結

迭代器模式早已融入我們的日常開發中,在使用 filter、reduce、map 等方法的時候,不要忘記這些便捷的方法就是迭代器模式的應用。當我們使用迭代器方法處理一個對象時,我們可以關注與處理的邏輯,而不必關心對象的內部結構,側面將對象內部結構和使用者之間解耦,也使得代碼中的循環結構變得緊湊而優美。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章