據說是面試題:由【if(a==1&&a==2&&a==3)】引發的思考探討

     有一天,突然在一個微信群有個群友發了張圖片拋出了一道題,如圖:

下面,我們先還原原題:

1 //下面程式碼什麼時候會列印1?
2 var a=?;
3 if(a==1&&a==2&&a==3){
4     console.log(1);
5 }

        說實話,我第一眼看到時居然理所當然地認為讓a=true或者a=!0應該就可以了,但是程式碼世界的種種複雜變數讓我不能輕易相信第一感覺,於是馬上打開電腦,在chrome瀏覽器的控制台快速敲下以下程式碼:

【a=true時】

1 var a=true;
2 if(a==1&&a==2&&a==3){
3     console.log("猜想正確:"+a);
4 }else{
5     console.log("猜想錯誤");
6 }

【a=!0時】

1 var a=!0;
2 if(a==1&&a==2&&a==3){
3     console.log("猜想正確:"+a);
4 }else{
5     console.log("猜想錯誤");
6 }

  然後列印結果讓我意識到錯誤的同時也激發了深入探索的興趣:既然猜想錯誤,那到底要怎麼做才能實現呢?背後的原理又會是怎樣的呢?帶著疑問,然後打開了百度上CSDN的一篇部落格文章開始看。

       文章一開頭就說是一道有趣的面試題:Excuse Me?!驚愕之餘就仔細品讀了共有4種主要方法。接下來我結合自己的理解儘力將4種方法的原理給解釋一下,如有錯誤,請多多指正,謝謝各位~

【解法①:利用對象的類型裝換】

 1 var a={
 2     num:1,
 3     toString:function(){
 4         return a.num++;
 5     }
 6 }
 7 
 8 if(a==1&&a==2&&a==3){
 9     console.log("猜想正確!");
10 }else{
11     console.log("猜想錯誤!");
12 }

       目的達到了,其中的原理是什麼呢?我們先看a==1&&a==2&&a==3,這是一個短路邏輯與運算符,這就表明只有左端條件為真能會繼續往右端進行判斷,否則立即整個判斷像短路一樣為假了,所以呢,a的第一個值必須是a==1為真之後才會進行第二步的a==2判斷,由此推斷a的值或者說是間接返回值(類型轉換後的值)應該是可以自增長的!另外,這種a==1的判斷,JavaScript中當遇到不同類型的值進行比較時,會根據類型轉換規則試圖將它們轉為同一個類型再比較。比如 Object 類型與 Number 類型進行比較時,Object 類型會轉換為 Number 類型。轉換為時會嘗試調用 Object.valueOf 和 Object.toString 來獲取對應的數字基本類型。

       在上述的程式碼中,邏輯轉換先調用了valueOf方法,如果返回的還是對象,再接著調用toString()方法。每次比較都會先執行重寫後的對象方法toString(),這個方法里先返回屬性num的值再自增(區分:return a.num++表示先返回再自增,return ++a.num表示先自增再把結果返回)。知道了對象a的內部之後就能明白,執行a==1判斷時,對象a調用toString()方法返回了屬性num的值1,此時比較兩個當然是相等的。與此類似,a==2和a==3一樣成立。看到這裡是否有豁然開朗的感覺捏?

【解法②:利用數組的取值和類型轉換】

        JavaScript里的數組真的是靈魂支柱,因為絕大多數的數據都在數組裡操作,因此很多時候解決問題的巧妙思路也能從它著手。下面先上程式碼和運行結果:

1 var a=[1,2,3,4];
2 a.join=a.shift;
3 if(a==1&&a==2&&a==3){
4     console.log("猜想正確!");
5 }else{
6     console.log("猜想錯誤!");
7 }

        眨眼一看這個寫法莫名其妙讓人匪夷所思,當好好地理解了之後就霎時拍案叫絕,程式碼之簡潔優雅,思路之清奇獨到,堪稱膩害!我們知道在JavaScript中一切皆對象,那麼Array當然也是對象的子類了,同樣繼承了Object對象的方法valueOf()和toString(),而且重寫了toString()方法,在調用數組中的每個元素的 toString() 返回值經調用 join() 方法連接(由逗號隔開)組成。所以在這裡可以不重寫toString()方法了,只需要對join()方法進行處理即可。那麼join()方法作用扮演的是什麼角色呢?沒錯,它用來將數組各項通過連接符拼接起來形成字元串,它不會改變原數組僅僅是取出元素連接起來。shift()方法是會將數組的第一個元素刪除並返回被刪除的元素,換言之就好像是直接將數組的第一個元素移出數組,因此它改變了原數組的結構和長度,但是自身不會創建新的數組。

        讓我們把目光聚焦到a.join=a.shift,這句話的意思是當數組調用toString()方法而間接調用join方法時,shift()方法替代了join方法,這樣就相當於每次從a數組中截取第一個元素返回。所以當判斷a==1時其實是從原數組截取了第一個元素的值返回後再判斷,這樣原數組就變成了[2,3],接著a==2判斷執行類似操作即可。怎麼樣,這個方法巧妙吧?有沒有被驚訝到捏?

【解法③:理由Object對象的defineProperty()方法定義屬性並重寫getter()方法】

同樣道理,Let’s show the code to see see!

 1 var num=1;
 2 Object.defineProperty(window,'a',{
 3     get:function(){
 4         return num++;
 5     }
 6 })
 7 
 8 if(a==1&&a==2&&a==3){
 9     console.log("猜想正確!");
10 }else{
11     console.log("猜想錯誤!");
12 }

        可能有的人看到defineProperty()並不是很了解它的用處,我查了下MDN上的說法:Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回此對象。

       首先,JavaScript的運行環境通常主要分為兩種:客戶端(瀏覽器)和服務端(node),這兩種環境下的全局對象管理所有的變數和函數,客戶端是window,node是global,在本例以window為參考。此時通過defineProperty()給window對象定義了一個a屬性,a屬性的值由get()方法返回後再自增。因此,當判斷a==1時,實際上是獲取省略掉window對象前綴的a的值後再比較。這個defineProperty()方法應當直接在 Object 構造器對象上調用此方法,而不是在任意一個 Object 類型的實例上調用。

【小擴展:defineProperty()方法有兩種給定義的屬性賦值的方法:數據描述符和存取描述符(有set或get方法)】

【解法④:利用Unicode字元編碼,這種方式沒什麼技術含量不必深究也不推薦,了解即可】

1 var aᅠ = 1;
2 var a = 2;
3 var ᅠa = 3;
4 if(aᅠ ==1 && a ==2 && ᅠa ==3){
5     console.log("Let's see see!");
6 }else{
7     console.log("Don't want to see see,ok?!");
8 }

解法⑤:利用ES6的類來實現】

ES6是引入了類的比較規範的寫法,我們可以在類的定義里做想做的事情,下面演示用傳統函數和類分別實現:

 1 //傳統函數寫法
 2 //定義內部變數,重寫valueOf並返回一個可以增長的變數值
 3 function fnA(){
 4     var num=1;
 5     this.valueOf=function(){    //只有對象的valueOf方法被調用時才執行
 6         return num++;
 7     }
 8 }
 9 
10 //ES6的規範類寫法
11 class clazzA{
12     constructor(){
13         this.num=0;  //類被調用創建對象就會執行構造函數,該變數會自增
14         this.valueOf();
15     }
16     valueOf(){
17         return this.num++;
18     }
19 }
20 
21 //let a=new fnA;  //此時valueOf並不會被調用
22 let a=new clazzA;  //構造函數調用了一次valueOf方法
23 if(a==1&&a==2&&a==3){
24     console.log("實現了!");
25 }else{
26     console.log("what's wrong?");
27 }

      從上面的程式碼和列印結果看出,傳統函數和ES6類都藉助了Object自帶的valueOf()方法,只是二者在處理時不一樣:傳統函數被調用時valueOf()並沒有被立即調用,只是通過匿名函數的方式聲明了函數,真正調用valueOf()還是在執行判斷時隱式調用的;而ES6類則選擇了再構造函數里直接調用在類里重寫後的valueOf()方法。因此,兩者在定義變數num的初始值時需要注意一下!

      通過上述的探討大體上就使用了5種解決方法,其中最簡潔優雅巧妙的當屬解法②數組對象方式,解法③方式屬於修改對象屬性,解法①和解法5的核心還是利用對象的內置方法valueOf()或toString()進行重寫值返回,解法④就權當看看了解吧~

     OK,本次探討暫且到此為止,如有錯漏,歡迎指正,謝謝~

版權所有,轉載請註明出處!