亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專(zhuān)欄INFORMATION COLUMN

也談JavaScript數(shù)組去重

崔曉明 / 866人閱讀

摘要:昨天在微博上看到一篇文章,也寫(xiě)數(shù)組去重,主要推崇的方法是將利用數(shù)組元素當(dāng)作對(duì)象來(lái)去重。我在微博轉(zhuǎn)發(fā)了用對(duì)象去重不是個(gè)好辦法然后作者問(wèn)什么才是推薦的方法。實(shí)例對(duì)象實(shí)例對(duì)象主要指通過(guò)構(gòu)造函數(shù)類(lèi)生成的對(duì)象。

本文同時(shí)發(fā)布于個(gè)人博客https://www.toobug.net/articl...

JavaScript的數(shù)組去重是一個(gè)老生常談的話題了。隨便搜一搜就能找到非常多不同版本的解法。

昨天在微博上看到一篇文章,也寫(xiě)數(shù)組去重,主要推崇的方法是將利用數(shù)組元素當(dāng)作對(duì)象key來(lái)去重。我在微博轉(zhuǎn)發(fā)了“用對(duì)象key去重不是個(gè)好辦法…”然后作者問(wèn)什么才是推薦的方法。

細(xì)想一下,這樣一個(gè)看似簡(jiǎn)單的需求,如果要做到完備,涉及的知識(shí)和需要注意的地方著實(shí)不少,于是誕生此文。

定義重復(fù)(相等)

要去重,首先得定義,什么叫作“重復(fù)”,即具體到代碼而言,兩個(gè)數(shù)據(jù)在什么情況下可以算是相等的。這并不是一個(gè)很容易的問(wèn)題。

對(duì)于原始值而言,我們很容易想到11是相等的,"1""1"也是相等的。那么,1"1"是相等的么?

如果這個(gè)問(wèn)題還好說(shuō),只要回答“是”或者“不是”即可。那么下面這些情況就沒(méi)那么容易了。

NaN

初看NaN時(shí),很容易把它當(dāng)成和null、undefined一樣的獨(dú)立數(shù)據(jù)類(lèi)型。但其實(shí),它是數(shù)字類(lèi)型。

// number
console.log(typeof NaN);

根據(jù)規(guī)范,比較運(yùn)算中只要有一個(gè)值為NaN,則比較結(jié)果為false,所以會(huì)有下面這些看起來(lái)略蛋疼的結(jié)論:

// 全都是false
0 < NaN;
0 > NaN;
0 == NaN;
0 === NaN;

以最后一個(gè)表達(dá)式0 === NaN為例,在規(guī)范中有明確規(guī)定(http://www.ecma-international...):

If Type(x) is Number, then

If x is NaN, return false.

If y is NaN, return false.

If x is the same Number value as y, return true.

If x is +0 and y is ?0, return true.

If x is ?0 and y is +0, return true.

Return false.

這意味著任何涉及到NaN的情況都不能簡(jiǎn)單地使用比較運(yùn)算來(lái)判定是否相等。比較科學(xué)的方法只能是使用isNaN()

var a = NaN;
var b = NaN;

// true
console.log(isNaN(a) && isNaN(b));
原始值和包裝對(duì)象

看完NaN是不是頭都大了。好了,我們來(lái)輕松一下,看一看原始值和包裝對(duì)象這一對(duì)冤家。

如果你研究過(guò)"a".trim()這樣的代碼的話,不知道是否產(chǎn)生過(guò)這樣的疑問(wèn):"a"明明是一個(gè)原始值(字符串),它為什么可以直接調(diào)用.trim()方法呢?當(dāng)然,很可能你已經(jīng)知道答案:因?yàn)镴S在執(zhí)行這樣的代碼的時(shí)候會(huì)對(duì)原始值做一次包裝,讓"a"變成一個(gè)字符串對(duì)象,然后執(zhí)行這個(gè)對(duì)象的方法,執(zhí)行完之后再把這個(gè)包裝對(duì)象脫掉??梢杂孟旅娴拇a來(lái)理解:

// "a".trim();
var tmp = new String("a");
tmp.trim();

這段代碼只是輔助我們理解的。但包裝對(duì)象這個(gè)概念在JS中卻是真實(shí)存在的。

var a = new String("a");
var b = "b";

a即是一個(gè)包裝對(duì)象,它和b一樣,代表一個(gè)字符串。它們都可以使用字符串的各種方法(比如trim()),也可以參與字符串運(yùn)算(+號(hào)連接等)。

但他們有一個(gè)關(guān)鍵的區(qū)別:類(lèi)型不同!

typeof a; // object
typeof b; // string

在做字符串比較的時(shí)候,類(lèi)型的不同會(huì)導(dǎo)致結(jié)果有一些出乎意料:

var a1 = "a";
var a2 = new String("a");
var a3 = new String("a");

a1 == a2; // true
a1 == a3; // true
a2 == a3; // false
a1 === a2; // false
a1 === a3; // false
a2 === a3; // false

同樣是表示字符串a的變量,在使用嚴(yán)格比較時(shí)竟然不是相等的,在直覺(jué)上這是一件比較難接受的事情,在各種開(kāi)發(fā)場(chǎng)景下,也非常容易忽略這些細(xì)節(jié)。

對(duì)象和對(duì)象

在涉及比較的時(shí)候,還會(huì)碰到對(duì)象。具體而言,大致可以分為三種情況:純對(duì)象、實(shí)例對(duì)象、其它類(lèi)型的對(duì)象。

純對(duì)象

純對(duì)象(plain object)具體指什么并不是非常明確,為減少不必要的爭(zhēng)議,下文中使用純對(duì)象指代由字面量生成的、成員中不含函數(shù)和日期、正則表達(dá)式等類(lèi)型的對(duì)象。

如果直接拿兩個(gè)對(duì)象進(jìn)行比較,不管是==還是===,毫無(wú)疑問(wèn)都是不相等的。但是在實(shí)際使用時(shí),這樣的規(guī)則是否一定滿足我們的需求?舉個(gè)例子,我們的應(yīng)用中有兩個(gè)配置項(xiàng):

// 原來(lái)有兩個(gè)屬性
// var prop1 = 1;
// var prop2 = 2;

// 重構(gòu)代碼時(shí)兩個(gè)屬性被放到同一個(gè)對(duì)象中

var config = {
    prop1: 1,
    prop2: 2
};

假設(shè)在某些場(chǎng)景下,我們需要比較兩次運(yùn)行的配置項(xiàng)是否相同。在重構(gòu)前,我們分別比較兩次運(yùn)行的prop1prop2即可。而在重構(gòu)后,我們可能需要比較config對(duì)象所代表的配置項(xiàng)是否一致。在這樣的場(chǎng)景下,直接用==或者===來(lái)比較對(duì)象,得到的并不是我們期望的結(jié)果。

在這樣的場(chǎng)景下,我們可能需要自定義一些方法來(lái)處理對(duì)象的比較。常見(jiàn)的可能是通過(guò)JSON.stringify()對(duì)對(duì)象進(jìn)行序列化之后再比較字符串,當(dāng)然這個(gè)過(guò)程并非完全可靠,只是一個(gè)思路。

如果你覺(jué)得這個(gè)場(chǎng)景是無(wú)中生有的話,可以再回想一下斷言庫(kù),同樣是基于對(duì)象成員,判斷結(jié)果是否和預(yù)期相符。

實(shí)例對(duì)象

實(shí)例對(duì)象主要指通過(guò)構(gòu)造函數(shù)(類(lèi))生成的對(duì)象。這樣的對(duì)象和純對(duì)象一樣,直接比較都是不等的,但也會(huì)碰到需要判斷是否是同一對(duì)象的情況。一般而言,因?yàn)檫@種對(duì)象有比較復(fù)雜的內(nèi)部結(jié)構(gòu)(甚至有一部分?jǐn)?shù)據(jù)在原型上),無(wú)法直接從外部比較是否相等。比較靠譜的判斷方法是由構(gòu)造函數(shù)(類(lèi))來(lái)提供靜態(tài)方法或者實(shí)例方法來(lái)判斷是否相等。

var a = Klass();
var b = Klass();

Klass.isEqual(a, b);

其它對(duì)象

其它對(duì)象主要指數(shù)組、日期、正則表達(dá)式等這類(lèi)在Object基礎(chǔ)上派生出來(lái)的對(duì)象。這類(lèi)對(duì)象各有各的特殊性,一般需要根據(jù)場(chǎng)景來(lái)構(gòu)造判斷方法,決定兩個(gè)對(duì)象是否相等。

比如,日期對(duì)象,可能需要通過(guò)Date.prototype.getTime()方法獲取時(shí)間戳來(lái)判斷是否表示同一時(shí)刻。正則表達(dá)式可能需要通過(guò)toString()方法獲取到原始字面量來(lái)判斷是否是相同的正則表達(dá)式。

==和===

在一些文章中,看到某一些數(shù)組去重的方法,在判斷元素是否相等時(shí),使用的是==比較運(yùn)算符。眾所周知,這個(gè)運(yùn)算符在比較前會(huì)先查看元素類(lèi)型,當(dāng)類(lèi)型不一致時(shí)會(huì)做隱式類(lèi)型轉(zhuǎn)換。這其實(shí)是一種非常不嚴(yán)謹(jǐn)?shù)淖龇?。因?yàn)闊o(wú)法區(qū)分在做隱匿類(lèi)型轉(zhuǎn)換后值一樣的元素,例如0、""、false、nullundefined等。

同時(shí),還有可能出現(xiàn)一些只能黑人問(wèn)號(hào)的結(jié)果,例如:

[] == ![]; //true
Array.prototype.indexOf()

在一些版本的去重中,用到了Array.prototype.indexOf()方法:

function unique(arr) {
    return arr.filter(function(item, index){
        // indexOf返回第一個(gè)索引值,
        // 如果當(dāng)前索引不是第一個(gè)索引,說(shuō)明是重復(fù)值
        return arr.indexOf(item) === index;
    });
}
function unique(arr) {
    var ret = [];
    arr.forEach(function(item){
        if(ret.indexOf(item) === -1){
            ret.push(item);
        }
    });
    return ret;
}

既然=====在元素相等的比較中是有巨大差別的,那么indexOf的情況又如何呢?大部分的文章都沒(méi)有提及這點(diǎn),于是只好求助規(guī)范。通過(guò)規(guī)范(http://www.ecma-international...),我們知道了indexOf()使用的是嚴(yán)格比較,也就是===

再次強(qiáng)調(diào):按照前文所述,===不能處理NaN的相等性判斷。

Array.prototype.includes()

Array.prototype.includes()是ES2016中新增的方法,用于判斷數(shù)組中是否包含某個(gè)元素,所以上面使用indexOf()方法的第二個(gè)版本可以改寫(xiě)成如下版本:

function unique(arr) {
    var ret = [];
    arr.forEach(function(item){
        if(!ret.includes(item)){
            ret.push(item);
        }
    });
    return ret;
}

那么,你猜猜,includes()又是用什么方法來(lái)比較的呢?如果想當(dāng)然的話,會(huì)覺(jué)得肯定跟indexOf()一樣嘍。但是,程序員的世界里最怕想當(dāng)然。翻一翻規(guī)范,發(fā)現(xiàn)它其實(shí)是使用的另一種比較方法,叫作“SameValueZero”比較(https://tc39.github.io/ecma26...)。

If Type(x) is different from Type(y), return false.

If Type(x) is Number, then

If x is NaN and y is NaN, return true.

If x is +0 and y is -0, return true.

If x is -0 and y is +0, return true.

If x is the same Number value as y, return true.

Return false.

Return SameValueNonNumber(x, y).

注意2.a,如果xy都是NaN,則返回true!也就是includes()是可以正確判斷是否包含了NaN的。我們寫(xiě)一段代碼驗(yàn)證一下:

var arr = [1, 2, NaN];
arr.indexOf(NaN); // -1
arr.includes(NaN); // true

可以看到indexOf()includes()對(duì)待NaN的行為是完全不一樣的。

一些方案

從上面的一大段文字中,我們可以看到,要判斷兩個(gè)元素是否相等(重復(fù))并不是一件簡(jiǎn)單的事情。在了解了這個(gè)背景后,我們來(lái)看一些前面沒(méi)有涉及到的去重方案。

遍歷

雙重遍歷是最容易想到的去重方案:

function unique(arr) {
    var ret = [];
    var len = arr.length;
    var isRepeat;
    for(var i=0; i

雙重遍歷還有一個(gè)優(yōu)化版本,但是原理和復(fù)雜度幾乎完全一樣:

function unique(arr) {
    var ret = [];
    var len = arr.length;
    for(var i=0; i

這種方案沒(méi)什么大問(wèn)題,用于去重的比較部分也是自己編寫(xiě)實(shí)現(xiàn)(arr[i] === arr[j]),所以相等性可以自己針對(duì)上文說(shuō)到的各種情況加以特殊處理。唯一比較受詬病的是使用了雙重循環(huán),時(shí)間復(fù)雜度比較高,性能一般。

使用對(duì)象key來(lái)去重
function unique(arr) {
    var ret = [];
    var len = arr.length;
    var tmp = {};
    for(var i=0; i

這種方法是利用了對(duì)象(tmp)的key不可以重復(fù)的特性來(lái)進(jìn)行去重。但由于對(duì)象key只能為字符串,因此這種去重方法有許多局限性:

無(wú)法區(qū)分隱式類(lèi)型轉(zhuǎn)換成字符串后一樣的值,比如1"1"

無(wú)法處理復(fù)雜數(shù)據(jù)類(lèi)型,比如對(duì)象(因?yàn)閷?duì)象作為key會(huì)變成[object Object]

特殊數(shù)據(jù),比如"__proto__"會(huì)掛掉,因?yàn)?b>tmp對(duì)象的__proto__屬性無(wú)法被重寫(xiě)

對(duì)于第一點(diǎn),有人提出可以為對(duì)象的key增加一個(gè)類(lèi)型,或者將類(lèi)型放到對(duì)象的value中來(lái)解決:

function unique(arr) {
    var ret = [];
    var len = arr.length;
    var tmp = {};
    var tmpKey;
    for(var i=0; i

該方案也同時(shí)解決第三個(gè)問(wèn)題。

而第二個(gè)問(wèn)題,如果像上文所說(shuō),在允許對(duì)對(duì)象進(jìn)行自定義的比較規(guī)則,也可以將對(duì)象序列化之后作為key來(lái)使用。這里為簡(jiǎn)單起見(jiàn),使用JSON.stringify()進(jìn)行序列化。

function unique(arr) {
    var ret = [];
    var len = arr.length;
    var tmp = {};
    var tmpKey;
    for(var i=0; i
Map Key

可以看到,使用對(duì)象key來(lái)處理數(shù)組去重的問(wèn)題,其實(shí)是一件比較麻煩的事情,處理不好很容易導(dǎo)致結(jié)果不正確。而這些問(wèn)題的根本原因就是因?yàn)閗ey在使用時(shí)有限制。

那么,能不能有一種key使用沒(méi)有限制的對(duì)象呢?答案是——真的有!那就是ES2015中的Map

Map是一種新的數(shù)據(jù)類(lèi)型,可以把它想象成key類(lèi)型沒(méi)有限制的對(duì)象。此外,它的存取使用多帶帶的get()、set()接口。

var tmp = new Map();
tmp.set(1, 1);
tmp.get(1); // 1

tmp.set("2", 2);
tmp.get("2"); // 2

tmp.set(true, 3);
tmp.get(true); // 3

tmp.set(undefined, 4);
tmp.get(undefined); // 4

tmp.set(NaN, 5);
tmp.get(NaN); // 5

var arr = [], obj = {};

tmp.set(arr, 6);
tmp.get(arr); // 6

tmp.set(obj, 7);
tmp.get(obj); // 7

由于Map使用多帶帶的接口來(lái)存取數(shù)據(jù),所以不用擔(dān)心key會(huì)和內(nèi)置屬性重名(如上文提到的__proto__)。使用Map改寫(xiě)一下我們的去重方法:

function unique(arr) {
    var ret = [];
    var len = arr.length;
    var tmp = new Map();
    for(var i=0; i
Set

既然都用到了ES2015,數(shù)組這件事情不能再簡(jiǎn)單一點(diǎn)么?當(dāng)然可以。

除了Map以外,ES2015還引入了一種叫作Set的數(shù)據(jù)類(lèi)型。顧名思義,Set就是集合的意思,它不允許重復(fù)元素出現(xiàn),這一點(diǎn)和數(shù)學(xué)中對(duì)集合的定義還是比較像的。

var s = new Set();
s.add(1);
s.add("1");
s.add(null);
s.add(undefined);
s.add(NaN);
s.add(true);
s.add([]);
s.add({});

如果你重復(fù)添加同一個(gè)元素的話,Set中只會(huì)存在一個(gè)。包括NaN也是這樣。于是我們想到,這么好的特性,要是能和數(shù)組互相轉(zhuǎn)換,不就可以去重了嗎?

function unique(arr){
    var set = new Set(arr);
    return Array.from(set);
}

我們討論了這么久的事情,居然兩行代碼搞定了,簡(jiǎn)直不可思議。

然而,不要只顧著高興了。有一句話是這么說(shuō)的“不要因?yàn)樽叩锰h(yuǎn)而忘了為什么出發(fā)”。我們?yōu)槭裁匆獮閿?shù)組去重呢?因?yàn)槲覀兿氲玫讲恢貜?fù)的元素列表。而既然已經(jīng)有Set了,我們?yōu)槭裁催€要舍近求遠(yuǎn),使用數(shù)組呢?是不是在需要去重的情況下,直接使用Set就解決問(wèn)題了?這個(gè)問(wèn)題值得思考。

小結(jié)

最后,用一個(gè)測(cè)試用例總結(jié)一下文中出現(xiàn)的各種去重方法:

var arr = [1,1,"1","1",0,0,"0","0",undefined,undefined,null,null,NaN,NaN,{},{},[],[],/a/,/a/]
console.log(unique(arr));

測(cè)試中沒(méi)有定義對(duì)象的比較方法,因此默認(rèn)情況下,對(duì)象不去重是正確的結(jié)果,去重是不正確的結(jié)果。

方法 結(jié)果 說(shuō)明
indexOf#1 NaN被去掉
indexOf#2 NaN重復(fù)
includes 正確
雙重循環(huán)#1 NaN重復(fù)
雙重循環(huán)#2 NaN重復(fù)
對(duì)象#1 字符串和數(shù)字無(wú)法區(qū)分,對(duì)象、數(shù)組、正則表達(dá)式被去重
對(duì)象#2 對(duì)象、數(shù)組、正則表達(dá)式被去重
對(duì)象#3 對(duì)象、數(shù)組被去重,正則表達(dá)式被消失 JSON.stringify(/a/)結(jié)果為{},和空對(duì)象一樣
Map 正確  
Set 正確  

最后的最后:任何脫離場(chǎng)景談技術(shù)都是妄談,本文也一樣。去重這道題,沒(méi)有正確答案,請(qǐng)根據(jù)場(chǎng)景選擇合適的去重方法。

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/88088.html

相關(guān)文章

  • 也談面試必備問(wèn)題之 JavaScript 數(shù)組去重

    摘要:而數(shù)組元素去重是基于運(yùn)算符的。而如果有迭代函數(shù),則計(jì)算傳入迭代函數(shù)后的值,對(duì)值去重,調(diào)用方法,而該方法的核心就是調(diào)用方法,和我們上面說(shuō)的方法一異曲同工。 Why underscore (覺(jué)得這部分眼熟的可以直接跳到下一段了...) 最近開(kāi)始看 underscore.js 源碼,并將 underscore.js 源碼解讀 放在了我的 2016 計(jì)劃中。 閱讀一些著名框架類(lèi)庫(kù)的源碼,就好像...

    Coly 評(píng)論0 收藏0
  • 案例 - 收藏集 - 掘金

    摘要:同行這么做使用實(shí)現(xiàn)圓形進(jìn)度條前端掘金在開(kāi)發(fā)微信小程序的時(shí)候,遇到圓形進(jìn)度條的需求。實(shí)現(xiàn)也談數(shù)組去重前端掘金的數(shù)組去重是一個(gè)老生常談的話題了。百度前端技術(shù)學(xué)院自定義前端掘金一標(biāo)簽概念元素表示用戶界面中項(xiàng)目的標(biāo)題。 閑話圖片上傳 - 掘金作者:孫輝,美團(tuán)金融前端團(tuán)隊(duì)成員。15年畢業(yè)加入美團(tuán),相信技術(shù),更相信技術(shù)只是大千世界里知識(shí)的一種,個(gè)人博客: https://sunyuhui.com ...

    張金寶 評(píng)論0 收藏0
  • 案例 - 收藏集 - 掘金

    摘要:同行這么做使用實(shí)現(xiàn)圓形進(jìn)度條前端掘金在開(kāi)發(fā)微信小程序的時(shí)候,遇到圓形進(jìn)度條的需求。實(shí)現(xiàn)也談數(shù)組去重前端掘金的數(shù)組去重是一個(gè)老生常談的話題了。百度前端技術(shù)學(xué)院自定義前端掘金一標(biāo)簽概念元素表示用戶界面中項(xiàng)目的標(biāo)題。 閑話圖片上傳 - 掘金作者:孫輝,美團(tuán)金融前端團(tuán)隊(duì)成員。15年畢業(yè)加入美團(tuán),相信技術(shù),更相信技術(shù)只是大千世界里知識(shí)的一種,個(gè)人博客: https://sunyuhui.com ...

    huangjinnan 評(píng)論0 收藏0
  • Underscore 源碼(二)常用思路和類(lèi)型判斷

    摘要:返回值是一個(gè)新數(shù)組,思路也很清楚,對(duì)于已經(jīng)排好序的數(shù)組,用后一個(gè)和前一個(gè)相比,不一樣就到中,對(duì)于沒(méi)有排好序的數(shù)組,要用到函數(shù)對(duì)是否包含元素進(jìn)行判斷。 前面已經(jīng)介紹過(guò)了,關(guān)于 _ 在內(nèi)部是一個(gè)什么樣的情況,其實(shí)就是定義了一個(gè)名字叫做 _ 的函數(shù),函數(shù)本身就是對(duì)象呀,就在 _ 上擴(kuò)展了 100 多種方法。 showImg(https://segmentfault.com/img/remot...

    mayaohua 評(píng)論0 收藏0
  • 也談前端面試常見(jiàn)問(wèn)題之『數(shù)組亂序』

    摘要:看完部分的源碼,首先迫不及待想跟大家分享的正是本文主題數(shù)組亂序。這是一道經(jīng)典的前端面試題,給你一個(gè)數(shù)組,將其打亂,返回新的數(shù)組,即為數(shù)組亂序,也稱(chēng)為洗牌問(wèn)題。關(guān)于數(shù)組亂序,正確的解法應(yīng)該是,復(fù)雜度。 前言 終于可以開(kāi)始 Collection Functions 部分了。 可能有的童鞋是第一次看樓主的系列文章,這里再做下簡(jiǎn)單的介紹。樓主在閱讀 underscore.js 源碼的時(shí)候,學(xué)到...

    tracy 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<