1. 程式人生 > 實用技巧 >(a == 1 && a == 2 && a == 3)為true,你所不知道的那些答案

(a == 1 && a == 2 && a == 3)為true,你所不知道的那些答案

看到這個標題,一部分同學的第一反應可能是,又是這個老套的問題,人家都講過好多遍了你還講。同學,你想錯啦。我可不是在炒冷飯。今天我們要從這個問題,延伸出更多的知識,保證超出你的預期。讓我們開始吧。

我記得我第一次看到這個題目的時候,感覺很吃驚,也很好奇;wow,還可以這樣嗎?這激起了我很大的興趣去了解這個問題。我就迫不及待的想著怎麼解決這個問題。後來使用了隱式轉換這個比較常用的方法算是達到了題目的要求。當然,解題的方法還有很多,讓我們一起來探索一下吧。

解題的基本思路

副作用 side effect

當我們看到a == 1 && a == 2 && a == 3的時候,我們首先要明白以下幾點

這個表示式中含有&&,當&&左邊的表示式的值為false的時候,那麼&&右邊的表示式就不再計算了。

a == 1在這個比較的過程,首先需要獲取a的值,這涉及到對a的讀取。如果a的型別不是一個數字型別的值,這又會涉及到資料的型別轉換相關的知識。

這個表示式是從左到右進行運算的,所以我們可以在a == 1計算之後對a的值進行更新,使a == 2能夠繼續成立

使用一個物件,進行隱式型別轉換

const a = (function() {
    let i = 1;
    return {
        valueOf: function() {
            return i++;
        }
    }
})();

console.log(a == 1 && a == 2 && a == 3); // true

上面這種解決方案應該是最容易想到的方案了,我們通過一個立即執行的函式,返回一個物件。這個物件的valueOf方法的返回值是i++,也就是說在返回i值之前,會將i的值增加1,然後返回之前i的值。我們在計算a == 1 && a == 2 && a == 3的過程中其實進行的步驟是這樣的。

計算a == 1的值,在比較的過程中物件a會轉換為數字1,然後和==右邊的數值進行比較,結果為true。此時i的值為2。

計算a == 2的值,在比較的過程中物件a會轉換為數字2,然後和==右邊的數值進行比較,結果為true。此時i的值為3。

計算a == 3的值,在比較的過程中物件a會轉換為數字3,然後和==右邊的數值進行比較,結果為true。此時i的值為4。

true && true && true表示式的結果為true,所以輸出結果為true。

大家如果對物件的隱式型別轉換不是很熟悉的話,可以參考我之前寫的一篇文章深入理解JS物件隱式型別轉換的過程。

定義一個全域性的屬性

let i = 1;

Reflect.defineProperty(this, 'a', {
    get() {
        return i++;
    }
});

console.log(a === 1 && a === 2 && a === 3);

我們還可以通過Reflect.defineProperty定義一個全域性的屬性a,當屬性a被訪問的時候就會呼叫上面定義的getter方法,所以和上面物件的隱式型別轉換過程是一樣的。每次比較之後,i的值會增加1。這個方案的好處是,我們可以使用===而不是==,因為不需要進行型別轉換,直接返回的就是相應的數字值。

在比較過程中修改獲取屬性的方法

Reflect.defineProperty(this, 'a', {
    configurable: true,
   get() {
      Reflect.defineProperty(this, 'a', {
            configurable: true,
         get() {
            Reflect.defineProperty(this, 'a', {
               get() {
                  return 3;
               },
            });
            return 2;
         },
      });
      return 1;
   },
});

console.log(a === 1 && a === 2 && a === 3);

上面這個方法,在每次獲取屬性a值的時候,都會設定它下一次讀取的值。因為屬性的descriptor預設的configurable是false。所以我們需要在前兩次將其設定為true以便我們接下來能夠對其進行修改。這個方法不僅可以讓我們使用===,而且我們還可以改變比較的順序。比如a === 1 && a === 3 && a === 2,只需要把上面程式碼的對應位置的值修改為相應的值就可以了。這個方法在目前來說是比較好的一種方案。

其它類似的方案

const a = {
   reg: /\d/g,
   valueOf: function() {
      return this.reg.exec(123)[0];
   },
};

console.log(a == 1 && a == 2 && a == 3);

上面也使用了物件的隱式型別轉換,只不過valueOf函式的返回值是通過執行正則表示式的exec方法後的返回值。需要注意的是正則表示式/\d/g需要帶有g修飾符,這樣正則表示式可以記住上次匹配的位置。還有需要注意的是,正則表示式匹配的結果是一個數組或者null。在上述的情境中,我們需要獲取匹配結果陣列的第一個值。當然上面的方法也可以更改比較的順序。

const a = [1, 2, 3];
a.join = a.shift;
console.log(a == 1 && a == 2 && a == 3);

這個方法也比較巧妙,而且程式碼量最少。陣列a在比較的過程中涉及物件的隱式型別轉換,會呼叫a的toString方法,而toString方法會在內部呼叫它自己的join方法,所以也能夠讓上面的表示式的值為true。

上面的這些方法我們可以把它們都歸類為副作用,因為它們大都利用了相等比較的副作用或者讀取屬性的副作用。我們在平時的開發中要儘量避免這樣的操作。

硬核方法,競態條件

雖然上面說了這麼多,但是其實我真正想要正式介紹給大家的卻是另一個方法,那就是Race Condition,也就是競態條件

為什麼說這個方法比較硬核呢,是因為它是在底層的記憶體上修改一個變數的值,而不是通過一些所謂的技巧去讓上面的表示式成立。而且這在現實的開發中是可能會出現的一種情況。在進入下面的講解之前,我們需要先了解一些前置的知識點。

SharedArrayBuffer

SharedArrayBuffer物件用來表示一個通用的,固定長度的原始二進位制資料緩衝區,類似於ArrayBuffer物件,它們都可以用來在共享記憶體上建立檢視。與ArrayBuffer不同的是SharedArrayBuffer不能被分離。詳情可以參考SharedArrayBuffer。

Web Worker

Web Worker為Web內容在後臺執行緒中執行指令碼提供了一種簡單的方法。執行緒可以執行任務而不干擾使用者介面。此外,他們可以使用XMLHttpRequest執行 I/O (儘管responseXML和channel屬性總是為空)。一旦建立, 一個worker 可以將訊息傳送到建立它的JavaScript程式碼, 通過將訊息釋出到該程式碼指定的事件處理程式(反之亦然)。詳情可以參考使用 Web Workers。

瞭解了前置的知識我們直接看接下來的程式碼實現吧。

index.js

// index.js
const worker = new Worker('./worker.js');
const competitors = [
   new Worker('./competitor.js'),
   new Worker('./competitor.js'),
];
const sab = new SharedArrayBuffer(1);
worker.postMessage(sab);
competitors.forEach(w => {
   w.postMessage(sab);
});

worker.js

// worker.js
self.onmessage = ({ data }) => {
   const arr = new Uint8Array(data);
   Reflect.defineProperty(self, 'a', {
      get() {
         return arr[0];
      },
   });
   let count = 0;
   while (!(a === 1 && a === 2 && a === 3)) {
      count++;
      if (count % 1e8 === 0) console.log('running...');
   }
   console.log(`After ${count} times, a === 1 && a === 2 && a === 3 is true!`);
};

competitor.js

// competitor.js
self.onmessage = ({ data }) => {
   const arr = new Uint8Array(data);
   setInterval(() => {
      arr[0] = Math.floor(Math.random() * 3) + 1;
   });
};

在開始深入上面的程式碼之前,你可以在本地執行一下上面的程式碼,在看到結果之前可能需要等上一小會。或者直接在這裡開啟瀏覽器的控制檯看一下執行的結果。需要注意的是,因為SharedArrayBuffer現在僅在Chrome瀏覽器中被支援,所以需要我們使用Chrome瀏覽器來執行這個程式。

執行之後你會在控制檯看到類似如下的結果:

158 running...
After 15838097593 times, a === 1 && a === 2 && a === 3 is true!

我們可以看到,運行了15838097593次才出現一次相等。不同的電腦執行這個程式所需要的時間是不一樣的,就算同一臺機器每次執行的結果也是不一樣的。在我的電腦上執行的結果如下圖所示:

下面我們來深入的講解一下上面的程式碼,首先我們在index.js中建立了三個worker,其中一個worker用來進行獲取a的值,並且一直迴圈進行比較。直到a === 1 && a === 2 && a === 3成立,才退出迴圈。另外兩個worker用來製造Race Condition,這兩個worker一直在對同一個地址的資料進行修改。

在index.js中,我們使用SharedArrayBuffer申請了一個位元組大小的一段連續的共享記憶體。然後我們通過worker的postMessage方法將這個記憶體的地址傳遞給了3個worker。

在這裡我們需要注意,一般情況下,通過WorkerpostMessage傳遞的資料要麼是可以由結構化克隆演算法處理的值(這種情況下是值的複製),要麼是Transferable型別的物件(這種情況下,一個物件的所有權被轉移,在傳送它的上下文中將變為不可用,並且只有在它被髮送到的worker中可用)。更多詳細內容可以參考Worker.postMessage()。但是如果我們傳遞的物件是SharedArrayBuffer型別的物件,那麼這個物件的代表的是一段共享的記憶體,是可以在主執行緒和接收這個物件的Worker中共享的。

在competitor.js中,我們獲取到了傳遞過來的SharedArrayBuffer物件,因為我們不可以直接操作這段記憶體,需要在這段記憶體上建立一個檢視,然後才能夠對這段記憶體做處理。我們使用Uint8Array建立了一個數組,然後設定了一個定時器一直對陣列中的第一個元素進行賦值操作,賦值是隨機的,可以是1,2,3中的任何一個值。因為我們有兩個worker同時在做這個操作,所以就形成了Race Condition。

在worker.js中,我們同樣在傳遞過來的SharedArrayBuffer物件上建立了一個Uint8Array的檢視。然後在全域性定義了一個屬性a,a的值是讀取Uint8Array陣列的第一個元素值。
然後是一個while迴圈,一直在對錶達式a === 1 && a === 2 && a === 3進行求值,直到這個表示式的值為true,就退出迴圈。

這種方法涉及到的知識點比較多,大家可以在看後自己在實踐一下,加深自己的理解。因為我們在實際的開發有可能會遇到這種情況,但是這種情況對於我們的應用程式來說並不是一個好事情,所以我們需要避免這種情況的發生。那麼如何避免這種情況的發生呢?我們可以使用Atomics物件來進行相應的操作。Atomics物件提供了一組靜態方法用來對SharedArrayBuffer物件進行原子操作。如果你很有興趣的話,可以點選Atomics繼續深入的探究,在這篇文章中就不再過多的講解了。

廣州vi設計公司 http://www.maiqicn.com 我的007辦公資源網 https://www.wode007.com

解題的其它思路

字元編碼
const a = 1; // 字元a
const a‍ = 2; // 字元a·
const a‍‍ = 3; // 字元a··
console.log(a === 1 && a‍ === 2 && a‍‍ === 3); // true

當你看到上面程式碼的時候,你的第一反應肯定是懷疑我是不是寫錯了。怎麼可以重複使用const宣告同一個變數呢?我們肯定不能夠使用const宣告同一個變數,所以你看到的a其實是不同的a,第一個a是ASCII中的a,第二個a是在後面添加了一個零寬的字元,第三個a是在後面添加了兩個零寬的字元。所以其實它們是不一樣的變數,那麼表示式a === 1 && a‍ === 2 && a‍‍ === 3為true就沒有什麼疑問了。

這個方法其實是利用了零寬字元,建立了三個我們肉眼看著一樣的變數。但是它們在程式中屬於三個變數。如果你把上面的程式碼複製到Chrome的控制檯中,控制檯就會給出很顯眼的提示,提示的圖片如下所示。

如果你把上面的程式碼複製到WebStrom中,後兩個變數的背景是黃色的,當你滑鼠懸浮在上面的時候,WebStrom會給你一些提示,提示你對應的變數使用了不同語言的字元。

Identifier contains symbols from different languages: [LATIN, INHERITED]
Name contains both ASCII and non-ASCII symbols: a‍
Non-ASCII characters in an identifier

我們有時在開發中也會遇到這種情況,肉眼看明明是相等的兩個值,比較的結果卻是不相等的,這個時候可以考慮一下是不是出現了上面這種情況。

關於讓a == 1 && a == 2 && a == 3為true,這篇文章涵蓋了大部分的解決方法。每一個方法的背後都代表了一些知識點,我們的目的不是記住這些方法,而是需要了解這些方法背後的知識和原理。這樣以後我們遇到了類似的問題才知道如何去解決,才能夠做到舉一反三。