[譯] 在JS中,如何讓(a===1 && a===2 && a === 3)(嚴格相等)的值為true?

騰訊IVWEB團隊發表於2018-11-27

原文: Will (a===1 && a===2 && a===3) (strict comparison) ever be true (in JavaScript)

本文是JS經典問題(a == 1 && a==2 && a==3)(寬鬆相等)的擴充套件和解決方案

如何使用getter/setter描述符讓(a===1 && a===2 && a === 3)的值為true

我們先簡單瞭解這道JS經典問題, 然後再解決它的擴充套件問題。

內容概覽:

重溫(a==1&&a==2&&a==3)(寬鬆相等)問題

如果你已經瞭解過這個問題並且知道如何解決這個JS謎題(是的,只是一個謎題,我並不想在生產程式碼中看到這樣的用例) , 那你可以直接跳到下一節,閱讀它的擴充套件問題。關於這個問題在reddit上有相關討論, 下面是我看到最有趣的評論

"如果我在程式碼庫中看到這樣的程式碼,我可能就很絕望了" // 譯者注: 誰看到都會很絕望吧

(a==1 && a== 2 && a ==3 )問題

(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 == BB == A具有相同的表現(除了轉換的順序不同)。可以在這裡詳細深度地瞭解寬鬆匹配==與嚴格匹配===

Javascript會如何強制轉換這個值呢?

在進行兩個值的比較時,執行了型別的強制轉換, 讓我們先了解下內建的轉換函式。

ToPrimitive(input, PreferredType?)
複製程式碼

可選引數PreferredType可以指定最終轉化的型別,它可以是Number型別或String型別,這依賴於ToPrimitive()方法執行的結果返回的是Number型別或String型別。

值的轉化過程如下

  1. 如果輸入Input是基本型別, 就返回這個值
  2. 如果輸入變數是Object型別, 那麼呼叫input.valueOf(). 如果返回結果是基本型別,就返回這個指
  3. 如果都不是的話就呼叫input.toString(). 如果結果是基本型別, 就返回它
  4. 如果以上都不可以,就會丟擲一個型別錯誤TypeError, 表示轉化input變數到基本型別失敗。

如果PreferredTypeNumber, 那轉換演算法就會像上述說明的順序執行,如果是String,步驟2和步驟3會交換順序。PreferredType是一個預設值,如果不輸入的話,Date型別會被當作String型別處理,其他變數會當作Number處理。預設的valueOf返回this,預設的toString()會返回型別資訊。

如上是操作符+==呼叫toPrimitive()的執行過程。

所以在上面的程式碼中, 如JS引擎所解析的,a == 11是基本型別, JS引擎會嘗試將a轉換成Number型別,然後在上面的演算法中,a.valueOf被呼叫並且返回1(自增1並且返回自己)。在a==2a==3發生了同樣的型別轉換並增加自己的值。

(a === 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描述符, 帶來了解決這個問題的辦法。

什麼是屬性描述符(property descriptors)?

屬性描述符有兩種型別, 資料描述符和存取描述符。

  1. 資料描述符

    強制鍵值 - value

    可選鍵值

     - configurable
     - enumable
     - writeable
    複製程式碼

    例子

    {
        value: 5,
        writable: true
    }
    複製程式碼
  2. 存取描述符

    強制鍵值 - 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的語法與定義。 有趣的是,getset是可以通過"."操作符呼叫的方法, 舉個例子, a有一個具有getterb屬性, 它可以像物件的其他屬性一樣去呼叫,類似於a.b。這可以解決我們最初的問題, 我們需要呼叫一個無需()的函式, 通過get屬性, 我們可以呼叫一個函式並且不用在函式名後新增()

在上面提到的解決方案中, 我們在window物件上定義了一個具有getter的a屬性, 所以a可以在程式碼中直接被訪問到(全域性變數), 因此也可以直接獲得a的值。如果我們在其他物件上定義了屬性a而不是window的話,例如object1, 我們就需要改變題目為object1.a===1 && object1.a===2 && object1.a===3了。

Github Gist

參考


《IVWEB 技術週刊》 震撼上線了,關注公眾號:IVWEB社群,每週定時推送優質文章。

相關文章