談談JavaScript的算數運算、二進位制浮點數舍入誤差及比較、型別轉換和變數宣告提前問題
在《JavaScript權威指南》一書第三章節“型別、值和變數”中,作者詳細介紹了Javascript的數字、文字、布林值等型別,全域性物件,包裝物件,型別轉換,變數作用域等概念。其中有3個地方需要我們在使用過程中引起注意,可能稍不留神就犯錯:
1)算數運算與浮點數比較問題
2)型別轉換問題
3)變數宣告提前問題
1、算數運算與浮點數比較問題
1)JavaScript算數運算中的溢位處理
JavaScript中的算數運算子在溢位(overflow)、下溢(underflow)或被零整除時不會報錯。當正數被0整除會得到無窮大Infinity, 負數被0整除則會得到-Infinity。但有一個例外,零除以零是沒有意義的,這種除法運算結果是一個非數字值NaN。
var a = 10, b = -10;
console.log("a/0: ", a/0); // a/0: Infinity
console.log("b/0: ", b/0); // b/0: -Infinity
console.log("0/0: ", 0/0); // 0/0: NaN
JavaScript中NaN值有別於其它值,它不等於任何值,包含自身,即NaN != NaN。
2)二進位制浮點數和四捨五入錯誤
實數有無數個,但javascript通過浮點數形式只能表示其中有限的個數(確切說是18 437 736 874 454 810 627個)。也就是說,當在javascript使用實數的時候,常常只是真實值的一個近似表示。
javascript採用了IEEE-754浮點表示,這是一種二進位制表示法,可以精確表示分數,比如1/2,1/8,1/1024。二進位制浮點數表示法並不能精確表示類似0.1這樣簡單的數字。
var a = .3, b = .2 + .1;
console.log("b = ", b); // b = 0.30000000000000004
console.log("a == b", a == b); // a == b false
上述程式碼可以看出:0.2 + 0.1 != 0.3,那麼為什麼會這樣?網上有篇博文“該死的IEEE-754浮點數,說「約」就「約」,你的底線呢?以JS的名義來好好查查你”做了詳細闡述。簡單理解就是有些浮點數不能被精確表示,如0.1,因為0.1已經不是龍了,所以它生出來的0.3也不是龍,即不會是精確的0.3。
JavaScript採用的是IEEE-754 64位浮點數,其儲存結構如上圖所示,有3段組成:
- 符號位S:第 1 位是正負數符號位,0代表正數,1代表負數
- 指數位E:中間的 11 位儲存指數,用來表示次方數
- 尾數位M:最後的 52 位是尾數,超出的部分進一舍零
我們用浮點數二進位制轉換工具,IEEE-754 Floating-Point Conversion工具,0.1轉換如下:
0 01111111011 1001100110011001100110011001100110011001100110011010,尾數部分是1001迴圈,由於只有52位,所以最後5位10011,超出部分進一舍零變成1010了,其它0.2大家可以轉換看看,由於0.1用二進位制表示已經有舍入誤差,自然0.3 != 0.1 + 0.2
3)浮點數比較
ECMA定義了Number.EPSILON,這個數大約是10^-16次方數量級的一個數,這個數定義為“大於1的能用IEEE754浮點數表示為數值的最小數與1的差值”,這個數用來幹嘛呢?
0.1+0.2-0.3<Number.EPSILON返回true,也就是說ECMA預設了一個精度,便於開發者使用,但是我們現在可以知道這個預定義的值其實是對應 100 數量級數值的精確度,如果你要比較更小數量級的兩個數,預定義的這個Number.EPSILON就不夠用了(不夠精確了),你可以用數學方式將這個預定義值的數量級進行縮小。上面a==b程式碼修改如下:
var a = .3, b = .2 + .1;
console.log(b); // 0.30000000000000004
console.log(Number.EPSILON); //2.220446049250313e-16
console.log("a == b", b - a < Number.EPSILON); //true
2、型別轉換問題
1)隱形型別轉換
JavaScript取值型別非常靈活且非常智慧,基本上你希望它是什麼型別,就會轉換成什麼型別。如:if判斷,則會把其它值先轉換成布林值(boolean值,undefined, null, 0, -0, NaN, ""空字串會轉換成false,其它則會轉換成true),其它隱形轉換,看個例子:
console.log(10 + " objects"); // 10 objects
console.log("7"*"4"); // 28
console.log(2 + null); // 2, null轉換為0後做加法
console.log(2 + undefined); // NaN, undefiend轉換為NaN後做加法
var n = 1 - "x"; // NaN
console.log(n + " objects"); // NaN objects
- 有字串連線時,非字串轉換成字串;
- 數字運算(如上例的* -),非數字轉換成數字;
參考JavaScript權威指南一書的型別轉換表格,如下:
值 | 字串 | 數字 | 布林值 | 物件 |
---|---|---|---|---|
undefiend null |
“unedfiend” “null” |
NaN 0 |
false | throws TypeError |
true false |
“true” “false” |
1 0 |
new Boolean(true) new Boolean(false) |
|
“”(空字串) “1.2”(非空,數字) “one”(非空,非數字) |
0 1.2 NaN |
false true true |
new String("") new String(“1.2”) new String(“one”) |
|
0 -0 NaN Infinity -Infinity 1(無窮大,非零) |
“0” “0" “NaN” “Infinity” ”-Infinity" “1” |
false false false true true true |
new Number(0) new Number(-0) new Number(NaN) new Number(Infinity) new Number(-Infinity) new Number(1) |
|
{}(任意物件) [](任意陣列) [9](1個數字元素) [‘a’](其它陣列) function(){}(任意函式) |
參考物件轉原始值 "" "9" 使用join方法 參考物件轉原始值 |
參考物件轉原始值 0 9 NaN NaN |
true true true true true |
補充說明:
陣列[9] 轉換成:
- 字串使用toString方法,內部join方法連得到"9";
- 轉換數字,由於陣列valueOf返回本身,所以使用陣列toString方法先得到字串"9",然後字串"9"再轉換成數字9。
2) 顯性轉換
做顯示型別轉換最簡單的方法就是使用Boolean(), Number(), String()或Object()函式,本文不再詳述。
Number("3") => 3
String(false) => "false"
Boolean([]) => true
Object(3) => new Number(3)
3) 物件轉換為原始值
物件到字串和物件到數字的轉換是通過呼叫物件的一個方法來完成的。一個麻煩的事實是,JavaScript物件有兩個不同的方法來執行轉換:toString()和valueOf()。
toString() 返回字串
陣列返回join字串,函式返回函式定義,日期返回可讀日期和時間字串,RegExp返回表示式字串等,看個例子:
[1,2,3].toString() //"1,2,3"
(function(x){f(x);}).toString() //"function(x){f(x);}"
/\d+/g.toString() //"/\\d+/g"
new Date().toString() //Tue Nov 06 2018 15:40:09 GMT+0800 (CST)
**valueOf() **
這個方法沒有詳細定義;如果物件存在任意原始值,它將返回原始值。物件是符合值,而且大多數物件無法真正表示為一個原始值,因此預設的valueOf()方法簡單返回物件本身。陣列、函式和正則表示式,呼叫vlaueOf返回物件本身。日期返回它的一個內部表示:1970年1月1日以來的毫秒數,看個例子:
var now = new Date;
console.log(now+1, typeof(now+1)); //Tue Nov 06 2018 15:49:26 GMT+0800 (CST)1 string
console.log(now-1, typeof(now-1)); //1541490566913 number
console.log(now == now.toString()); //true
console.log(now > now - 1); //true
console.log(now.valueOf()); //1541490566914
程式碼說明:
typeof(now+1),字串連線,now.toString() + 1
typeof(now-1),數字相減,now.valueOf() - 1
3、變數和函式宣告提前問題
1)變數申明提前
JavaScript會將變數申明提前,執行到var語句的時候,變數才會被真正賦值,看個例子:
console.log(a); //undefined
var a = 1;
console.log(a); //1
程式碼等價於:
var a;
console.log(a); //undefined
a = 1;
console.log(a); //1
2)函式宣告提前
函式建立的三種寫法:
- 函式申明:function fn(){…};(只有這個會函式申明提前)
- 函式表示式:var fn = function(){…};
- 建構函式:var fn = new Function(‘msg’, ‘alert(msg)’);
看個例子:
num(); //num()=>1
console.log(num) //函式本身
function num(){
console.log(1);
}
num(); //1
3)變數和函式宣告順序問題
函式申明優先順序高於變數申明,但是不會覆蓋變數賦值,看個例子:
console.log(fn); //輸出函式定義fn(){ console.log("函式申明"); }
var fn = "變數申明";
function fn(){ console.log("函式申明"); }
console.log(fn); // 變數申明
程式碼等價於:
var fn;
function fn(){ console.log("函式申明"); } //函式申明覆蓋變數宣告
console.log(fn);
fn = "變數申明";
console.log(fn); // 變數申明