當我第一次看到這一題目的時候,我是比較震驚的,分析了下很不合我們程式設計的常理,並認為不大可能,變數a
要在同一情況下要同時等於1,2和3這三個值,這是天方夜譚吧,不亞於哥德巴赫1+1=1
的猜想吧,不過一切皆有可能,出於好奇心,想了許久之後我還是決定嘗試解決的辦法。
我的思路來源於更早前遇到的另外一題相似的面試題:
// 設定一個函式輸出一下的值
f(1) = 1;
f(1)(2) = 3;
f(1)(2)(3) = 6;
當時的解決辦法是使用toString
或者valueOf
實現的,那我們先回顧下toString
和valueOf
方法,方便我們更深入去了解這型別的問題:
比如我們有一個物件,在不重寫toString()
方法和valueOf()
方法的情況下,在 Node 或者瀏覽器輸出的結果是這樣的
class Person {
constructor() {
this.name = name;
}
}
const best = new Person("Kobe");
console.log(best); // log: Person {name: "Kobe"}
console.log(best.toString()); // log: [object Object]
console.log(best.valueOf()); // log: Person {name: "Kobe"}
console.log(best + "GiGi"); // log: [object Object]GiGi
best | Person |
best.toString() | [object Object] |
best.valueOf() | Person |
best + 'GiGi' | [object Object]GiGi |
從上面的輸出我們可以觀察到一個細節,toString()
輸出的是[object Object]
,而valueOf()
輸出的是Person
物件本身,而當運算到best + 'GiGi'
的時候竟然是輸出了[object Object]GiGi
,我們可以初步推斷是物件呼叫的toString()
方法得到的字串進行計算的,難道是運算子+
的鬼斧神工嗎?
為了驗證我們上一步的推斷,我們稍微做一點改變,把 valueOf
方法進行一次複寫:
class Person {
constructor(name) {
this.name = name;
}
// 複寫 valueOf 方法
valueOf() {
return this.name;
}
}
best | Person |
best.toString() | [object Object] |
best.valueOf() | Person |
best + 'GiGi' | KobeGiGi |
這次跟上面只有一處產生了不一樣的結果,那就是最後的best + 'GiGi'
前後兩次結果在複寫了valueOf()
方法之後發生了改變,從中我們可以看出來,物件的本質其實沒有發生根本的改變,但是當它被用作直接運算的時候,它的值是從複寫的valueOf()
中獲取的,並繼續參與後續的運算。
當然不要忘了我們還有個toString()
方法,所以我們也複寫它,看看結果會不會也受影響:
class Person {
constructor(name) {
this.name = name;
}
valueOf() {
return this.name;
}
toString() {
return `Bye ${this.name}`;
}
}
best | Person |
best.toString() | Bye Kobe |
best.valueOf() | Kobe |
best + 'GiGi' | KobeGiGi |
我們發現 best + 'GiGi'
還是沒有發生任何改變,還是使用我們上一次複寫valueOf()
的結果
其實我們重寫了valueOf
方法,不是一定呼叫valueOf()
的返回值進行計算的。而是valueOf
返回的值是基本資料型別時才會按照此值進行計算,如果不是基本資料型別,則將使用toString()
方法返回的值進行計算。
class Person {
constructor(name) {
this.name = name;
}
valueOf() {
return this.name;
}
toString() {
return `Bye ${this.name}`;
}
}
const best = new Person({ name: "Kobe" });
console.log(best); // log: Person name: {name: "Kobe"}
console.log(best.toString()); // log: Bye [object Object]
console.log(best.valueOf()); // log: Person {name: "Kobe"}
console.log(best + "GiGi"); // log: [object Object]GiGi
best | Person |
best.toString() | Bye [object Object] |
best.valueOf() | {name: "Kobe"} |
best + 'GiGi' | Bye [object Object]GiGi |
看上面的例子,現在傳入的name
是一個物件new Person({ name: "Kobe" })
,並不是基本資料型別,所以當執行加法運算的時候取toString()
方法返回的值進行計算,當然如果沒有valueOf()
方法,就會去執行toString()
方法。
所以鋪墊了這麼久,我們就要揭開答案,我們正是使用上面這些原理去解答這一題:
class A {
constructor(value) {
this.value = value;
}
toString() {
return this.value++;
}
}
const a = new A(1);
if (a == 1 && a == 2 && a == 3) {
console.log("Hi Eno!");
}
這裡就比較簡單,直接改寫toString()
方法,由於沒有valueOf()
,當他做運算判斷a == 1
的時候會執行toString()
的結果。
class A {
constructor(value) {
this.value = value;
}
valueOf() {
return this.value++;
}
}
const a = new A(1);
if (a == 1 && a == 2 && a == 3) {
console.log("Hi Eno!");
}
當然,你也可以不使用toString
,換成valueOf
也行,效果也是一樣的:
class A {
constructor(value) {
this.value = value;
}
valueOf() {
return this.value++;
}
}
const a = new A(1);
console.log(a);
if (a == 1 && a == 2 && a == 3) {
console.log("Hi Eno!");
}
所以,當一個物件在做運算的時候(比如加減乘除,判斷相等)時候,往往會有valueOf()
或者toString
的呼叫問題,這個物件的變數背後通常隱藏著一個函式。
當然下面這題原理其實也是一樣的,附上解法:
// 設定一個函式輸出一下的值
f(1) = 1;
f(1)(2) = 3;
f(1)(2)(3) = 6;
function f() {
let args = [...arguments];
let add = function() {
args.push(...arguments);
return add;
};
add.toString = function() {
return args.reduce((a, b) => {
return a + b;
});
};
return add;
}
console.log(f(1)(2)(3)); // 6
當然還沒有結束,這裡還會有一些特別的解法,其實在使用物件的時候,如果物件是一個陣列的話,那麼上面的邏輯還是會成立,但此時的toString()
會變成隱式呼叫join()
方法,換句話說,物件中如果是陣列,當你不重寫其它的toString()
方法,其預設實現就是呼叫陣列的join()
方法返回值作為toString()
的返回值,所以這題又多了一個新的解法,就是在不復寫toString()
的前提下,複寫join()
方法,把它變成shift()
方法,它能讓陣列的第一個元素從其中刪除,並返回第一個元素的值。
class A extends Array {
join = this.shift;
}
const a = new A(1, 2, 3);
if (a == 1 && a == 2 && a == 3) {
console.log("Hi Eno!");
}
我們的探尋之路還沒結束,細心的同學會發現我們題目是如何讓(a===1&&a===2&&a===3)的值為 true
,但是上面都是討論寬鬆相等==
的情況,在嚴格相等===
的情況下,上面的結果會不同嗎?
答案是不一樣的,你們可以試試把剛才上面的寬鬆條件改成嚴格除錯再試一次就知道結果了。
class A extends Array {
join = this.shift;
}
const a = new A(1, 2, 3);
// == 改成 === 後:
if (a === 1 && a === 2 && a === 3) {
console.log("Hi Eno!"); // Hi Eno!此時再也沒出現過了
}
那麼此時的情況又要怎麼去解決呢?我們可以考慮一下使用Object.defineProperty
來解決,這個因為Vue
而被眾人熟知的方法,也是現在面試中一個老生常談的知識點了,我們可以使用它來劫持a
變數,當我們獲取它的值得時候讓它自增,那麼問題就可以迎刃而解了:
var value = 1;
Object.defineProperty(window, "a", {
get() {
return this.value++;
}
});
if (a === 1 && a === 2 && a === 3) {
console.log("Hi Eno!");
}
上面我們就是劫持全域性window
上面的a
,當a
每一次做判斷的時候都會觸發get
屬性獲取值,並且每一次獲取值都會觸發一次函式實行一次自增,判斷三次就自增三次,所以最後會讓公式成立。
當然這裡還有其他方法,這裡再舉例一個,比如使用隱藏字元去做障眼法瞞過面試官的:
var aᅠ = 1;
var a = 2;
var ᅠa = 3;
if (aᅠ == 1 && a == 2 && ᅠa == 3) {
console.log("Hi Eno!");
}
上面這種解法的迷惑性很強,如果不細心會以為是三個一樣的a
,其實本質上是定義三個不一樣的a
值,a
的前後都有隱藏的字元,所以除錯的時候,請複製貼上上面的程式碼除錯,自己在Chrome
手打的話可以用特殊手段讓 a
後面放一個或者兩個紅點實現,並在回車的時候,除錯工具會把這些痕跡給隱藏,從而瞞天過海,秀到一時半刻還沒反應過來的面試官。
最後,祝願大家在新的一年找到一份如意的工作,上面的程式碼在實際情況中基本是不會被運用到的,但是用來探索JS
的無限可能是具有啟發性的,也建議面試官不要使用這類面試題去難為面試者~
如果文章和筆記能帶您一絲幫助或者啟發,請不要吝嗇你的贊和收藏,你的肯定是我前進的最大動力?
附筆記連結,閱讀往期更多優質文章可移步檢視,喜歡的可以給我點贊鼓勵哦: