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

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

我記得我第一次看到這個題目的時候,感受很吃驚,也很好奇;wow,還能夠這樣嗎?這激起了我很大的興趣去了解這個問題。我就火燒眉毛的想着怎麼解決這個問題。後來使用了隱式轉換這個比較經常使用的方法算是達到了題目的要求。固然,解題的方法還有不少,讓咱們一塊兒來探索一下吧。html

解題的基本思路

反作用 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的過程當中其實進行的步驟是這樣的。java

  • 計算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對象隱式類型轉換的過程git

定義一個全局的屬性

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。這個方案的好處是,咱們可使用===而不是==,由於不須要進行類型轉換,直接返回的就是相應的數字值。github

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

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默認的configurablefalse。因此咱們須要在前兩次將其設置爲true以便咱們接下來可以對其進行修改。這個方法不只可讓咱們使用===,並且咱們還能夠改變比較的順序。好比a === 1 && a === 3 && a === 2,只須要把上面代碼的對應位置的值修改成相應的值就能夠了。這個方法在目前來講是比較好的一種方案。web

其它相似的方案

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在比較的過程當中涉及對象的隱式類型轉換,會調用atoString方法,而toString方法會在內部調用它本身的join方法,因此也可以讓上面的表達式的值爲true算法

上面的這些方法咱們能夠把它們都歸類爲反作用,由於它們大都利用了相等比較的反作用或者讀取屬性的反作用。咱們在平時的開發中要儘可能避免這樣的操做。數組

硬核方法,競態條件

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

爲何說這個方法比較硬核呢,是由於它是在底層的內存上修改一個變量的值,而不是經過一些所謂的技巧去讓上面的表達式成立。並且這在現實的開發中是可能會出現的一種狀況。在進入下面的講解以前,咱們須要先了解一些前置的知識點。

  • SharedArrayBuffer

SharedArrayBuffer對象用來表示一個通用的,固定長度的原始二進制數據緩衝區,相似於 ArrayBuffer對象,它們均可以用來在共享內存上建立視圖。與ArrayBuffer不一樣的是SharedArrayBuffer不能被分離。詳情能夠參考SharedArrayBuffer

  • Web Worker

Web WorkerWeb內容在後臺線程中運行腳本提供了一種簡單的方法。線程能夠執行任務而不干擾用戶界面。此外,他們可使用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次纔出現一次相等。不一樣的電腦運行這個程序所須要的時間是不同的,就算同一臺機器每次運行的結果也是不同的。在個人電腦上運行的結果以下圖所示:
1.png

下面咱們來深刻的講解一下上面的代碼,首先咱們在index.js中建立了三個worker,其中一個worker用來進行獲取a的值,而且一直循環進行比較。直到a === 1 && a === 2 && a === 3成立,才退出循環。另外兩個worker用來製造Race Condition,這兩個worker一直在對同一個地址的數據進行修改。

index.js中,咱們使用SharedArrayBuffer申請了一個字節大小的一段連續的共享內存。而後咱們經過workerpostMessage方法將這個內存的地址傳遞給了3個worker

在這裏咱們須要注意,通常狀況下,經過WorkerpostMessage傳遞的數據要麼是能夠由結構化克隆算法處理的值(這種狀況下是值的複製),要麼是Transferable類型的對象(這種狀況下,一個對象的全部權被轉移,在發送它的上下文中將變爲不可用,而且只有在它被髮送到的worker中可用)。更多詳細內容能夠參考Worker.postMessage() 。可是若是咱們傳遞的對象是SharedArrayBuffer類型的對象,那麼這個對象的表明的是一段共享的內存,是能夠在主線程和接收這個對象的Worker中共享的。

competitor.js中,咱們獲取到了傳遞過來的SharedArrayBuffer對象,由於咱們不能夠直接操做這段內存,須要在這段內存上建立一個視圖,而後纔可以對這段內存作處理。咱們使用Uint8Array建立了一個數組,而後設置了一個定時器一直對數組中的第一個元素進行賦值操做,賦值是隨機的,能夠是1,2,3中的任何一個值。由於咱們有兩個worker同時在作這個操做,因此就造成了Race Condition

worker.js中,咱們一樣在傳遞過來的SharedArrayBuffer對象上建立了一個Uint8Array的視圖。而後在全局定義了一個屬性aa的值是讀取Uint8Array數組的第一個元素值。
而後是一個while循環,一直在對錶達式a === 1 && a === 2 && a === 3進行求值,直到這個表達式的值爲true,就退出循環。

這種方法涉及到的知識點比較多,你們能夠在看後本身在實踐一下,加深本身的理解。由於咱們在實際的開發有可能會遇到這種狀況,可是這種狀況對於咱們的應用程序來講並非一個好事情,因此咱們須要避免這種狀況的發生。那麼如何避免這種狀況的發生呢?咱們可使用Atomics對象來進行相應的操做。Atomics對象提供了一組靜態方法用來對 SharedArrayBuffer對象進行原子操做。若是你頗有興趣的話,能夠點擊Atomics繼續深刻的探究,在這篇文章中就再也不過多的講解了。

解題的其它思路

字符編碼
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,第一個aASCII中的a,第二個a是在後面添加了一個零寬的字符,第三個a是在後面添加了兩個零寬的字符。因此其實它們是不同的變量,那麼表達式a === 1 && a‍ === 2 && a‍‍ === 3true就沒有什麼疑問了。

這個方法實際上是利用了零寬字符,建立了三個咱們肉眼看着同樣的變量。可是它們在程序中屬於三個變量。若是你把上面的代碼複製到Chrome的控制檯中,控制檯就會給出很顯眼的提示,提示的圖片以下所示。
DE9655F2-B549-4026-A16B-1B15E71D6BD6.png

若是你把上面的代碼複製到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,這篇文章涵蓋了大部分的解決方法。每個方法的背後都表明了一些知識點,咱們的目的不是記住這些方法,而是須要了解這些方法背後的知識和原理。這樣之後咱們遇到了相似的問題才知道如何去解決,纔可以作到觸類旁通。

這篇文章到這裏就結束了,若是你們對這篇文章有什麼建議和意見均可以在這裏反饋給我,文章若有更新,會第一時間更新在個人博客dreamapplehappy/blog,關注我學習更多實用有趣的前端知識喲~

參考連接:

相關文章
相關標籤/搜索