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

看到這個標(biāo)題夺刑,一部分同學(xué)的第一反應(yīng)可能是缅疟,又是這個老套的問題,人家都講過好多遍了你還講遍愿。同學(xué)存淫,你想錯啦。我可不是在炒冷飯沼填。今天我們要從這個問題桅咆,延伸出更多的知識,保證超出你的預(yù)期坞笙。讓我們開始吧岩饼。

我記得我第一次看到這個題目的時候,感覺很吃驚薛夜,也很好奇籍茧;wow,還可以這樣嗎梯澜?這激起了我很大的興趣去了解這個問題寞冯。我就迫不及待的想著怎么解決這個問題。后來使用了隱式轉(zhuǎn)換這個比較常用的方法算是達到了題目的要求晚伙。當(dāng)然吮龄,解題的方法還有很多,讓我們一起來探索一下吧咆疗。

解題的基本思路

副作用 side effect

當(dāng)我們看到a == 1 && a == 2 && a == 3的時候漓帚,我們首先要明白以下幾點

  • 這個表達式中含有&&,當(dāng)&&左邊的表達式的值為false的時候民傻,那么&&右邊的表達式就不再計算了胰默。
  • a == 1在這個比較的過程,首先需要獲取a的值漓踢,這涉及到對a的讀取牵署。如果a的類型不是一個數(shù)字類型的值,這又會涉及到數(shù)據(jù)的類型轉(zhuǎn)換相關(guān)的知識喧半。
  • 這個表達式是從左到右進行運算的奴迅,所以我們可以在a == 1計算之后對a的值進行更新,使a == 2能夠繼續(xù)成立

使用一個對象挺据,進行隱式類型轉(zhuǎn)換

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

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

上面這種解決方案應(yīng)該是最容易想到的方案了取具,我們通過一個立即執(zhí)行的函數(shù),返回一個對象扁耐。這個對象的valueOf方法的返回值是i++暇检,也就是說在返回i值之前,會將i的值增加1婉称,然后返回之前i的值块仆。我們在計算a == 1 && a == 2 && a == 3的過程中其實進行的步驟是這樣的。

  • 計算a == 1的值王暗,在比較的過程中對象a會轉(zhuǎn)換為數(shù)字1悔据,然后和==右邊的數(shù)值進行比較,結(jié)果為true俗壹。此時i的值為2科汗。
  • 計算a == 2的值,在比較的過程中對象a會轉(zhuǎn)換為數(shù)字2绷雏,然后和==右邊的數(shù)值進行比較头滔,結(jié)果為true。此時i的值為3涎显。
  • 計算a == 3的值拙毫,在比較的過程中對象a會轉(zhuǎn)換為數(shù)字3,然后和==右邊的數(shù)值進行比較棺禾,結(jié)果為true缀蹄。此時i的值為4
  • true && true && true表達式的結(jié)果為true膘婶,所以輸出結(jié)果為true缺前。
    大家如果對對象的隱式類型轉(zhuǎn)換不是很熟悉的話,可以參考我之前寫的一篇文章深入理解JS對象隱式類型轉(zhuǎn)換的過程悬襟。

定義一個全局的屬性

let i = 1;

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

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

我們還可以通過Reflect.defineProperty定義一個全局的屬性a衅码,當(dāng)屬性a被訪問的時候就會調(diào)用上面定義的getter方法,所以和上面對象的隱式類型轉(zhuǎn)換過程是一樣的脊岳。每次比較之后逝段,i的值會增加1垛玻。這個方案的好處是,我們可以使用===而不是==奶躯,因為不需要進行類型轉(zhuǎn)換帚桩,直接返回的就是相應(yīng)的數(shù)字值。

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

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值的時候账嚎,都會設(shè)置它下一次讀取的值。因為屬性的descriptor默認(rèn)的configurablefalse儡蔓。所以我們需要在前兩次將其設(shè)置為true以便我們接下來能夠?qū)ζ溥M行修改郭蕉。這個方法不僅可以讓我們使用===,而且我們還可以改變比較的順序喂江。比如a === 1 && a === 3 && a === 2召锈,只需要把上面代碼的對應(yīng)位置的值修改為相應(yīng)的值就可以了。這個方法在目前來說是比較好的一種方案获询。

其它類似的方案

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

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

上面也使用了對象的隱式類型轉(zhuǎn)換烟勋,只不過valueOf函數(shù)的返回值是通過執(zhí)行正則表達式的exec方法后的返回值。需要注意的是正則表達式/\d/g需要帶有g修飾符筐付,這樣正則表達式可以記住上次匹配的位置卵惦。還有需要注意的是,正則表達式匹配的結(jié)果是一個數(shù)組或者null瓦戚。在上述的情境中沮尿,我們需要獲取匹配結(jié)果數(shù)組的第一個值。當(dāng)然上面的方法也可以更改比較的順序较解。

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

這個方法也比較巧妙畜疾,而且代碼量最少。數(shù)組a在比較的過程中涉及對象的隱式類型轉(zhuǎn)換印衔,會調(diào)用atoString方法啡捶,而toString方法會在內(nèi)部調(diào)用它自己的join方法,所以也能夠讓上面的表達式的值為true奸焙。

上面的這些方法我們可以把它們都?xì)w類為副作用瞎暑,因為它們大都利用了相等比較的副作用或者讀取屬性的副作用。我們在平時的開發(fā)中要盡量避免這樣的操作与帆。

硬核方法了赌,競態(tài)條件

雖然上面說了這么多,但是其實我真正想要正式介紹給大家的卻是另一個方法玄糟,那就是Race Condition勿她,也就是競態(tài)條件

為什么說這個方法比較硬核呢阵翎,是因為它是在底層的內(nèi)存上修改一個變量的值逢并,而不是通過一些所謂的技巧去讓上面的表達式成立之剧。而且這在現(xiàn)實的開發(fā)中是可能會出現(xiàn)的一種情況。在進入下面的講解之前砍聊,我們需要先了解一些前置的知識點背稼。

  • SharedArrayBuffer

SharedArrayBuffer對象用來表示一個通用的,固定長度的原始二進制數(shù)據(jù)緩沖區(qū)辩恼,類似于 ArrayBuffer對象,它們都可以用來在共享內(nèi)存上創(chuàng)建視圖谓形。與ArrayBuffer不同的是SharedArrayBuffer不能被分離灶伊。詳情可以參考SharedArrayBuffer聘萨。

  • Web Worker

Web WorkerWeb內(nèi)容在后臺線程中運行腳本提供了一種簡單的方法米辐。線程可以執(zhí)行任務(wù)而不干擾用戶界面翘贮。此外狸页,他們可以使用XMLHttpRequest執(zhí)行 I/O (盡管responseXML和channel屬性總是為空)芍耘。一旦創(chuàng)建熄阻, 一個worker 可以將消息發(fā)送到創(chuàng)建它的JavaScript代碼, 通過將消息發(fā)布到該代碼指定的事件處理程序(反之亦然)秃殉。詳情可以參考使用 Web Workers钾军。

了解了前置的知識我們直接看接下來的代碼實現(xiàn)吧巧颈。

  • 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;
   });
};

在開始深入上面的代碼之前砸泛,你可以在本地運行一下上面的代碼蛆封,在看到結(jié)果之前可能需要等上一小會勾栗∥Х或者直接在這里打開瀏覽器的控制臺看一下運行的結(jié)果界牡。需要注意的是,因為SharedArrayBuffer現(xiàn)在僅在Chrome瀏覽器中被支持,所以需要我們使用Chrome瀏覽器來運行這個程序克胳。

運行之后你會在控制臺看到類似如下的結(jié)果:

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

我們可以看到漠另,運行了15838097593次才出現(xiàn)一次相等酗钞。不同的電腦運行這個程序所需要的時間是不一樣的砚作,就算同一臺機器每次運行的結(jié)果也是不一樣的葫录。在我的電腦上運行的結(jié)果如下圖所示:

Chrome瀏覽器控制臺的顯示

下面我們來深入的講解一下上面的代碼领猾,首先我們在index.js中創(chuàng)建了三個worker米同,其中一個worker用來進行獲取a的值摔竿,并且一直循環(huán)進行比較面粮。直到a === 1 && a === 2 && a === 3成立继低,才退出循環(huán)熬苍。另外兩個worker用來制造Race Condition,這兩個worker一直在對同一個地址的數(shù)據(jù)進行修改柴底。

index.js中婿脸,我們使用SharedArrayBuffer申請了一個字節(jié)大小的一段連續(xù)的共享內(nèi)存。然后我們通過workerpostMessage方法將這個內(nèi)存的地址傳遞給了3個worker狐树。

在這里我們需要注意,一般情況下抑钟,通過WorkerpostMessage傳遞的數(shù)據(jù)要么是可以由結(jié)構(gòu)化克隆算法處理的值(這種情況下是值的復(fù)制)野哭,要么是Transferable類型的對象(這種情況下傲武,一個對象的所有權(quán)被轉(zhuǎn)移蓉驹,在發(fā)送它的上下文中將變?yōu)椴豢捎茫⑶抑挥性谒话l(fā)送到的worker中可用)疟位。更多詳細(xì)內(nèi)容可以參考Worker.postMessage() 瞻润。但是如果我們傳遞的對象是SharedArrayBuffer類型的對象,那么這個對象的代表的是一段共享的內(nèi)存甜刻,是可以在主線程和接收這個對象的Worker中共享的绍撞。

competitor.js中,我們獲取到了傳遞過來的SharedArrayBuffer對象得院,因為我們不可以直接操作這段內(nèi)存傻铣,需要在這段內(nèi)存上創(chuàng)建一個視圖,然后才能夠?qū)@段內(nèi)存做處理祥绞。我們使用Uint8Array創(chuàng)建了一個數(shù)組非洲,然后設(shè)置了一個定時器一直對數(shù)組中的第一個元素進行賦值操作,賦值是隨機的蜕径,可以是1两踏,2,3中的任何一個值兜喻。因為我們有兩個worker同時在做這個操作梦染,所以就形成了Race Condition

worker.js中朴皆,我們同樣在傳遞過來的SharedArrayBuffer對象上創(chuàng)建了一個Uint8Array的視圖弓坞。然后在全局定義了一個屬性a隧甚,a的值是讀取Uint8Array數(shù)組的第一個元素值。
然后是一個while循環(huán)渡冻,一直在對表達式a === 1 && a === 2 && a === 3進行求值戚扳,直到這個表達式的值為true,就退出循環(huán)族吻。

這種方法涉及到的知識點比較多帽借,大家可以在看后自己在實踐一下,加深自己的理解超歌。因為我們在實際的開發(fā)有可能會遇到這種情況砍艾,但是這種情況對于我們的應(yīng)用程序來說并不是一個好事情,所以我們需要避免這種情況的發(fā)生巍举。那么如何避免這種情況的發(fā)生呢脆荷?我們可以使用Atomics對象來進行相應(yīng)的操作。Atomics對象提供了一組靜態(tài)方法用來對 SharedArrayBuffer對象進行原子操作懊悯。如果你很有興趣的話蜓谋,可以點擊Atomics繼續(xù)深入的探究,在這篇文章中就不再過多的講解了炭分。

解題的其它思路

字符編碼

const a = 1; // 字符a
const a? = 2; // 字符a·
const a?? = 3; // 字符a··
console.log(a === 1 && a? === 2 && a?? === 3); // true

當(dāng)你看到上面代碼的時候桃焕,你的第一反應(yīng)肯定是懷疑我是不是寫錯了。怎么可以重復(fù)使用const聲明同一個變量呢捧毛?我們肯定不能夠使用const聲明同一個變量观堂,所以你看到的a其實是不同的a,第一個aASCII中的a呀忧,第二個a是在后面添加了一個零寬的字符师痕,第三個a是在后面添加了兩個零寬的字符。所以其實它們是不一樣的變量而账,那么表達式a === 1 && a? === 2 && a?? === 3true就沒有什么疑問了胰坟。

這個方法其實是利用了零寬字符,創(chuàng)建了三個我們?nèi)庋劭粗粯拥淖兞扛Q铩5撬鼈冊诔绦蛑袑儆谌齻€變量腕铸。如果你把上面的代碼復(fù)制到Chrome的控制臺中,控制臺就會給出很顯眼的提示铛碑,提示的圖片如下所示狠裹。

Chrome瀏覽器控制臺的顯示

如果你把上面的代碼復(fù)制到WebStrom中,后兩個變量的背景是黃色的汽烦,當(dāng)你鼠標(biāo)懸浮在上面的時候涛菠,WebStrom會給你一些提示,提示你對應(yīng)的變量使用了不同語言的字符。

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

我們有時在開發(fā)中也會遇到這種情況俗冻,肉眼看明明是相等的兩個值礁叔,比較的結(jié)果卻是不相等的,這個時候可以考慮一下是不是出現(xiàn)了上面這種情況迄薄。

關(guān)于讓a == 1 && a == 2 && a == 3為true琅关,這篇文章涵蓋了大部分的解決方法。每一個方法的背后都代表了一些知識點讥蔽,我們的目的不是記住這些方法涣易,而是需要了解這些方法背后的知識和原理。這樣以后我們遇到了類似的問題才知道如何去解決冶伞,才能夠做到舉一反三新症。

這篇文章到這里就結(jié)束了,如果大家對這篇文章有什么建議和意見都可以在這里反饋給我响禽,文章如有更新徒爹,會第一時間更新在我的博客dreamapplehappy/blog,關(guān)注我學(xué)習(xí)更多實用有趣的前端知識喲~

參考鏈接:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末芋类,一起剝皮案震驚了整個濱河市隆嗅,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌梗肝,老刑警劉巖榛瓮,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件铺董,死亡現(xiàn)場離奇詭異巫击,居然都是意外死亡,警方通過查閱死者的電腦和手機精续,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門坝锰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人重付,你說我怎么就攤上這事顷级。” “怎么了确垫?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵弓颈,是天一觀的道長。 經(jīng)常有香客問我删掀,道長翔冀,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任披泪,我火速辦了婚禮纤子,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己控硼,他們只是感情好泽论,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著卡乾,像睡著了一般翼悴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上幔妨,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天抄瓦,我揣著相機與錄音,去河邊找鬼陶冷。 笑死钙姊,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的埂伦。 我是一名探鬼主播煞额,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼沾谜!你這毒婦竟也來了膊毁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤基跑,失蹤者是張志新(化名)和其女友劉穎婚温,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體媳否,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡栅螟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了篱竭。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片力图。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖掺逼,靈堂內(nèi)的尸體忽然破棺而出吃媒,到底是詐尸還是另有隱情,我是刑警寧澤吕喘,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布赘那,位于F島的核電站,受9級特大地震影響氯质,放射性物質(zhì)發(fā)生泄漏募舟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一病梢、第九天 我趴在偏房一處隱蔽的房頂上張望胃珍。 院中可真熱鬧梁肿,春花似錦、人聲如沸觅彰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽填抬。三九已至烛芬,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間飒责,已是汗流浹背赘娄。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留宏蛉,地道東北人遣臼。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像拾并,于是被迫代替她去往敵國和親揍堰。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353