理解Underscore中的uniq(陣列去重)函式

Russ_Zhong發表於2019-03-04

uniq函式,是Underscore中的一個陣列去重函式,給它傳遞一個陣列,它將會返回該陣列的去重副本。

1 ES6版本去重

在ES6版本中,引入了一個新的資料結構——set,這是一種類似陣列的資料結構,它有個最大的特點就是內部的每一個元素都是獨一無二的,所以我們可以利用它來對陣列進行去重:

var uniq = function(array) {
    var set = new Set(array);
    return [...set];
}
複製程式碼

這是目前而言最快速簡介的陣列去重方法。但是由於瀏覽器相容問題,目前ES6還沒有完全普及,這樣的方法可能在老舊版本的瀏覽器當中無法起到作用。所以我們還是需要使用ES5來實現。

2 ES5版本去重

對於接受的陣列,我們可以對其進行遍歷,使用一個result陣列存放獨一無二的元素,對於傳入陣列的每一項,在result中進行檢索,如果result中不存在,那麼就推入result中,最後返回result即可:

var uniq = function(array) {
    var result = [];
    var length = array.length;
    var i;
    for(i = 0; i < length; i++) {
        if(result.indexOf(array[i]) < 0) {
            result.push(array[i]);
        }
    }
    return result;
};
複製程式碼

該函式已經能夠比較簡單的數值、字串、布林值等簡單值了,但是如果是複雜物件的話,可能就達不到去重的目的,比如:

var objArr = [{name: `a`}, {name: `a`}];
console.log(uniq(objArr));
複製程式碼

我們可能會希望返回值是[{name: `a`}],但是由於連個物件引用值不相等,所以比較時,不會對這兩個物件進行去重,導致最後返回的結果是兩個都存在,這顯然不是我們所期望的。

我們需要一個指定比較規則的函式。

3 規則定製版去重函式

我們無法預知使用者傳遞的陣列內元素的型別,所以我們最好能夠讓使用者自定義比較規則,最好的辦法就是讓使用者傳遞函式作為引數。

預設函式接受的引數即為陣列中的某一項:

var uniq = function(array, func) {
    var result = [];
    var length = array.length;
    var i;
    if(!func) {
        for(i = 0; i < length; i++) {
            if(result.indexOf(array[i]) < 0) {
                result.push(array[i]);
            }
        }
    }
    else {
        var seen = [];
        for(i = 0; i < length; i++) {
            if(seen.indexOf(func(array[i])) < 0) {
                seen.push(func(array[i]));
                result.push(array[i]);
            }
        }
    }
    return result;
};
複製程式碼

在func沒有被傳遞時,直接進行比較;如果傳遞了func函式,那麼對於array中的每一項,使用func處理後的返回值再進行比較,這樣就可以達到物件比較的目的。

再次使用物件進行實驗:

var objArr = [{id: `a`}, {id: `a`}, {id: `b`}];
console.log(uniq(objArr, function(item) {
    return item.id;
}));
複製程式碼

輸出結果中只有兩個物件,說明達到了要求。

傳遞了這個自定義函式之後,去重的靈活性就大大的增加了。比如對於一個傳遞的物件陣列,其中的每個物件都包含兩個屬性——name和age,我們需要比較這些物件,只有當name和age都相同的時候,我們才認為兩個物件相同,那麼:

var persons = [{name: `dm`, age: 22}, {name: `dm`, age: 23}, {name: `dm`, age: 22}];
console.log(uniq(persons, function(item) {
    return item.name + item.age;
}));
複製程式碼

最後返回的結果能夠符合我們去重的要求。

現在去重的問題解決了,可以提高一下效率嗎?

如果我們得到的是一個有序的陣列(無論是陣列排序還是字串排序),我們可以只比較相鄰兩項是否相同來去重,這樣更加簡單快速。

4 快速去重

我們可以給uniq函式新增一個引數——isSorted,代表傳遞的陣列是否是有序陣列。

var uniq = function(array, isSorted, func) {
    var result = [];
    var length = array.length;
    var i;
    var seen = [];
    if(isSorted && !func) {
        for(i = 0; i< length; i++) {
            if(array[i] == seen) continue;
            else {
                result.push(array[i]);
                seen = array[i];
            }
        }
    }
    else if(func){
        for(i = 0; i < length; i++) {
            if(seen.indexOf(func(array[i])) < 0) {
                seen.push(func(array[i]));
                result.push(array[i]);
            }
        }
    }
    else{
        for(i = 0; i < length; i++) {
            if(result.indexOf(array[i]) < 0) {
                result.push(array[i]);
            }
        }
    }
    return result;
};
複製程式碼

這樣的實現就比較完善了,其中重要的點是對於seen這個變數的運用。

以上程式碼的實現思想就是來源於Underscore,只不過實現得比Underscore更加簡陋,相對而言不那麼完善。

5 Underscore實現陣列去重

以下就是Underscore的原始碼(附註釋):

// Produce a duplicate-free version of the array. If the array has already
// been sorted, you have the option of using a faster algorithm.
// The faster algorithm will not work with an iteratee if the iteratee
// is not a one-to-one function, so providing an iteratee will disable
// the faster algorithm.
// Aliased as `unique`.
//陣列去重函式,使得陣列中的每一項都是獨一無二的。
_.uniq = _.unique = function (array, isSorted, iteratee, context) {
	//如果沒有傳遞isSorted引數(即傳遞值不是Boolean型別),那麼預設為false,其餘引數重新賦值。
	if (!_.isBoolean(isSorted)) {
		context = iteratee;
		iteratee = isSorted;
		isSorted = false;
	}
	//如果傳遞了iteratee,那麼使用cb方法包裝(確保返回一個函式),然後重新賦值。
	if (iteratee != null) iteratee = cb(iteratee, context);
	//儲存結果。
	var result = [];
	//用於存放array的值便於下一次比較,或者用於儲存computed值。
	var seen = [];
	//遍歷array陣列。
	for (var i = 0, length = getLength(array); i < length; i++) {
		//value表示當前項,computed表示要比較的項(有iteratee時是iteratee的返回值,無iteratee時是value自身)。
		var value = array[i],
			computed = iteratee ? iteratee(value, i, array) : value;
		if (isSorted && !iteratee) {
			//如果陣列是有序的,並且沒有傳遞iteratee,則依次比較相鄰的兩項是否相等。
			//!0===true,其餘皆為false。
			if (!i || seen !== computed) result.push(value);
			//seen存放當前的項,以便於下一次比較。
			seen = computed;
		} else if (iteratee) {
			//如果傳遞了iteratee,那麼seen就用於存放computed值,便於比較。
			//之所以不直接使用result存放computed值是因為computed只用於比較,result存放的值必須是原來陣列中的值。
			if (!_.contains(seen, computed)) {
				seen.push(computed);
				result.push(value);
			}
		} else if (!_.contains(result, value)) {
			//isSorted為false並且iteratee為undefined。
			//可以理解為引數陣列中是亂序數字,直接比較就好了。
			result.push(value);
		}
	}
	return result;
};
複製程式碼

陣列去重是一件說容易也容易,說簡單也簡單的事情,就看你怎麼做了。

更多Underscore原始碼解讀:GitHub

相關文章