看到這個標題,一部分同窗的第一反應多是,又是這個老套的問題,人家都講過好多遍了你還講。同窗,你想錯啦。我可不是在炒冷飯。今天咱們要從這個問題,延伸出更多的知識,保證超出你的預期。讓咱們開始吧。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
默認的configurable
是false
。因此咱們須要在前兩次將其設置爲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
在比較的過程當中涉及對象的隱式類型轉換,會調用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 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 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 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
。
在這裏咱們須要注意,通常狀況下,經過Worker的postMessage傳遞的數據要麼是能夠由結構化克隆算法處理的值(這種狀況下是值的複製),要麼是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繼續深刻的探究,在這篇文章中就再也不過多的講解了。
字符編碼
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,這篇文章涵蓋了大部分的解決方法。每個方法的背後都表明了一些知識點,咱們的目的不是記住這些方法,而是須要了解這些方法背後的知識和原理。這樣之後咱們遇到了相似的問題才知道如何去解決,纔可以作到觸類旁通。
這篇文章到這裏就結束了,若是你們對這篇文章有什麼建議和意見均可以在這裏反饋給我,文章若有更新,會第一時間更新在個人博客dreamapplehappy/blog,關注我學習更多實用有趣的前端知識喲~
參考連接: