原文:https://pantao.parcmg.com/press/parseint-mystery.html
Javascript 總是以超自然的方式執行我們的代碼,這是一件很神奇的事情,如果不行的話,思考一下 ['1', '7', '11'].map(parseInt)
的結果是什麼?你以爲會是 [1, 7, 11]
嗎?我都這麼問了,那肯定不是:
['1', '7', '11'].map(parseInt)
// 輸出:(3) [1, NaN, 3]
要理解爲什麼爲會這裏,首先需要了解一些 Javascript 概念,如果你是一個不太喜歡閱讀長文的人,那直接跳到最後看結論吧
真值與假值
請看下面示例:
if (true) {
// 這裏將啓動都會被執行
} else {
// 這裏永遠都不會被執行
}
在上面的示例中, if-else
聲明爲 true
,所以 if-block
會一直都執行,而 else-block
永遠都會被忽略掉,這是個很容易理解的示例,下面我們來看看另一個例子:
if ("hello world") {
// 這裏會被執行嗎?
console.log("條件判斷爲真");
} else {
// 或者會執行這裏嗎?
console.log("條件判斷爲假");
}
打開你的開發者工具 console
面板,執行上面的代碼,你會得到看到會打印出 條件判斷爲真,也就是說 "hello world"
這一個字符串被認爲是真值。
在 JavaScript 中,truthy(真值)指的是在布爾值上下文中,轉換後的值爲真的值。所有值都是真值,除非它們被定義爲 假值(即除 false、0、""、null、undefined 和 NaN 以外皆爲真值)。
這裏一定要劃重點:在布爾上下文中,除 false
、0
、""
(空字符串)、null
、undefined
以及 NaN
外,其它所有值都爲真值
基數
0 1 2 3 4 5 6 7 8 9 10
當我們從 0
數到 9
,每一個數字的表示符號都是不一樣的(0-9),但是當我們數到 10
的時候,我們就需要兩個不同的符號 1
與 0
來表示這個值,這是因爲,我們的數學計數系統是一個十進制的。
基數,是一個進制系統下,能使用僅僅超過一個符號表示的數字的最小值,不同的計數進制有不同的基數,所以,同一個數字在不同的計數體系下,表示的真實數據可能並不一樣,我們來看一下下面這張在十進制、二進制以及十六進制不同值在具體表示方法:
十進制(基數:10) | 二進制(基數:2) | 十六進制(基數:16) |
---|---|---|
0 | 0 | 0 |
1 | 1 | 1 |
2 | 10 | 2 |
3 | 11 | 3 |
4 | 100 | 4 |
5 | 101 | 5 |
6 | 110 | 6 |
7 | 111 | 7 |
8 | 1000 | 8 |
9 | 1001 | 9 |
10 | 1010 | A |
11 | 1011 | B |
12 | 1100 | C |
13 | 1101 | D |
14 | 1110 | E |
15 | 1111 | F |
16 | 10000 | 10 |
17 | 10001 | 11 |
18 | 10002 | 12 |
你應該已經注意到了, 11 在上表中,總共出現了三次。
函數的參數
在 Javascript 中,一個函數可以傳遞任何多個數量的參數,即使調用時傳遞的數量與定義時的數量不一致。缺失的參數會以 undefined
作爲實際值傳遞給函數體,然後多餘的參數會直接被忽略掉(但是你還是可以在函數體內通過一個類數組對象 arguments
訪問到)。
function sum(a, b) {
console.log(a);
console.log(b);
}
sum(1, 2); // 輸出:1, 2
sum(1); // 輸出:1, undefined
sum(1, 2, 3); // 輸出:1, 2
map()
快要有結果了。
map()
是一個存在於數組原型鏈上的方法,它將其數組實例中的每一個元素,傳遞給它的第一個參數(在本文最開始的例子中就是 parseInt
),然後將每一個返回值都保存到同一個新的數組中,遍歷完所有元素之後,將新的包含了所有結果的數組返回。
function multiplyBy3 (number) {
return number * 3;
}
const result = [1, 2, 3, 4, 5].map(multiplyBy3);
console.log(result); // 輸出:[3, 6, 9, 12, 15]
現在,假想一下,我們想使用 map()
將一個數組中的每一個元素都打印到控制檯,我可以直接將 console.log
函數傳遞給 map()
方法:
[1, 2, 3, 4, 5].map(console.log);
輸出:
1 0 (5) [1, 2, 3, 4, 5]
2 1 (5) [1, 2, 3, 4, 5]
3 2 (5) [1, 2, 3, 4, 5]
4 3 (5) [1, 2, 3, 4, 5]
5 4 (5) [1, 2, 3, 4, 5]
是不是感覺有點神奇了?爲什麼會這樣?來分析一下上面輸出的內容都有些什麼?
- 第一列:這個看上去跟我們想要的輸出是一致的
- 第二列:這個是一個從
0
開始遞增的數字 - 第三列:總是
(5) [1, 2, 3, 4, 5]
再來看看下面這段代碼:
[1, 2, 3, 4, 5].map((value, index, array) => console.log(value, index, array));
在控制檯上試試執行上面這行代碼,你會發現,它與 [1, 2, 3, 4, 5].map(console.log);
輸出的結果是完全一致的,**總是會被我們忽略的一點是,map()
方法會將三個參數傳遞傳遞給它的函數,分別是 currentValue
(當前值)、currentIndex
(當前索引)以及 array
本身,這就是,上面爲什麼結果有三列的原因,如果我們只是想將每一個值打印,那麼需要像下面這樣寫:
[1, 2, 3, 4, 5].map((value) => console.log(value));
回到最開始的問題
現在讓我們來回顧一下本文最開始的問題:
爲什麼 ['1', '7', '11'].map(parseInt)
的結果是 [1, NaN, 3]
?
來看看 parseInt()
是一個什麼樣的函數:
parseInt(string, radix)
將一個字符串string
轉換爲radix
進制的整數,radix
爲介於2-36
之間的數。詳情:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/parseInt
這裏還有一條需要注意的是,雖然我們一般都是使用 parseInt('11')
這樣的調用方式,認爲是將 11
按十進制轉換成數字,但是,請在使用的時候,永遠都添加 radix
參數,因爲, radix
爲 10
並不保證永遠有效(這並不是規範的一部分)。
那麼看看下面這些的輸出:
parseInt('11'); // 輸出:11
parseInt('11', 2); // 輸出:3
parseInt('11', 16); // 輸出:17
parseInt('11', undefined); // 輸出:11 (radix 是假值)
parseInt('11', 0); // 輸出:11 (radix 是假值)
現在,讓我們一步一步來看 ['1', '7', '11'].map(parseInt)
的整個執行過程,首先,我們可以將這一行代碼,轉換爲完整的版本:
['1', '7', '11'].map((value, index, array) => parseInt(value, index, array))
根據前面的知識,我們應該知道, parseInt(value, index, array)
中的 array
會被忽略(並放入 arguments
對象中。
那麼,完整的代碼就是下面這樣的:
['1', '7', '11'].map((value, index, array) => parseInt(value, index))
-
遍歷到第一個元素時:
parseInt('1', 0);
radix
爲0
,是一個假值,在我們的控制檯中,一般將使用10
進制,所有,得到結果1
-
遍歷到第二個元素時:
parseInt('7', 1)
在一個
1
進制系統中,7
是不存在的,所以得到結果NaN
(不是一個數字) -
遍歷到第三個元素時:
parseInt('11', 2)
在一個
2
進制系統中,11
就是進十制的3
總結
['1', '7', '11'].map(parseInt)
的結果是 [1, NaN, 3]
的原因是因爲,map()
方法是向傳遞給他的函數中傳遞三個參數,分別爲當前值,當前索引以及整個數組,而 parseInt
函數接收兩個參數:需要轉換的字符串,以及進制基數,所以,整個語句可以寫作:['1', '7', '11'].map((value, index, array) => parseInt(value, index, array))
,array
被 parseInt
捨棄之後,得到的結果分別是:parseInt('1', 0)
、parseInt('7', 1)
以及 parseInt('11', 2)
,也就是上面看到的 [1, NaN, 3]
。
正確的寫法應該是:
['1', '7', '11'.map(numStr => parseInt(numStr, 10));