JavaScript類型轉換的有趣應用


文章出自個人博客網站:https://knightyun.github.io/2019/10/07/js-magic-expression,轉載請聲明。


背景

可以訪問這個網站提前預覽:https://knightyun.github.io/magic-expression/

先來看一串代碼:

(!(~+[])+{})[--[~+''][+[]]*[~+[]]+~~!+[]]+({}+[])[[~!+[]]*~+[]]

也許你在其他地方看見過這種黑科技操作,那麼不妨猜一下上面的代碼的值等於多少,實在猜不到可以複製它粘貼到瀏覽器 console 中回車看看;

這裏劇透一下,會得到下面的結果(手動解釋,僅供娛樂並無其他意思):

"sb"

當然,你可能還見過這種形式的 "hello world!" 版本的,即最後輸出的字符串是 "hello world!",這裏由於它的代碼過長就不粘貼出來了,原理和上面類似,我們暫且稱它爲魔法表達式,下面就以上面的例子還分析一下這竄代碼的“魔法”;

類型轉換

分析之前先來了解一些基礎,在 js 的世界中,存在一種類型轉換的機制,大致就是字面意思,下面分別舉例說明;

Number / String

console.log(1 + 1); // 2
console.log('1' + '1'); // "11"
console.log(1 + '1'); // "11"
console.log('1' + 1); // "11"

console.log(1 - 1); // 0
console.log('1' - '1'); // 0
console.log(1 - '1'); // 0
console.log('1' - 1); // 0

console.log('a' - 'b'); // NaN
console.log('a' - '1'); // NaN
console.log('a' - 1); // NaN
// *,/,% 等運算符與 - 運算類似

可以看到,上面的行爲可能有點怪異,因爲在其他語言中可能會報錯,但是在 js 中確實是這樣執行的,即靜默地儘可能地把運算符兩邊的類型轉換一致再運算;

Number / Boolean

當然這種轉換不限於字符串和數字類型之間,也包括其他類型,比如很經典的一中轉換:

console.log(1 == true); // true
console.log(1 === true); // false

上面就是把數字類型和 Boolean 類型的值進行比較,第一行的輸出結果是因爲使用 == 操作符時會自行把 1 轉換爲 true,所以兩邊相等(可以理解爲執行 Boolean(0) 操作);而 === 操作符除了比較兩邊的值,還會比較兩邊類型,二者都相同才判斷相等,即沒有進行類型轉換;

其他類型

其他類型的轉換情況:

console.log(Boolean([])); // true
console.log(Boolean('')); // false
console.log(Boolean(String(''))); // false
console.log(Boolean(new String(''))); // true
console.log(Boolean(' ')); // true
console.log(Boolean({})); // true
console.log(Boolean(0)); // false
console.log(Boolean(Number(0))); // false
console.log(Boolean(new Number(0))); // true

應用

下面是該機制的一些實際應用:

console.log(+'1', typeof +'1'); // 1 "number"
console.log(1 + '', typeof (1 + '')); // 1 "number"
console.log('' + 1, typeof ('' + 1)); // 1 "string"

console.log(+[], typeof +[]); // 0 "number"
console.log(-[], typeof -[]); // -0 "number"
console.log(+[1], typeof +[1]); // 1 "number"
console.log(+[1,2], typeof +[1,2]); // NaN "number"
console.log('' + [], typeof ('' + []), ('' + []).length); // '' "string" 0
console.log([] + '', typeof ('' + []), ('' + []).length); // '' "string" 0

console.log(+{}, typeof +{}); // NaN "number"
console.log({} + '', typeof ({} + '')); // [object Object] "string"
console.log('' + {}, typeof ('' + {})); // [object Object] "string"

另外,"!"“~” 運算符也算是 js 中較爲常見的,其中 ! 是邏輯運算符,代表,而 ~ 是位運算符,代表按位取反,它們有以下關係:

console.log(!true); // false
console.log(!false); // true
console.log(!!true); // true

console.log(~0); // -1
console.log(~3); // -4
console.log(~~3); // 3

分析

現在開始分析最早提到的那串代碼,它的神奇之處就在於整個代碼在不包括任何一個字母的情況下輸出了字母,代碼中都是一些運算符和操作符,代碼串挨在一起不利於觀察,我們先稍微格式化一下:

( !(~+[]) + {} )
[
    --[~+''][+[]] * 
    [~+[]] + 
    ~~!+[]
]

+

( {} + [] ) 
[
    [~!+[]] *
    ~+[]
];

這裏只是拆分美化了一下格式,輸出結果不變,然後一行一行進行分析,根據拆分結果,其實整個代碼就是兩大部分相加;

第一部分

第一部分中,第一行是 (!(~+[]) + {}),也是兩部分加和,根據前面的基礎,可以得到如下分析結果:

     (!(~+[]) + {})

            ↓
     
(!(~0) + "[object Object]")
     
            ↓
            
 (!-1 + "[object Object]")

            ↓
            
(false + "[object Object]")
            
            ↓
            
(false + "[object Object]")
            
            ↓
            
  ("false[object Object]")

第一大部分剩下的內容:

[
    --[~+''] [+[]] * 
    [~+[]] + 
    ~~!+[]
]

分析結果如下:

--[~+''][+[]]  *  [~+[]]   +  ~~!+[]

      ↓             ↓           ↓
      
  --[~0][0]    *   [~0]    +  ~~!0
      
      ↓             ↓           ↓
      
  --[-1][0]    *   [-1]    + ~~true
      
      ↓                         ↓
      
   --(-1)                      ~~1
      
      ↓                         ↓
      
     -2                         1
     
=> -2 * [-1] + 1 = -2 * -1 + 1
                 = 3

所以第一大部分的結果是:

("false[object Object]")[3]; // "s"

第二部分

第二大部分也執行類似的分解,第一行內容是 ({} + []),其實也是在拼接字符串,分析如下:

      ({} + [])
      
          ↓
      
("[object Object]" + "")
      
          ↓
          
  ("[object Object]")

餘下內容是:

[
    [~!+[]] *
    ~+[]
];

分析如下:

 [~!+[]]  *  ~+[]
 
    ↓         ↓
    
  [~!0]   *   ~0
    
    ↓         ↓
    
 [~true]  *   -1
    
    ↓         ↓
    
   [~1]   *   -1
    
    ↓         ↓

   [-2]   *   -1
   
         ↓
         
      -2 * -1
         
         ↓
         
         2

所以第二大部分結果就是:

("[object Object]")[2]; // "b"

彙總

最後兩個大部分內容字符串拼接就是最終結果,這裏彙總一下:

( !(~+[]) + {} ) // "false[object Object]"
[
    --[~+''] [+[]] * // -2
    [~+[]] + // -1
    ~~!+[] // 1
] // => ("false[object Object]")[3] => "s"

+

({} + []) // [object Object]
[
    [~!+[]] * // -2
    ~+[] // -1
]; // => ("[object Object]")[2] => "b"

// => "s" + "b" = "sb"

費了這麼大功夫就得出兩個字母,想必這個過程對於理解 js 中的類型轉換機制是很有幫助的;

魔力所在

然後就是回顧之前那個問題,爲什麼整個代碼沒有出現字母卻在結果中出現了,根據上面的分析可以看出,字符串是通過類似以下方式得到的:

console.log([] + {}); // "[object Object]"
console.log([] + true); // "true"
console.log([] + false); // "false"

然後在 js 中字符串也可以通過類似數組的方式獲取某個字符:

console.log(("[object Objact]")[2]); // "b"
// "b" 在字符串中的索引爲 2

那麼前面提到的輸出 hello world! 的代碼,其實也是通過類似的方式獲取字符然後拼接而成,只是需要思考從哪些格式化輸出中獲取想要的那個字符而已;

拓展

順着上面的思路,如果我們想要輸出任意指定字符,該如何實現呢?即字符中可能包含 a-z, A-Z, 0-9 中的任何一個字符,甚至是特殊字符;

可能的輸出

前面提到輸出的關鍵是存在這個一個標準格式化輸出(如 true, false),然後就能從裏面扣取字符了,作者目前能想到的標準輸出的字符串有如下(可能疏漏):

console.log([]+[]); // ""
console.log([]+!![]); // "true"
console.log([]+![]); // "false"
console.log([]+{}); // "[object Object]"
console.log([]+!![]-[]); // "NaN"
console.log([]+[][+[]]); // "undefined"
console.log([]+~~!![]/+[]) // "Infinity"
console.log(([]+~[])[~~[]]); // "-"

即使這樣,彙總下來的字母也只有:

abcdefiIjlnNoOrstuy-

離目標似乎有點遠~~,數字就比較好弄了:

console.log(+[]); // 0
console.log(+!![]); // 1
console.log(!![]+!![]); // 2(後續的數字可以這樣疊加)
console.log(-~+!![]); // 2(也可以換個簡短的方法)
console.log(!![]+!![]+!![]); // 3
console.log(!![]+!![]+!![]+!![]); // 4
console.log(!![]+!![]+!![]+!![]+!![]); // 5
console.log(!![]+!![]+!![]+!![]+!![]+!![]); // 6
console.log(!![]+!![]+!![]+!![]+!![]+!![]+!![]); // 7
console.log(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]); // 8
console.log(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]); // 9

更大的數字就可以通過字符拼接或者四則運算獲得;

擴大輸出範圍

目前還是沒有獲得大部分大寫字母和特殊字符 ~!@#$%^&*()_+-=\|[]{};':",./<>?,甚至是漢字或者是其他國字符,等等,說到這裏是不是想起了什麼?沒錯就是Unicode(號稱萬國符來着),首先 js 中使用 Unicode 的形式是 "\uXXXX",後面的 XXXX 是四個十六進制字符,例如:

console.log('\u0061'); // "a"

// 獲取 a 的字符編碼
console.log('a'.charCodeAt()); // 97

// 轉換爲 16 進制
console.log('a'.charCodeAt().toString(16)); // "61"
// 0061 = 61

// 漢字
console.log('黃'.charCodeAt().toString(16)); // "9ec4"
console.log('\u9ec4'); // "黃"

所以只要能夠表示 \, u, a-f, 0-9 這幾個字符,就能表示所有 Unicode 字符了!根據前面的總結,其實我們已經能夠表示這幾個字符了!不過呢,直接拼接 Unicode 的話會出現下面的問題:

console.log('\u0061'); // "a"
console.log('\u' + '0061'); // SyntaxError: Invalid Unicode escape sequence
console.log('\\u' + '0061'); // "\u0061"

魔力召喚

eval

所以我們不能通過直接拼接 Unicode 字符串來獲得能被解析的 Unicode 符的,因此不得不換個思路;既然不能拼接直接 Unicode,那麼有麼有間接的方法或者 API 呢?
可能會想到使用 eval()

var s = eval('"' + '\\u' + '0061' + '"');
console.log(s); // "a"

雖然成功了,但是目前我們似乎還無法獲得 eval 中的字母 v,所以要繼續換個方法;

Function

其實還有一個函數可以實現類似的功能,它就是 Function(),即構造函數的函數,也是聲明函數的另一種方法,舉例:

var fn = Function('a', 'b', 'return a + b');
var fn2 = Function('return 4');

console.log(fn(1, 2)); // 3
console.log(fn2()); // 4

然後我們就可以像這樣拼接 Unicode 了:

console.log(Function('return ' + '"\\u' + '0061' + '"')());
// "a"

另外我們需要知道的是:[]['constructor']['constructor'] === Function,所以最後只要構造出這樣的字符串就行了:

[]['constructor']['constructor']('return '+'"'+'\\u0061'+'"')();

魔力聚集

根據前面的基礎,我們已經能夠獲取 constructor, return 裏面的所有字母了,這裏再把需要用到的字符全部彙總一下:

([]+{})[!![]+!![]+!![]+!![]+!![]+!![]+!![]]; // " ""
([]+/\\\\/)[+!![]]; // "\"
[]+(+[]); // "0"
[]+(+!![]); // "1"
[]+(!![]+!![]); // "2"
[]+(!![]+!![]+!![]); // "3"
[]+(!![]+!![]+!![]+!![]); // "4"
[]+(!![]+!![]+!![]+!![]+!![]); // "5"
[]+(!![]+!![]+!![]+!![]+!![]+!![]); // "6"
[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![]); // "7"
[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]); // "8"
[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]); // "9"
([]+![])[+!![]]; // "a"
([]+{})[!![]+!![]]; // "b"
([]+{})[!![]+!![]+!![]+!![]+!![]]; // "c"
([]+[][+[]])[!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]]; // "d"
([]+!![])[!![]+!![]+!![]]; // "e"
([]+![])[+[]]; // "f"
([]+[][+[]])[+!![]]; // "n"
([]+{})[+!![]]; // "o"
([]+!![])[+!![]]; // "r"
([]+![])[!![]+!![]+!![]]; // "s"
([]+!![])[+[]]; // "t"
([]+!![])[!![]+!![]; // "u"

魔力釋放

最後剩下的就是把想要的輸出,轉換爲 Unicode,再拆分爲單個字符對應上面的表達式進行拼接就行了,我們來試一下效果(拿走不謝 ?):

var s1 = 
[][([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([]+[][+[]])[+!![]]+([]+![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[+!![]]+([]+!![])[!![]+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+!![])[+[]]+([]+{})[+!![]]+([]+!![])[+!![]]][([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([]+[][+[]])[+!![]]+([]+![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[+!![]]+([]+!![])[!![]+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+!![])[+[]]+([]+{})[+!![]]+([]+!![])[+!![]]](([]+!![])[+!![]]+([]+!![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[!![]+!![]]+([]+!![])[+!![]]+([]+[][+[]])[+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]+!![]+!![]]+'"'+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![])+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![])+[]+(+[])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![])+([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![])+([]+![])[+[]]+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![]+!![]+!![]+!![]+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![]+!![]+!![]+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![])+[]+(+[])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![])+([]+![])[+[]]+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![]+!![]+!![]+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![])+[]+(+!![])+'"')();

console.log(s1);
// "I love you!"

var s2 = 
[][([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([]+[][+[]])[+!![]]+([]+![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[+!![]]+([]+!![])[!![]+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+!![])[+[]]+([]+{})[+!![]]+([]+!![])[+!![]]][([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([]+[][+[]])[+!![]]+([]+![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[+!![]]+([]+!![])[!![]+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+!![])[+[]]+([]+{})[+!![]]+([]+!![])[+!![]]](([]+!![])[+!![]]+([]+!![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[!![]+!![]]+([]+!![])[+!![]]+([]+[][+[]])[+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]+!![]+!![]]+'"'+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![])+[]+(+!![])+[]+(+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![])+[]+(!![]+!![]+!![])+[]+(+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(!![]+!![]+!![]+!![])+([]+![])[+[]]+[]+(!![]+!![]+!![]+!![]+!![]+!![])+[]+(+[])+'"')();

console.log(s2);
// "我愛你"

在線轉換

這裏放一個在線轉換的網站: 點我在線轉換


技術文章推送
手機、電腦實用軟件分享
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章