[譯] 在JS中,如何讓(a===1 && a===2 && a === 3)(嚴格相等)的值為true?
我們先簡單瞭解這道JS經典問題, 然後再解決它的擴充套件問題。
內容概覽:
-
重溫(a==1&&a==2&&a==3)(寬鬆相等)問題
- (a==1&&a==2&&a==3)問題描述
- (a===1&&a===2&&a===3)擴充套件問題
如果你已經瞭解過這個問題並且知道如何解決這個JS謎題(是的,只是一個謎題,我並不想在生產程式碼中看到這樣的用例) , 那你可以直接跳到下一節,閱讀它的擴充套件問題。關於這個問題在reddit上有相關討論, 下面是我看到最有趣的評論
"如果我在程式碼庫中看到這樣的程式碼,我可能就很絕望了" // 譯者注: 誰看到都會很絕望吧
(a ==1 && a==2 && a==3) 的值可以是true嗎?
回答是肯定的, 具體可以看下面的程式碼
const a = { value : 0 }; a.valueOf = function() { return this.value += 1; }; console.log(a==1 && a==2 && a==3); //true 複製程式碼
通常, 在面試中問這類問題的目的並不是要求面試者記住這樣的答案,而是想要了解面試者在面對這道題目時,是如何思考的以及他們是否有了解過Javascript中關於==
的奇特的語法特性。
祕密就在於"寬鬆相等操作符==
"
在JS中,寬鬆相等==
會先將左右兩兩邊的值轉化成相同的原始型別,然後再去比較他們是否相等。在轉化之後(==
一邊或兩邊都需要轉化),最後的相等匹配會像===
符號一樣去執行判斷。寬鬆相等是可逆的,對於任何值A與B,通常A == B
與B == A
具有相同的表現(除了轉換的順序不同)。可以在這裡詳細深度地瞭解寬鬆匹配==
與嚴格匹配===
。
Javascript會如何強制轉換這個值呢?
在進行兩個值的比較時,執行了型別的強制轉換, 讓我們先了解下內建的轉換函式。
ToPrimitive(input, PreferredType?) 複製程式碼
可選引數PreferredType
可以指定最終轉化的型別,它可以是Number
型別或String
型別,這依賴於ToPrimitive()
方法執行的結果返回的是Number
型別或String
型別。
值的轉化過程如下
TypeError
如果PreferredType
是Number
, 那轉換演算法就會像上述說明的順序執行,如果是String
,步驟2和步驟3會交換順序。PreferredType
是一個預設值,如果不輸入的話,Date
型別會被當作String
型別處理,其他變數會當作Number
處理。預設的valueOf
返回this
,預設的toString()會返回型別資訊。
如上是操作符+
和==
呼叫toPrimitive()
的執行過程。
所以在上面的程式碼中, 如JS引擎所解析的,a == 1
,1
是基本型別, JS引擎會嘗試將a
轉換成Number
型別,然後在上面的演算法中,a.valueOf
被呼叫並且返回1(自增1並且返回自己)。在a==2
和a==3
發生了同樣的型別轉換並增加自己的值。
(a === 1 && a === 2 && a ===3)的值也能是true嗎?
當然也可以, 具體請看下面的程式碼
var value = 0; //window.value Object.defineProperty(window, 'a', { get: function() { return this.value += 1; } }); console.log(a===1 && a===2 && a===3) // true 複製程式碼
從經典問題的解答中,我們瞭解到JS中的原始型別將不再滿足於上面的條件(嚴格相等沒有轉化的過程),所以我們需要通過一些方式去呼叫一個函式,並在這個函式中做我們想做的事情。但是執行函式往往需要在函式名字後引入()
。並且由於這裡不是寬鬆相等==
,valueOf
將不會被JS引擎呼叫。Emmm, 有點棘手。還好有Property
函式, 特別是getter
描述符, 帶來了解決這個問題的辦法。
屬性描述符有兩種型別, 資料描述符和存取描述符。
-
資料描述符
強制鍵值 -value
可選鍵值
- configurable - enumable - writeable 複製程式碼
例子
{ value: 5, writable: true } 複製程式碼
-
存取描述符
強制鍵值 - get/set或都設定 可選鍵值 - confiturable - enumerable 例子
{ get: function () { return 5; }, enumerable: true } 複製程式碼
MDN上關於存取描述符的例子
// Example of an object property added // with defineProperty with an accessor property descriptor var bValue = 38; Object.defineProperty(o, 'b', { // Using shorthand method names (ES2015 feature). // This is equivalent to: // get: function() { return bValue; }, // set: function(newValue) { bValue = newValue; }, get() { return bValue; }, set(newValue) { bValue = newValue; }, enumerable: true, configurable: true }); o.b; // 38 // 'b' property exists in the o object and its value is 38 // The value of o.b is now always identical to bValue, // unless o.b is redefined 複製程式碼
在問題的解決方案中, 我們使用Object.defineProperty
為物件定義了一個屬性。你可以在這裡深入瞭解Object.defineProperty
的語法與定義。 有趣的是,get
和set
是可以通過"."操作符
呼叫的方法, 舉個例子,a
有一個具有getter
的b
屬性, 它可以像物件的其他屬性一樣去呼叫,類似於a.b
。這可以解決我們最初的問題, 我們需要呼叫一個無需()
的函式, 通過get
屬性, 我們可以呼叫一個函式並且不用在函式名後新增()
在上面提到的解決方案中, 我們在window物件上定義了一個具有getter的a
屬性, 所以a
可以在程式碼中直接被訪問到(全域性變數), 因此也可以直接獲得a的值。如果我們在其他物件上定義了屬性a
而不是window的話,例如object1, 我們就需要改變題目為object1.a===1 && object1.a===2 && object1.a===3
了。