在《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); // 变量申明