第1条:了解使用的JavaScript版本
第2条:理解JavaScript的浮点数
大多数编程语言都有几种数值性数据类型,但是JavaScript却只有一种。你可以使用typeof运算符查看数字的类型。不管是整数还是浮点数,JavaScript都将它们简单地归为数字。
typeof 17;//number typeof 98.6;//number typeof -2.1;//number
事实上,JavaScript中所有的数字都是双精度浮点数。这是有IEEE 754标准制定的64位编码数字————即“doubles”。如果这一事实使你疑惑JavaScript是如何表示整数的,请记住,双精度浮点数能完美地表示高达53位精度的整数。从-9007 199 254 740 992(-253)到9007 199 254 740 992(253)的所有整数都是有效的双精度浮点数。因此,尽管JavaScript中缺少明显的整数类型,但是完全可以进行整数运算。
大多数的算术运算符可以使用整数、实数或两者的组合进行计算。
0.1 * 1.9; //0.19 -99 + 100; //1 21 - 12.3; //8.7 2.5 / 5; //0.5 21 % 8; //5
然而位算术运算符比较特殊。JavaScript不会直接将操作数作为浮点数进行运算,而是会将其隐式地转换为32位整数后进行运算。(确切地说,它们被转化为32大端(big-endian)的2的补码表示的整数。)以按位或运算表达式为例:
8 | 1;//9
看似简单的表达式实际上需要几个步骤来完成运算。如前所述,JavaScript中的数字8和1都是双精度浮点数。但是它们也可以表示成32位整数,即32位0、1的序列。整数8表示为32位二进制序列如下所示:
00000000000000000000000000001000
你自己可以使用数字类型的toString()方法来查看:
(8).toString(2); //1000
toString 方法的参数指定了其转换基数,此例子以基数2(即二进制)表示。结果值省略了左端多余的0(位),因为它们并不影响最终值。
整数1表示为32位二进制如下所示:
00000000000000000000000000000001
按位或运算表达式合并两个比特序列。只要参与运算的两位比特中任意一位为1,运算结果的该位就为1.以位模型表示的结果如下:
00000000000000000000000000001001
这个序列表示整数9.你可以使用标准的库函数parseInt 验证,同样以2作为基数:
parseInt("1001",2);//9
(同样,前导0位是不必要的,因为它们并不影响运算结果。)
所有位运算符的工作方式都是相同的。它们将操作数转换为整数,然后使用整数位模式进行运算,最后将结果转换为标准的JavaScript浮点数。一般情况下,JavaScript引擎需要做些额外的工作来进行这些转换。因为数字是以浮点数存储的,必须将其转换为整数,然后再转换回浮点数。然而,在某些情况下,算术表达式甚至变量只能使用整数参与运算,优化编译器有时候可以推断出这些情形而在内部将数字以整数的方式存储以避免多余的转换。
关于浮点数的最后警示是,你应该对它们保持时刻警惕。浮点数看似熟悉,但是它们是出了名的不精确。甚至一些看起来最简单的算术运算都会产生不正确的结果。
0.1+0.2; //0.30000000000000004
尽管64位的精度已经相当高了,但是双精度浮点数也只能表示一组有限的数字,而不能表示所有的实数集。浮点运算只能产生近似的结果,四舍五入到最接近的可表示的实数。当你执行一系列的运算,随着舍入误差的积累,元素结果会越来越不精确。舍入也会使我们通常所期望的算术运算定律产生一些出人意料的偏差。例如,实数满足结合律,这意味着,对于任意的实数x,y,z,总是满足(x+y)+z=x+(y+z)。
然而,对于浮点数来说,却并不总是这样。
(0.1+0.2)+0.3; //0.6000000000000001
0.1+(0.2+0.3); //0.6
浮点数权衡了精度和性能。当我们关心精度时,要小心浮点数的局限性。一个有效的解决方法是尽可能地采用整数值运算,因为整数在表示时不需要舍入。当进行货币相关的计算时,程序员通常会按比例将数值转换为最小的货币单位来表示再进行计算,这样就可以以整数进行计算。例如,如果上面的计算是以美元为单位,那么,我们可以将其转换为整数表示的美分进行计算。
(10+20)+30; //60 10+(20+30); //60
对于整数运算,你不必担心舍入误差,但是你还是要当心所有的计算只适合于 -253 ~253的整数。
提示:
□ JavaScript的数字都是双精度的浮点数
□ JavaScript中的整数仅仅是双精度浮点数的一个子集,而不是一个单独的数据类型。
□ 位运算符将数字视为32位的有符号整数
□ 当心浮点运算中的精度陷阱。
第3条:当心隐式的强制转换
JavaScript对类型错误错误出奇宽容。许多语言都认为表达式错误的,
3 + true; //4
因为布尔表达式(如 true)与算术运算是不兼容的。在静态类型语言中,含有类似这样表达式的程序甚至不会被允许运行。在一些动态类型语言中,含有类似这样表达式的程序可以运行,但是会抛出一个异常。然而,JavaScript不仅允许程序运行,而且还会顺利地产生出结果4!
在JavaScript中有一些极少数的情况,提供错误的类型会产生一个即时错误。例如,调用一个非函数对象(nonfunction)或试图选择null的属性。
"hello"(1) //error: not a function null.x; //error: cannot read property 'x' of null
但是在大多数情况下,JavaScript不会抛出一个错误,而是按照多种多样的自动转换协议将值强制转换为期望的类型。例如,算术运算符 -、*、/和%在计算之前都会尝试将其参数转换为数字。而运算符+更为微妙,因为它既重载了数字相加,又重载了字符串连接操作。
具体是数字相加还是字符串连接,这取决于其参数的类型。
2 + 3; //5 "hello" + " world"; //"hello world"
接下来,合并一个数字和一个字符串会发生什么呢?JavaScript打破了这一束缚,它更偏爱字符串,将数字转换为字符串。
"2" +3; //"23" 2 +"3"; //"23"
类似这样的混合表达式有时令人困惑,因为JavaScript对操作顺序时敏感的。例如,表达式:
1 + 2 + "3"; //"33"
由于加法运算符是自左结合的(即左结合律),因此,它等同于下面的表达式:
(1 + 2 ) + "3"; //"33"
与此相反,表达式:
1 + "2" + 3; //"123"
的计算结果为字符串“123”。左结合律相当于是将表达式左侧的加法运算包裹在括号中。
(1 + "2") + 3; //"123"
位运算符不仅会将操作数转换为数字,而且还会将操作数转换为32位整数(表示的数字的子集)。我们在第2条已经讨论过。这些运算符包括位算术运算符(~、&、^和|)以及移位运算符(<<、>>和>>>)
这些强制转换十分方便。例如:来自用户输入,文本文件或网络流的字符串都将被自动转换。
"17" * 3; //51 "8" | 1; //9
但是强制转换也会隐藏错误。结果为null的变量在算术运算中不会导致失败,而是隐式地转换为0;一个未定义的变量将被特殊的浮点数值NaN(自相矛盾地命名为"not a number"。谴责IEEE浮点数标准!)。这些强制转换不是立即抛出一个异常,而是继续运算,旺旺导致一些令人困惑和不可预测的结果。无奈的是,即使是测试NaN值也是异常困难的。第一,JavaScript遵循了IEEE浮点数标准令人头疼的要求————NaN不等于其本身。因此,测试一个值是否等于NaN根本行不通。
var x = NaN; x === NaN; //false
另外,标准的库函数isNaN也是不是很可靠,因为它带有自己的隐式强制转换,在测试其参数之前,会将参数转换为数组(isNaN函数的一个更精确的名称可能是coercesToNaN)。如果已经知道一个值是数字,可以使用isNaN函数判断是否是NaN.
NaN是什么? not a nunber //不是数字
js判断NaN?不是有isNaN?标准的函数库isNaN不可靠的。
isNaN(NaN);//true 这很正常
isNaN("张为是最帅的");//true 这TM还正常吗?
isNaN({});//true 这TM还正常吗?
还好NaN是Javascript中唯一一个不等于本身的值,呵呵,写个自定义函数来判断它是不是NaN。
//判断是不是NaN--not a number function isReallyNaN(z) { return z != z; }
其实测试一个值是否与其自身相等是非常简洁的,通常没有必要借助于一个辅助函数,但关键在于识别和理解。
隐式的强制转换使得调试一个出问题的程序变得令人异常沮丧,因为它掩饰了错误,使错误更难以诊断。当一个计算出了问题,最好的调试方式是检查这个计算的中间结果,回到出错前的“最后一点”。在那里,你可以检查每个操作的参数,查看错误类型的参数。根据错误的不同,它可能是一个逻辑错误(如使用了错误的算术运算符),也可能是一个类型错误 (如传入了一个undefined的值而不是数字)。
对象也可以被强制转换为原始值。最常见的用法是转换为字符串。
"the Math object:"+Math;//"the Math object:[object Math]" "the JSON object:"+JSON;//"the JSON object:[object JSON]"
对象通过隐式地调用其自身的toString方法转换为字符串。你可以调用对象的toString方法进行调试。
Math.toString();//"[object Math]" JSON.toString();//"[object JSON]"
类似地,对象也可以通过其valueOf方法转换为数字。通过定义类似下面这些方法,你可以控制对象的类型转换。
"J" +{toString:function(){ return "S"}};//"JS" 2 * {valueOf:function(){ return 3}};//6
再一次,当你认识到运算符+被重载来实现字符串连接和加法时,事情变得棘手起来。特别是,当一个对象同时包含toString和valueOf方法时,运算符+应该调用那个方法并不明显————做字符串连接还是加法应该根据参数的类型,但是存在隐式的强制转换,因此类型并不是显而易见!JavaScript通过盲目地选择valueOf方法而不是toString方法来解决这种含糊的情况。但是,这就意味着如果有人打算对一个对象执行字符串连接操作,那么产生的行为将会出乎意料。
1 var obj={ 2 toString:function(){ 3 return "[object MyObject]"; 4 }, 5 valueOf:function(){ 6 return 17; 7 } 8 }; 9 console.log("object:"+obj);//"object:17"
这个例子的说明,valueOf方法才真正是为那些代表数值的对象(如Number对象)而设计的。对于这些对象,toString和valueOf方法应返回一至的结果(相同数字的字符串或数值表示),因此,不管是对象的连接还是对象的相加,重载的运算符+总是一致的行为。一般情况下,字符串的强制转换远比数字的强制转化更常见,更有用。最好避免使用valueOf方法,除非对象的确时一个数字的抽象,并且obj.toString()能产生一个obj.valueOf()的字符串表示。
最后一种强制转换有时成为真值运算(truthiness)。if、|| 和&&等运算符逻辑上需要布尔值作为操作参数,但实际上可以接受任何值。JavaScript按照简单的隐式强制转换规则将值解释为布尔值。大多数的JavaScript值都为真值(truthy),也就是能隐式强制转换对true。对于字符串和数字以外的其他对象,真值运算不会隐式调用任何强制转换方法。JavaScript中有7个假值:false、0、-0、“”、NaN、null和undefined。其他所有值都为真值。由于数字和字符串可能为假值,因此,使用真值运算检查函数参数或者对象属性是否已定义不是绝对安全的。例如,一个带有默认值的接收可选参数的函数:
1 function point(x, y) { 2 if(!x) { 3 x = 320; 4 } 5 if(!y) { 6 y = 240; 7 } 8 return { x: x, y: y }; 9 } 10 //此函数忽略任何为假值的参数,包括0 11 point(0,0);//{x:320,y:240} 12 //检查参数是否为undefined更为严格的方式是使用typeof。 13 function point(x,y){ 14 if(typeof x ==="undefined"){ 15 x=320; 16 } 17 if(typeof y ==="undefined"){ 18 y=240; 19 } 20 return {x:x,y:y}; 21 } 22 //此版本的point函数可以正确地识别0和undefined 23 point(); //{x:320,y:240} 24 point(0,0);//{x:0,y:0} 25 //另一种方式是与undefined进行比较 26 if(x===undefined){}
□ 类型错误可能被隐式的强制转换所隐藏。
□ 重载的运算符+是进行加法运算还是字符串连接操作取决于其参数类型。
□ 对象通过valueOf方法强制转换为数字,通过toString 方法强制转换为字符串。
□ 具有valueOf 方法的对象应该实现toString 方法,返回一个valueOf方法产生的数字的字符串表示。
□ 此时一个值是否为未定义的值,应该使用typeof或者与undefined进行比较而不是使用真值运算。
第4条:原始类型优于封装对象
除了对象之外,JavaScript有5个原始值类型:布尔值、数字、字符串、null和undefined。(令人困惑的是,对null类型进行typeof操作得到的结果为“object”,然而,ECMAScript标准描述其为一个独特的类型。)同时,标准库提供了构造函数来封装布尔值、数字和字符串作为对象。你可以创建一个String对象,该对象封装了一个字符串值。
var s=new String("hello");
在某些方面,String对象的行为与其封装的字符串值类似。你可以通过将它与另一个值连接来创建字符串。
s+" world";//hello world
你也可以提取其索引的子字符串。
s[4]; //o
但是不同于原始的字符串,String对象时一个真正的对象
typeof "hello";//"string" typeof s;//"object"
这是一个重要的区别,因为这意味着你不能使用内置的操作符来比较两个截然不同的String对象的内容。
var s1=new String("hello"); var s2=new String("hello"); s1===s2;//false
由于每个String对象都是一个单独的对象,其总是只等于自身。对于非阉割过相等运算符,结果同样如此
s1==s2;//false
由于这些封装的行为并不十分正确,所以用处不大。其存在的主要函数是它们的实用方法。结合另外的隐式强制转换,JavaScript使得我们可以方便地使用这些实用方法因为这里有另一个隐式转化:当对原始值提取属性和进行方法调用时,它表现得就像已经使用了对应的对象类型封装了该值一样。例外:String的原型对象有一个toUpperCase方法,可以将字符串转换为大写。你可以对原始字符串值调用这个方法。
"hello".toUpperCase();//HELLO
这种隐式封装的一个奇怪后果是你可以对原始值设置属性,但是对其丝毫没有影响。
"hello".someProperty=17; "hello".someProperty;//undefined
因为每次隐式封装都会产生一个新的String对象,更新第一个封装对象并不会造成持久的影响。对原始值设置属性的确是没有意义的,但是觉察到这种行为是值得的。事实证明,这是JavaScript隐藏类型错误的又一种情形。本来你想给一个对象设置属性,但没注意其实它是个原始值,程序只是忽略更新而继续运行。这容易导致一些难以发现的错误,并且难以诊断。
□ 当做相等比较时,原始类型的封装对象与其原始值行为不一样。
□ 获取和设置原始类型值的属性会隐式地创建封装对象。
第5条:避免对混合类型使用==运算符
你认为下面表达式的值是什么?
"1.0e0"=={valueOf:function(){ return true}};
对这两个看似无关的值使用==运算符实际上相等的。就像第3条描述的隐式强制转换一样,在比较之前,它们都被转换为数字。字符串“1.0e0”被解析为数字1,而匿名对象也通过调用其自身的valueOf方法得到结果为true,然后再转换为数字,得到1.
很容易使用这些强制转换完成一些工作。例如,从一个web表单读取一个字段并与一个数字进行比较。
"1.0e0"=={valueOf:function(){ return true}}; //true
1 var today=new Date(); 2 3 if(form.month.value ==(today.getMonth()+1) && form.day.value==today.getDate()){ 4 //happy birthday 5 }
但实际上,它只是显式地使用了Number函数或者一元运算符+将值转换为数字。
1 var today=new Date(); 2 3 if(+form.month.value ==(today.getMonth()+1) && +form.day.value==today.getDate()){ 4 //happy birthday 5 }
上面这段代码更加清晰,因为它想读者传达了代码到底在做什么样的转换,而不要求读者记住这些转换规则。一个更好的代替方法是使用严格相等运算符。
1 var today=new Date(); 2 3 if(+form.month.value === (today.getMonth()+1) && +form.day.value === today.getDate()){ 4 //happy birthday 5 }
当两个参数属于同一个类型时,==和===运算符的行为是没有区别的。因此,如果你知道参数是同一类型,那么,==和===运算符可以互换。但最好使用严格相等运算符,因为读者会非常清晰地知道:在比较操作中并没有涉及任何转换。否则,你需要读者准确地记住这些强制转换规则以解读代码的行为。
事实上,这些强制转换规则一点也不明显。表1.1包含了===运算符针对不同类型参数的强制转换规则。这些规则具有对称性。例如,第一条规则既适于null==undefined,也适用于undefined == null 。在很多时候,这些转换都试图产生数字。但当它们处理对象时会变得难以捉弄。操作符视图将对象转换为原始值,可通过调用对象的valueOf和toString方法而实现。更令人难以捉摸的是,Date对象以相反的顺序尝试调用这两个方法。
表1.1 ==运算符的强制转换规则 | ||
参数类型1 | 参数类型2 | 强制转换 |
null | undefined | 不转换,总是返回true |
null 或 undefined | 其他任何非null 或 undefined的类型 | 不转换,总是返回false |
原始类型:string,number或boolean | Date对象 | 将原始类型转换为数字;将Date对象转换为原始类型(优先尝试toString方法,再尝试valueOf方法) |
原始类型:string,number或boolean | 非Date对象 | 将原始类型转换为数字;将非Date对象转换为原始类型(优先尝试toString方法,再尝试valueOf方法) |
原始类型:string,number或boolean | 原始类型:string,number或boolean | 将原始类型转换为数字 |
==运算符将数据以不同的表现呈现出来,这种纠错有时称为“照我的意思去做”(do what I mean)的语义。但计算机并不能真正地了解你的心思。世界上有太多的数据表现形式,JavaScript需要知道你使用的是那种。例如,你可能希望你能将一个包含日期的字符串和一个Date对象进行比较。
var date= new Date("1999/12/31"); date=="1999/12/31";//false
这个例子失败是因为Date对象被转换成一种不同格式的字符串,而不是本例所采用的格式。
date.toString();"Tue Dec 31 199 00:00:00 GMT+0800 (中国标准时间)"
但是,这种错误是一个更普遍的强制转换误解的“症状”。==运算符并不能推断和统一所有的数据格式。它需要你和读者都能理解其微妙的强制转换规则。更好的策略是显式自定义应用程序转换的逻辑,并使用严格相等运算符。
1 function toYMD(date){ 2 var y=date.getYear()+1900,// year is 1900-indexed 3 m=date.getMonth() +1,//month is 0-indexed 4 d=date.getDate(); 5 return y+"/"+(m<10?"0"+m:m)+"/"+(d<10?"0"+d:d); 6 } 7 8 toYMD(date)==="1999/12/31";//true
显式地定义转换的逻辑能确保你不会混淆==运算符的强制转换规则,而且免除了读者不得不查找或记住这些规则的麻烦。
□ 当参数类型不同时,==运算符应用了一套难以理解的隐式强制转换规则。
□ 使用===运算符,使读者不需要涉及任何隐式强制转换就能明白你的比较运算。
□ 当比较不同类型的值时,使用你自己的显式强制转换使程序的行为更清晰。
第6条:了解分号插入的局限
JavaScript的一个便利是能够离开语句结束分号工作。删除分号后,结果变得轻量而优雅。
function Point(){ isLock=false; } Print.prototype.islock=function(){ isLock=true; }
上面的代码能工作多亏了JavaScript的自动分号插入(automatic semicolon insertion)技术,它是一种程序解析技术。它能推断出某些上下文中省略的分号,然后有效地自动地将分号“插入”到程序中。ECMAScript标准细心地指定了分号插入机制,因此,可选分号可以在不同的JavaScript引擎之间移植。
但是同第3条和第5条的隐式强制转换一样,分号插入也有其陷阱。
分号插入的第一条规则:
分号仅在 | 标记之前、一个或多个换行之后和程序输入的结尾被插入。
换句好说,你只能在一行、一个代码块和一段程序结束的地方省略分号。因此,下面的函数定义是合法的。
function square(x0){ var n=+x return n*n; } function area(r){r=+r;return Math.PI*r*r}
但是,下面这个却不合法。
function area(r){r=+r return Math.PI*r*r} //error
分号插入的第二条规则:
分号仅在随后的输入标记不能解析时插入。
换句话说,分号插入是一种错误校正机制。下面这段代码作为一个简单的例子。
a = b
(f());
能正确地解析为一条单独的语句,等价于:
a= b(f());
也就是说,没有分号插入。与此相反,下面这段代码:
a= b
f();
被解析为两条独立的语句,因为
a=b f();
解析有误。
这条规则有一个不幸的影响:你总是要注意下一条语句的开始,从而发现你是否能合法地省略分号。如果某条语句的下一行的初始标记不能被解析为一个语句的延续,那么,你就不能省略该语句的分号。
有5个明确的有问题的自负需要密切注意:(、[、+、和/。每一个字符都能作为一个表达式与运算符或者一条语句的前缀,这依赖于具体上下文。因此,要小心堤防那些以表达式结束的语句,就想上面的复制语句一样。如果下一行以这5个有问题的字符之一开始,那么不会插入分号。到目前为止,最常见的情况是以一个括号开始,就像上面的例子。另一个常见的情况是数组字面量。