看到這個標(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)的configurable
是false
儡蔓。所以我們需要在前兩次將其設(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)用a
的toString
方法啡捶,而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 Worker
為Web
內(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é)果如下圖所示:
下面我們來深入的講解一下上面的代碼领猾,首先我們在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)存。然后我們通過worker
的postMessage
方法將這個內(nèi)存的地址傳遞給了3個worker
狐树。
在這里我們需要注意,一般情況下抑钟,通過Worker的postMessage傳遞的數(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
,第一個a
是ASCII
中的a
呀忧,第二個a
是在后面添加了一個零寬的字符师痕,第三個a
是在后面添加了兩個零寬的字符。所以其實它們是不一樣的變量而账,那么表達式a === 1 && a? === 2 && a?? === 3
為true
就沒有什么疑問了胰坟。
這個方法其實是利用了零寬字符,創(chuàng)建了三個我們?nèi)庋劭粗粯拥淖兞扛Q铩5撬鼈冊诔绦蛑袑儆谌齻€變量腕铸。如果你把上面的代碼復(fù)制到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í)更多實用有趣的前端知識喲~
參考鏈接: