程式設計師發了段程式碼,被人拿著四十米大刀追了一條街!
程式設計師每天敲敲程式碼,不會很枯燥嗎?是的,如果作為一名程式設計師每天就只敲程式碼,的確很乏味。但是誰說程式設計師每天就只會敲程式碼呢?我們有我們的快樂,我們程式設計師騷起來,就沒你們的事了。
比如:
(!( +[])+{})[--[ +""][+[]] [~+[]] + ~ !+[]]+({}+[])[[ !+[]] ~+[]]
如果你覺得某個人令你很不爽,於是,程式設計師就會發一段程式碼,我們來看看結果:

哈哈,是不是被罵了還在問人這段程式碼是什麼意思呢?
我來告訴你:
這段程式碼為什麼會輸出sb呢?其實這段程式碼考的是js的型別轉化的一些基本原理。
首先要運用到的第一個知識就是js運算子的優先順序,因為這麼長一段運算看的人眼花,我們必須得先根據優先順序分成n小段,然後再各個擊破。
1、js運算子的優先順序
優先順序的排列如下表,優先順序從高到低:

根據此規則,我們把這一串運算分為以下16個子表示式:

運算子用紅色標出,有一點可能大家會意識不到,其實中括號[]也是一個運算子,用來通過索引訪問陣列項,另外也可以訪問字串的子字元,有點類似charAt方法,如:'abcd'[1] // 返回'b'。而且中括號的優先順序還是最高的哦。
接下來需要運用的就是java的型別轉化知識了,我們先說說什麼情況下需要進行型別轉化。當操作符兩邊的運算元型別不一致或者不是基本型別(也叫原始型別)時,需要進行型別轉化。
讓我們快速的複習一下,在Java中,一共有兩種型別的值:原始值(primitives)和物件值(objects)。
原始值有:undefined、null、布林值(booleans)、數字(numbers)、還有字串(strings)。
其他的所有值都是物件型別的值,包括陣列(arrays)和函式(functions)。
2、型別轉化
先按運算子來分一下類:
減號-,乘號*,肯定是進行數學運算,所以運算元需轉化為number型別。
加號+,可能是字串拼接,也可能是數學運算,所以可能會轉化為number或string。
一元運算,如+[],只有一個運算元的,轉化為number型別。
下面來看一下轉化規則。
1、對於非原始型別的,通過
ToPrimitive()
將值轉換成原始型別
ToPrimitive(input, PreferredType?)
可選引數PreferredType是Number或者是String。返回值為任何原始值.如果PreferredType是Number,執行順序如下:
如果input為primitive,返回;
否則,input為Object。呼叫
obj.valueOf()
。如果結果是primitive,返回;
否則,呼叫
obj.toString()
,如果結果是primitive,返回;
否則,丟擲TypeError。
如果 PreferredType是String,步驟2跟3互換,如果PreferredType沒有,Date例項被設定成String,其他都是Number。
2、通過ToNumber()將值轉換為數字
通過ToNumber()把值轉換成Number,直接看ECMA 9.3的表格:

如果輸入的值是一個物件,則會首先會呼叫
ToPrimitive(obj,Number)
將該物件轉換為原始值,然後在呼叫ToNumber()將這個原始值轉換為數字。
3、通過ToString()將值轉換為字串
通過ToString()把值轉化成字串, 直接看ECMA 9.8的表格

如果輸入的值是一個物件,則會首先會呼叫
ToPrimitive(obj,String)
將該物件轉換為原始值,然後再呼叫ToString()將這個原始值轉換為字串.
規則就這麼多,接下來實踐一下,根據我們上面劃分出的子表示式,一步一步將這個神奇的程式碼給執行出來。開工~
先看最簡單的子表示式16:
+[]
只有一個運算元[],肯定是轉化為number了,根據上面的規則2,[]是個陣列,object型別,即物件。所以得先呼叫toPrimitive轉化為原始型別,並且PreferredType為number,這個引數表示更“傾向於”轉化的型別,這裡肯定是number了。然後首先呼叫陣列的valueOf方法,陣列呼叫valueOf會返回自身,
這個時候,我們得到一個空串“”,還沒有結束,看上面的規則2描述,繼續呼叫toNumber,轉化為number型別,
大功告成!子表示式16轉化完畢,+[],最終得到0。
來看子表示式15:
[~+""]
空串""前面有兩個一元操作符,但是運算元還是隻有一個,所以,最終要轉化為的型別是number。看規則2吧,空串呼叫toNumber得到0。接下來是 ,這是個什麼東東呢?它是位運算子,作用可以記為把數字取負然後減一,所以 0就是-1 。
別忘了,這個子表示式外頭還包著中括號,所以最終的值為[-1],即一個數組,裡面只有一個元素-1.
接下來看子表示式13就簡單了
把15、16求出來的填進去,就變成了這樣:
--[-1][0]
,取陣列的第0個元素,然後自減,結果為-2,是不so easy!
繼續往上走,子表示式14:
[~+[]]
其實把15、和16的原理用上就非常明顯了,答案[-1]。
繼續來求子表示式9,此刻它已變成:
-2*[-1]
,有稍許不一樣,不過沒關係,我們還是按照規則來,運算子是乘號*,當然是做數學運算,那後面的
[-1]
就得轉化為number,與16的求法類似,過程如下:
呼叫toPrimitive,發現是object型別
呼叫valueOf,返回自身[-1]
因為不是原始型別,繼續呼叫toString,返回"-1"
"-1"是原始型別了,然後呼叫toNumber,返回-1
與-2相乘,返回2
子表示式10:
~~!+[]
不多說了,答案1。就是從右往左依次一元計算。
有了9和10,我們來到了子表示式4,此刻它已經長這樣了:2+1, 好,我不多說了。
繼續看錶達式7:
!( +[]), +[]=-1
這個根據上面已經知道了,那!-1是什麼呢?這裡要說一下這個感嘆號,它是邏輯取非的意思,會把表示式轉化為布林型別,轉化規則和js的Truthy和Falsy原則是一樣的,後面跟數字的,除0以外都為false,後面跟字串的,除空串以外都為false。這裡的
!-1
當然就是false了。

接下來這個表示式3:false+{}有點關鍵
一個布林加一個物件,那這個{}應該先轉化為原始型別,流程如下:
呼叫toPrimitive,發現是object型別
呼叫valueOf,返回自身{},
不是原始型別,呼叫toString,返回
[objectObject]
false與
[objectObject]
相加,false先轉化為字串"false"
相加得結果
false[objectObject]
知道了表示式3和4,我們就可以來看錶達式1了,此時它是這樣的:
false[objectObject]
[3],因為這個[]可以取字串的子字元,像charAt一樣,所以得到了結果"s"
經過上面艱難的流程,我們拿到了字元"s",也就是那張圖的左半邊,剩下的那個"b",相同的原理可以搞出來,我這裡就不一一演示了,留給你練練吧~
回顧一下這個過程其實也不復雜,只是有一些需要重複勞動的,只要你掌握了運算的優先順序,能把大串分解成一個個小串,然後運用型別轉化的知識挨個處理就搞定了。怎麼樣,看到這裡你還覺得神奇嗎?
同樣的,中文字元也是由這樣組成的,跟英文同樣的道理。
瞭解了嗎?如果以後有人給你發這段程式碼,二話不說,平底鍋伺候。哈哈。