1、為什么要判斷?
10年積累的網(wǎng)站建設(shè)、成都做網(wǎng)站經(jīng)驗,可以快速應(yīng)對客戶對網(wǎng)站的新想法和需求。提供各種問題對應(yīng)的解決方案。讓選擇我們的客戶得到更好、更有力的網(wǎng)絡(luò)服務(wù)。我雖然不認識你,你也不認識我。但先網(wǎng)站設(shè)計后付款的網(wǎng)站建設(shè)流程,更有貴陽免費網(wǎng)站建設(shè)讓你可以放心的選擇與我們合作。
可能有些同學看到這個標題就會產(chǎn)生疑惑,為什么我們要判斷JavaScript中的兩個變量是否相等,JavaScript不是已經(jīng)提供了雙等號“==”以及三等號“===”給我們使用了嗎?
其實,JavaScript雖然給我們提供了相等運算符,但是還是存在一些缺陷,這些缺陷不符合我們的思維習慣,有可能在使用的時候得到一些意外的結(jié)果。為了避免這種情況的出現(xiàn),我們需要自己函數(shù)來實現(xiàn)JavaScript變量之間的對比。
2、JavaScript等號運算符存在哪些缺陷?
2.1 0與-0
在JavaScript中:
0 === 0
//true
+0 === -0
//true
相等運算符認為+0和-0是相等的,但是我們應(yīng)當認為兩者是不等的,具體原因源碼中給出了一個鏈接:Harmony egal proposal.
2.2 null和undefined
在JavaScript中:
null == undefined
//true
null === undefined
//false
我們應(yīng)當認為null不等于undefined,所以在比較null和undefined時,應(yīng)當返回false。
2.3 NaN
前文有說過,NaN是一個特殊的值,它是JavaScript中唯一一個自身不等于自身的值。
NaN == NaN
//false
NaN === NaN
//false
但是我們在對比兩個NaN時,我們應(yīng)當認為它們是相等的。
2.4 數(shù)組之間的對比
由于在JavaScript中,數(shù)組是一個對象,所以如果兩個變量不是引用的同一個數(shù)組的話,即使兩個數(shù)組一模一樣也不會返回true。
var a = [];
//undefined
var b = [];
//undefined
a=== b
//false
a==b
//false
但是我們應(yīng)當認為,兩個元素位置、順序以及值相同的數(shù)組是相等的。
2.5 對象之間的對比
凡是涉及到對象的變量,只要不是引用同一個對象,都會被認為不相等。我們需要做出一些改變,兩個完全一致的對象應(yīng)當被認為是相等的。
var a = {};
//undefined
var b = {};
//undefined
a == b
//false
a === b
//false
這種情況在所有JavaScript內(nèi)置對象中也適用,比如我們應(yīng)當認為兩個一樣的RegExp對象是相等的。
2.6 基本數(shù)據(jù)類型與包裝數(shù)據(jù)類型之間的對比
在JavaScript中,數(shù)值2和Number對象2是不嚴格相等的:
2 == new Number(2);
//true
2 === new Number(2);
//false
但是我們在對比2和new Number(2)時應(yīng)當認為兩者相等。
3 underscore的實現(xiàn)方法
我們實現(xiàn)的方法當然還是依賴于JavaScript相等運算符的,只不過針對特例需要有特定的處理。我們在比較之前,首先應(yīng)該做的就是處理特殊情況。
underscore的代碼中,沒有直接將邏輯寫在_.isEqual方法中,而是定義了兩個私有方法:eq和deepEq。在GitHub用戶@hanzichi的repo中,我們可以看到1.8.3版本的underscore中并沒有deepEq方法,為什么后來添加了呢?這是因為underscore的作者把一些特例的處理提取了出來,放到了eq方法中,而更加復(fù)雜的對象之間的對比被放到了deepEq中(同時使得deepEq方法更加便于遞歸調(diào)用)。這樣的做法使得代碼邏輯更加鮮明,方法的功能也更加單一明確,維護代碼更加簡潔快速。
eq方法的源代碼:
var eq = function (a, b, aStack, bStack) { // Identical objects are equal. `0 === -0`, but they aren't identical. // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). //除了0 === -0這個特例之外,其余所有a === b的例子都代表它們相等。 //應(yīng)當判斷0 !== -0,但是JavaScript中0 === -0。 //下面這行代碼就是為了解決這個問題。 //當a !== 0或者1/a === 1/b時返回true,一旦a === 0并且1/a !== 1/b就返回false。 //而a === 0且1/a !== 1/b就代表a,b有一個為0,有一個為-0。 if (a === b) return a !== 0 || 1 / a === 1 / b; //一旦a、b不嚴格相等,就進入后續(xù)檢測。 //a == b成立但是a === b不成立的例子中需要排除null和undefined,其余例子需要后續(xù)判斷。 // `null` or `undefined` only equal to itself (strict comparison). //一旦a或者b中有一個為null就代表另一個為undefined,這種情況可以直接排除。 if (a == null || b == null) return false; // `NaN`s are equivalent, but non-reflexive. //自身不等于自身的情況,一旦a,b都為NaN,則可以返回true。 if (a !== a) return b !== b; // Exhaust primitive checks //如果a,b都不為JavaScript對象,那么經(jīng)過以上監(jiān)測之后還不嚴格相等的話就可以直接斷定a不等于b。 var type = typeof a; if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; //如果a,b是JavaScript對象,還需要做后續(xù)深入的判斷。 return deepEq(a, b, aStack, bStack); };
對于源碼的解讀我已經(jīng)作為注釋寫在了源碼中。 那么根據(jù)源碼,可以將其邏輯抽象出來:
deepEq的源碼:
var deepEq = function (a, b, aStack, bStack) { // Unwrap any wrapped objects. //如果a,b是_的一個實例的話,需要先把他們解包出來再進行比較。 if (a instanceof _) a = a._wrapped; if (b instanceof _) b = b._wrapped; // Compare `[[Class]]` names. //先根據(jù)a,b的Class字符串進行比較,如果兩個對象的Class字符串都不一樣, //那么直接可以認為兩者不相等。 var className = toString.call(a); if (className !== toString.call(b)) return false; //如果兩者的Class字符串相等,再進一步進行比較。 //優(yōu)先檢測內(nèi)置對象之間的比較,非內(nèi)置對象再往后檢測。 switch (className) { // Strings, numbers, regular expressions, dates, and booleans are compared by value. //如果a,b為正則表達式,那么轉(zhuǎn)化為字符串判斷是否相等即可。 case '[object RegExp]': // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') case '[object String]': // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is // equivalent to `new String("5")`. //如果a, b是字符串對象,那么轉(zhuǎn)化為字符串進行比較。因為一下兩個變量: //var x = new String('12'); //var y = new String('12'); //x === y是false,x === y也是false,但是我們應(yīng)該認為x與y是相等的。 //所以我們需要將其轉(zhuǎn)化為字符串進行比較。 return '' + a === '' + b; case '[object Number]': //數(shù)字對象轉(zhuǎn)化為數(shù)字進行比較,并且要考慮new Number(NaN) === new Number(NaN)應(yīng)該要成立的情況。 // `NaN`s are equivalent, but non-reflexive. // Object(NaN) is equivalent to NaN. if (+a !== +a) return +b !== +b; // An `egal` comparison is performed for other numeric values. //排除0 === -0 的情況。 return +a === 0 ? 1 / +a === 1 / b : +a === +b; case '[object Date]': //Date類型以及Boolean類型都可以轉(zhuǎn)換為number類型進行比較。 //在變量前加一個加號“+”,可以強制轉(zhuǎn)換為數(shù)值型。 //在Date型變量前加一個加號“+”可以將Date轉(zhuǎn)化為毫秒形式;Boolean類型同上(轉(zhuǎn)換為0或者1)。 case '[object Boolean]': // Coerce dates and booleans to numeric primitive values. Dates are compared by their // millisecond representations. Note that invalid dates with millisecond representations // of `NaN` are not equivalent. return +a === +b; case '[object Symbol]': return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b); } var areArrays = className === '[object Array]'; //如果不是數(shù)組對象。 if (!areArrays) { if (typeof a != 'object' || typeof b != 'object') return false; // Objects with different constructors are not equivalent, but `Object`s or `Array`s // from different frames are. //比較兩個非數(shù)組對象的構(gòu)造函數(shù)。 var aCtor = a.constructor, bCtor = b.constructor; if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor && _.isFunction(bCtor) && bCtor instanceof bCtor) && ('constructor' in a && 'constructor' in b)) { return false; } } // Assume equality for cyclic structures. The algorithm for detecting cyclic // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. // Initializing stack of traversed objects. // It's done here since we only need them for objects and arrays comparison. //初次調(diào)用eq函數(shù)時,aStack以及bStack均未被傳遞,在循環(huán)遞歸的時候,會被傳遞進來。 //aStack和bStack存在的意義在于循環(huán)引用對象之間的比較。 aStack = aStack || []; bStack = bStack || []; var length = aStack.length; while (length--) { // Linear search. Performance is inversely proportional to the number of // unique nested structures. if (aStack[length] === a) return bStack[length] === b; } // Add the first object to the stack of traversed objects. //初次調(diào)用eq函數(shù)時,就把兩個參數(shù)放入到參數(shù)堆棧中去,保存起來方便遞歸調(diào)用時使用。 aStack.push(a); bStack.push(b); // Recursively compare objects and arrays. //如果是數(shù)組對象。 if (areArrays) { // Compare array lengths to determine if a deep comparison is necessary. length = a.length; //長度不等,直接返回false認定為數(shù)組不相等。 if (length !== b.length) return false; // Deep compare the contents, ignoring non-numeric properties. while (length--) { //遞歸調(diào)用。 if (!eq(a[length], b[length], aStack, bStack)) return false; } } else { // Deep compare objects. //對比純對象。 var keys = _.keys(a), key; length = keys.length; // Ensure that both objects contain the same number of properties before comparing deep equality. //對比屬性數(shù)量,如果數(shù)量不等,直接返回false。 if (_.keys(b).length !== length) return false; while (length--) { // Deep compare each member key = keys[length]; if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false; } } // Remove the first object from the stack of traversed objects. //循環(huán)遞歸結(jié)束,把a,b堆棧中的元素推出。 aStack.pop(); bStack.pop(); return true; };
對于源碼的解讀我已經(jīng)作為注釋寫在了源碼中。 那么根據(jù)源碼,可以將其邏輯抽象出來:
1 使用Object.prototype.toString方法獲取兩參數(shù)類型,如果兩參數(shù)的原始數(shù)據(jù)類型都不同,那么可以認為兩個參數(shù)不相等。
2 如果進入了第二步,那么說明兩個參數(shù)的原始類型相同。針對獲取到的字符串進行分類,如果是除Object和Array之外的類型,進行處理。RegExp以及String對象轉(zhuǎn)化為字符串進行比較。
Number類型的話,需要先使用+運算符強制轉(zhuǎn)化為基本數(shù)據(jù)類型中的數(shù)值型,然后處理特例。比如NaN === NaN,0 !== -0.
Date以及Boolean對象轉(zhuǎn)化為數(shù)字類型進行對比。(+運算符強制轉(zhuǎn)換,Date轉(zhuǎn)化為13位的毫秒形式,Boolean轉(zhuǎn)化為0或1)
Symbol類型使用Symbol.prototype.valueOf獲取字符串,然后進行對比(即認為傳遞給Symbol函數(shù)相同字符串所獲取到的Symbol對象應(yīng)該相等)。3 經(jīng)過以上比較,所剩類型基本只剩Array和基本對象了。如果不是數(shù)組對象,那么構(gòu)造函數(shù)不同的對象可以被認為是不相等的對象。
4 初始化對象棧aStack以及bStack,因為初次調(diào)用deepEq函數(shù)時不會傳遞這兩個參數(shù),所以需要手動初始化。因為之后比較的數(shù)組對象以及基本對象需要用到對象棧,所以現(xiàn)在應(yīng)該把當前的a,b推入到兩個棧中。
5 針對數(shù)組,先比較長度,長度不等則數(shù)組不等。長度相等再遞歸調(diào)用deepGet比較數(shù)組的每一項,有一項不等則返回false。
6 基本對象類型比較,先使用_.keys獲取對象的所有鍵。鍵數(shù)量不同的兩對象不同,如果鍵數(shù)目相等,再遞歸調(diào)用deepEq比較每一個鍵的屬性,有一個鍵值不等則返回false。
7 經(jīng)過所有檢測如果都沒有返回false的話,可以認為兩參數(shù)相等,返回true。在返回之前會把棧中的數(shù)據(jù)推出一個。
4 underscore的精髓
4.1 將RegExp對象和String對象用相同方法處理
有同學可能會疑惑:/[a-z]/gi與/[a-z]ig/在意義上是一樣的,但是轉(zhuǎn)化為字符串之后比較會不會是不相等的?
這是一個非常好的問題,同時也是underscore處理的巧妙之所在。在JavaScript中,RegExp對象重寫了toString方法,所以在強制將RegExp對象轉(zhuǎn)化為字符串時,flags會按規(guī)定順序排列,所以將之前兩個RegExp對象轉(zhuǎn)化為字符串,都會得到/[a-z]/gi。這就是underscore可以放心大膽的將RegExp對象轉(zhuǎn)化為字符串處理的原因。
4.2 Date對象和Boolean對象使用相同方法處理
underscore選擇將Date對象和Boolean對象都轉(zhuǎn)化為數(shù)值進行處理,這避免了紛繁復(fù)雜的類型轉(zhuǎn)換,簡單粗暴。而且作者沒有使用強制轉(zhuǎn)換方法進行轉(zhuǎn)換,而是只使用了一個“+”符號,就強制將Date對象和Boolean對象轉(zhuǎn)換成了數(shù)值型數(shù)據(jù)。
4.3 使用對象棧保存當前比較對象的上下文
很多童鞋在閱讀源碼時,可能會很疑惑aStack以及bStack的作用在哪里。aStack和bStack用于保存當前比較對象的上下文,這使得我們在比較某個對象的子屬性時,還可以獲取到其自身。這樣做的好處就在于我們可以比較循環(huán)引用的對象。
var a = { name: 'test' }; a['test1'] = a; var b = { name: 'test' }; b['test1'] = b; _.isEqual(a, b); //true
underscore使用aStack和bStack作比較的代碼:
aStack = aStack || []; bStack = bStack || []; var length = aStack.length; while (length--) { // Linear search. Performance is inversely proportional to the number of // unique nested structures. if (aStack[length] === a) return bStack[length] === b; }
上面的測試代碼中,a、b對象的test1屬性都引用了它們自身,這樣的對象在比較時會消耗不必要的時間,因為只要a和b的test1屬性都等于其某個父對象,那么可以認為a和b相等,因為這個被遞歸的方法返回之后,還要繼續(xù)比較它們對應(yīng)的那個父對象,父對象相等,則引用的對象屬性必相等,這樣的處理方法節(jié)省了很多的時間,也提高了underscore的性能。
4.4 優(yōu)先級分明,有的放矢
underscore的處理具有很強的優(yōu)先級,比如在比較數(shù)組對象時,先比較數(shù)組的長度,數(shù)組長度不相同則數(shù)組必定不相等;比如在比較基本對象時,優(yōu)先比較對象鍵的數(shù)目,鍵數(shù)目不等則對象必定不等;比如在比較兩個對象參數(shù)之前,優(yōu)先對比Object.prototype.toString返回的字符串,如果基本類型不同,那么兩個對象必定不相等。
這樣的主次分明的對比,大大提高了underscore的工作效率。所以說每一個小小的細節(jié),都可以體現(xiàn)出作者的處心積慮。閱讀源碼,能夠使我們學習到太多的東西。
5 underscore的缺陷之處
我們可以在其他方法中看到underscore對ES6中新特征的支持,比如_.is[Type]方法已經(jīng)支持檢測Map(_.isMap)和Set(_.isSet)等類型了。但是_.isEqual卻沒有對Set和Map結(jié)構(gòu)的支持。如果我們使用_.isEqual比較兩個Map或者兩個Set,總是會得到true的結(jié)果,因為它們可以通過所有的檢測。
在underscore的官方GitHub repo上,我看到有同學已經(jīng)提交了PR添加了_.isEqual對Set和Map的支持。
我們可以看一下源碼:
var size = a.size; // Ensure that both objects are of the same size before comparing deep equality. if (b.size !== size) return false; while (size--) { // Deep compare the keys of each member, using SameValueZero (isEq) for the keys if (!(isEq(a.keys().next().value, b.keys().next().value, aStack, bStack))) return false; // If the objects are maps deep compare the values. Value equality does not use SameValueZero. if (className === '[object Map]') { if (!(eq(a.values().next().value, b.values().next().value, aStack, bStack))) return false; } }
可以看到其思路如下:
1 比較兩參數(shù)的長度(或者說是鍵值對數(shù)),長度不一者即為不等,返回false。
2 如果長度相等,就逐一遞歸比較它們的每一項,有任意一項不等者就返回false。
3 全部通過則可以認為是相等的,返回true。
這段代碼有一個很巧妙的地方在于它沒有區(qū)分到底是Map對象還是Set對象,先直接使用a.keys().next().value以及b.keys().next().value獲取Set的元素值或者Map的鍵。后面再進行類型判斷,如果是Map對象的話,再使用a.values().next().value以及b.values().next().value獲取Map的鍵值,Map對象還需要比較其鍵值是否相等。
個人認為,這段代碼也有其局限性,因為Set和Map可以認為是一個數(shù)據(jù)集,這區(qū)別于數(shù)組對象。我們可以說[1,2,3]不等于[2,1,3],因為其相同元素的位置不同;但是我認為new Set([1,2,3])應(yīng)該認為等于new Set([2,1,3]),因為Set是無序的,它內(nèi)部的元素具有單一性。
獲取更多underscore源碼解讀:GitHub
好了這篇文章就介紹這么多,更多關(guān)于js相等操作符請看下面的相關(guān)文章。
當前文章:判斷JavaScript中的兩個變量是否相等的操作符
文章出自:http://bm7419.com/article34/jciese.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供搜索引擎優(yōu)化、靜態(tài)網(wǎng)站、網(wǎng)站制作、品牌網(wǎng)站建設(shè)、網(wǎng)站內(nèi)鏈、商城網(wǎng)站
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)