JavaScript中如何判斷兩變量是否「相等」?

1 爲何要判斷?

可能有些同窗看到這個標題就會產生疑惑,爲何咱們要判斷JavaScript中的兩個變量是否相等,JavaScript不是已經提供了雙等號「==」以及三等號「===」給咱們使用了嗎?javascript

其實,JavaScript雖然給咱們提供了相等運算符,可是仍是存在一些缺陷,這些缺陷不符合咱們的思惟習慣,有可能在使用的時候獲得一些意外的結果。爲了不這種狀況的出現,咱們須要本身函數來實現JavaScript變量之間的對比。php

2 JavaScript等號運算符存在哪些缺陷?

2.1 0與-0

在JavaScript中:java

0 === 0
//true
+0 === -0
//true

  

相等運算符認爲+0和-0是相等的,可是咱們應當認爲二者是不等的,具體緣由源碼中給出了一個連接:Harmony egal proposal.git

2.2 null和undefined

在JavaScript中:github

null == undefined
//true
null === undefined
//false

  

咱們應當認爲null不等於undefined,因此在比較null和undefined時,應當返回false。正則表達式

2.3 NaN

前文有說過,NaN是一個特殊的值,它是JavaScript中惟一一個自身不等於自身的值。express

NaN == NaN
//false
NaN === NaN
//false

  

可是咱們在對比兩個NaN時,咱們應當認爲它們是相等的。數組

2.4 數組之間的對比

因爲在JavaScript中,數組是一個對象,因此若是兩個變量不是引用的同一個數組的話,即便兩個數組如出一轍也不會返回true。app

var a = [];
//undefined
var b = [];
//undefined
a=== b
//false
a==b
//false

  

可是咱們應當認爲,兩個元素位置、順序以及值相同的數組是相等的。ecmascript

2.5 對象之間的對比

凡是涉及到對象的變量,只要不是引用同一個對象,都會被認爲不相等。咱們須要作出一些改變,兩個徹底一致的對象應當被認爲是相等的。

var a  = {};
//undefined
var b = {};
//undefined
a == b
//false
a === b
//false

  

這種狀況在全部JavaScript內置對象中也適用,好比咱們應當認爲兩個同樣的RegExp對象是相等的。

2.6 基本數據類型與包裝數據類型之間的對比

在JavaScript中,數值2和Number對象2是不嚴格相等的:

2 == new Number(2);
//true
2 === new Number(2);
//false

  

可是咱們在對比2和new Number(2)時應當認爲二者相等。

3 underscore的實現方法

咱們實現的方法固然仍是依賴於JavaScript相等運算符的,只不過針對特例須要有特定的處理。咱們在比較以前,首先應該作的就是處理特殊狀況。

underscore的代碼中,沒有直接將邏輯寫在_.isEqual方法中,而是定義了兩個私有方法:eq和deepEq。在GitHub用戶@hanzichi的repo中,咱們能夠看到1.8.3版本的underscore中並無deepEq方法,爲何後來添加了呢?這是由於underscore的做者把一些特例的處理提取了出來,放到了eq方法中,而更加複雜的對象之間的對比被放到了deepEq中(同時使得deepEq方法更加便於遞歸調用)。這樣的作法使得代碼邏輯更加鮮明,方法的功能也更加單一明確,維護代碼更加簡潔快速。

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的例子都表明它們相等。
	//應當判斷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不嚴格相等,就進入後續檢測。
	//a == b成立可是a === b不成立的例子中須要排除null和undefined,其他例子須要後續判斷。
	// `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對象,那麼通過以上監測以後還不嚴格相等的話就能夠直接判定a不等於b。
	var type = typeof a;
	if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
	//若是a,b是JavaScript對象,還須要作後續深刻的判斷。
	return deepEq(a, b, aStack, bStack);
};

  

對於源碼的解讀我已經做爲註釋寫在了源碼中。 那麼根據源碼,能夠將其邏輯抽象出來:

jsequal

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.
	//先根據a,b的Class字符串進行比較,若是兩個對象的Class字符串都不同,
	//那麼直接能夠認爲二者不相等。
	var className = toString.call(a);
	if (className !== toString.call(b)) return false;
	//若是二者的Class字符串相等,再進一步進行比較。
	//優先檢測內置對象之間的比較,非內置對象再日後檢測。
	switch (className) {
		// Strings, numbers, regular expressions, dates, and booleans are compared by value.
		//若是a,b爲正則表達式,那麼轉化爲字符串判斷是否相等便可。
		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是字符串對象,那麼轉化爲字符串進行比較。由於一下兩個變量:
			//var x = new String('12');
			//var y = new String('12');
			//x === y是false,x === y也是false,可是咱們應該認爲x與y是相等的。
			//因此咱們須要將其轉化爲字符串進行比較。
			return '' + a === '' + b;
		case '[object Number]':
			//數字對象轉化爲數字進行比較,而且要考慮new Number(NaN) === new Number(NaN)應該要成立的狀況。
			// `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類型均可以轉換爲number類型進行比較。
		//在變量前加一個加號「+」,能夠強制轉換爲數值型。
		//在Date型變量前加一個加號「+」能夠將Date轉化爲毫秒形式;Boolean類型同上(轉換爲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]';
	//若是不是數組對象。
	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.
		//比較兩個非數組對象的構造函數。
		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.
	//初次調用eq函數時,aStack以及bStack均未被傳遞,在循環遞歸的時候,會被傳遞進來。
	//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;
	}

	// Add the first object to the stack of traversed objects.
	//初次調用eq函數時,就把兩個參數放入到參數堆棧中去,保存起來方便遞歸調用時使用。
	aStack.push(a);
	bStack.push(b);

	// Recursively compare objects and arrays.
	//若是是數組對象。
	if (areArrays) {
		// Compare array lengths to determine if a deep comparison is necessary.
		length = a.length;
		//長度不等,直接返回false認定爲數組不相等。
		if (length !== b.length) return false;
		// Deep compare the contents, ignoring non-numeric properties.
		while (length--) {
			//遞歸調用。
			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.
		//對比屬性數量,若是數量不等,直接返回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.
	//循環遞歸結束,把a,b堆棧中的元素推出。
	aStack.pop();
	bStack.pop();
	return true;
};

  

對於源碼的解讀我已經做爲註釋寫在了源碼中。 那麼根據源碼,能夠將其邏輯抽象出來:

  • 1 使用Object.prototype.toString方法獲取兩參數類型,若是兩參數的原始數據類型都不一樣,那麼能夠認爲兩個參數不相等。

  • 2 若是進入了第二步,那麼說明兩個參數的原始類型相同。針對獲取到的字符串進行分類,若是是除Object和Array以外的類型,進行處理。

    • RegExp以及String對象轉化爲字符串進行比較。
    • Number類型的話,須要先使用+運算符強制轉化爲基本數據類型中的數值型,而後處理特例。好比NaN === NaN,0 !== -0.
    • Date以及Boolean對象轉化爲數字類型進行對比。(+運算符強制轉換,Date轉化爲13位的毫秒形式,Boolean轉化爲0或1)
    • Symbol類型使用Symbol.prototype.valueOf獲取字符串,而後進行對比(即認爲傳遞給Symbol函數相同字符串所獲取到的Symbol對象應該相等)。
  • 3 通過以上比較,所剩類型基本只剩Array和基本對象了。若是不是數組對象,那麼構造函數不一樣的對象能夠被認爲是不相等的對象。

  • 4 初始化對象棧aStack以及bStack,由於初次調用deepEq函數時不會傳遞這兩個參數,因此須要手動初始化。由於以後比較的數組對象以及基本對象須要用到對象棧,因此如今應該把當前的a,b推入到兩個棧中。

  • 5 針對數組,先比較長度,長度不等則數組不等。長度相等再遞歸調用deepGet比較數組的每一項,有一項不等則返回false。

  • 6 基本對象類型比較,先使用_.keys獲取對象的全部鍵。鍵數量不一樣的兩對象不一樣,若是鍵數目相等,再遞歸調用deepEq比較每個鍵的屬性,有一個鍵值不等則返回false。

  • 7 通過全部檢測若是都沒有返回false的話,能夠認爲兩參數相等,返回true。在返回以前會把棧中的數據推出一個。

4 underscore的精髓

4.1 將RegExp對象和String對象用相同方法處理

有同窗可能會疑惑:/[a-z]/gi/[a-z]ig/在乎義上是同樣的,可是轉化爲字符串以後比較會不會是不相等的?

這是一個很是好的問題,同時也是underscore處理的巧妙之所在。在JavaScript中,RegExp對象重寫了toString方法,因此在強制將RegExp對象轉化爲字符串時,flags會按規定順序排列,因此將以前兩個RegExp對象轉化爲字符串,都會獲得/[a-z]/gi。這就是underscore能夠放心大膽的將RegExp對象轉化爲字符串處理的緣由。

4.2 Date對象和Boolean對象使用相同方法處理

underscore選擇將Date對象和Boolean對象都轉化爲數值進行處理,這避免了紛繁複雜的類型轉換,簡單粗暴。並且做者沒有使用強制轉換方法進行轉換,而是隻使用了一個「+」符號,就強制將Date對象和Boolean對象轉換成了數值型數據。

4.3 使用對象棧保存當前比較對象的上下文

不少童鞋在閱讀源碼時,可能會很疑惑aStack以及bStack的做用在哪裏。aStack和bStack用於保存當前比較對象的上下文,這使得咱們在比較某個對象的子屬性時,還能夠獲取到其自身。這樣作的好處就在於咱們能夠比較循環引用的對象。

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相等,由於這個被遞歸的方法返回以後,還要繼續比較它們對應的那個父對象,父對象相等,則引用的對象屬性必相等,這樣的處理方法節省了不少的時間,也提升了underscore的性能。

4.4 優先級分明,有的放矢

underscore的處理具備很強的優先級,好比在比較數組對象時,先比較數組的長度,數組長度不相同則數組一定不相等;好比在比較基本對象時,優先比較對象鍵的數目,鍵數目不等則對象一定不等;好比在比較兩個對象參數以前,優先對比Object.prototype.toString返回的字符串,若是基本類型不一樣,那麼兩個對象一定不相等。

這樣的主次分明的對比,大大提升了underscore的工做效率。因此說每個小小的細節,均可以體現出做者的處心積慮。閱讀源碼,可以使咱們學習到太多的東西。

5 underscore的缺陷之處

咱們能夠在其餘方法中看到underscore對ES6中新特徵的支持,好比_.is[Type]方法已經支持檢測Map(_.isMap)和Set(_.isSet)等類型了。可是_.isEqual卻沒有對Set和Map結構的支持。若是咱們使用_.isEqual比較兩個Map或者兩個Set,老是會獲得true的結果,由於它們能夠經過全部的檢測。

在underscore的官方GitHub repo上,我看到有同窗已經提交了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 比較兩參數的長度(或者說是鍵值對數),長度不一者即爲不等,返回false。
  • 2 若是長度相等,就逐一遞歸比較它們的每一項,有任意一項不等者就返回false。
  • 3 所有經過則能夠認爲是相等的,返回true。

這段代碼有一個很巧妙的地方在於它沒有區分究竟是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能夠認爲是一個數據集,這區別於數組對象。咱們能夠說[1,2,3]不等於[2,1,3],由於其相同元素的位置不一樣;可是我認爲new Set([1,2,3])應該認爲等於new Set([2,1,3]),由於Set是無序的,它內部的元素具備單一性。

 

獲取更多underscore源碼解讀:GitHub

相關文章
相關標籤/搜索