首页 » 编写高质量代码:改善JavaScript程序的188个建议 » 编写高质量代码:改善JavaScript程序的188个建议全文在线阅读

《编写高质量代码:改善JavaScript程序的188个建议》建议4:注意JavaScript数据类型的特殊性

关灯直达底部

1.防止浮点数溢出

二进制的浮点数不能正确地处理十进制的小数,因此0.1+0.2不等于0.3。


num=0.1+0.2;//0.30000000000000004


这是JavaScript中最经常报告的Bug,并且这是遵循二进制浮点数算术标准(IEEE 754)而导致的结果。这个标准适合很多应用,但它违背了数字基本常识。幸运的是,浮点数中的整数运算是精确的,所以小数表现出来的问题可以通过指定精度来避免。例如,针对上面的相加可以这样进行处理:


a=(1+2)/10;//0.3


这种处理经常在货币计算中用到,在计算货币时当然期望得到精确的结果。例如,元可以通过乘以100而全部转成分,然后就可以准确地将每项相加,求和后的结果可以除以100转换回元。

2.慎用JavaScript类型自动转换

在JavaScript中能够自动转换变量的数据类型,这种转换是一种隐性行为。在自动转换数据类型时,JavaScript一般遵循:如果某个类型的值被用于需要其他类型的值的环境中,JavaScript就自动将这个值转换成所需要的类型,具体说明见表1.1。

如果把非空对象用在逻辑运算环境中,则对象被转换为true。此时的对象包括所有类型的对象,即使是值为false的包装对象也被转换为true。

如果把对象用在数值运算环境中,则对象会被自动转换为数字,如果转换失败,则返回值为NaN。

当数组被用在数值运算环境中时,数组将根据包含的元素来决定转换的值。如果数组为空数组,则被转换为数值0。如果数组仅包含一个数字元素,则被转换为该数字的数值。如果数组包含多个元素,或者仅包含一个非数字元素,则返回NaN。

当对象用于字符串环境中时,JavaScript能够调用toString方法把对象转换为字符串再进行相关计算。当对象与数值进行加号运算时,则会尝试将对象转换为数值,然后参与求和运算。如果不能够将对象转换为有效数值,则执行字符串连接操作。

3.正确检测数据类型

使用typeof运算符返回一个用于识别其运算数类型的字符串。对于任何变量来说,使用typeof运算符总是以字符串的形式返回以下6种类型之一:

❑/"number/"

❑/"string/"

❑/"boolean/"

❑/"object/"

❑/"function/"

❑/"undefined/"

不幸的是,在使用typeof检测null值时,返回的是“object”,而不是“null”。更好的检测null的方式其实很简单。下面定义一个检测值类型的一般方法:


function type(o){

return(o===null)?/"null/":(typeof o);

}


这样就可以避开因为null值影响基本数据的类型检测。注意:typeof不能够检测复杂的数据类型,以及各种特殊用途的对象,如正则表达式对象、日期对象、数学对象等。

对于对象或数组,可以使用constructor属性,该属性值引用的是原来构造该对象的函数。如果结合typeof运算符和constructor属性,基本能够完成数据类型的检测。表1.2所示列举了不同类型数据的检测结果。

使用constructor属性可以判断绝大部分数据的类型。但是,对于undefined和null特殊值,就不能使用constructor属性,因为使用JavaScript解释器会抛出异常。此时可以先把值转换为布尔值,如果为true,则说明不是undefined和null值,然后再调用constructor属性,例如:


var value=undefined;

alert(typeof value);///"undefined/"

alert(value&&value.constructor);//undefined

var value=null;

alert(typeof value);///"object/"

alert(value&&value.constructor);//null


对于数值直接量,也不能使用constructor属性,需要加上一个小括号,这是因为小括号运算符能够把数值转换为对象,例如:


alert((10).constructor);


使用toString方法检测对象类型是最安全、最准确的。调用toString方法把对象转换为字符串,然后通过检测字符串中是否包含数组所特有的标志字符可以确定对象的类型。toString方法返回的字符串形式如下:


[object class]


其中,object表示对象的通用类型,class表示对象的内部类型,内部类型的名称与该对象的构造函数名对应。例如,Array对象的class为“Array”,Function对象的class为“Function”,Date对象的class为“Date”,内部Math对象的class为“Math”,所有Error对象(包括各种Error子类的实例)的class为“Error”。

客户端JavaScript的对象和由JavaScript实现定义的其他所有对象都具有预定义的特定class值,如“Window”、“Document”和“Form”等。用户自定义对象的class值为“Object”。

class值提供的信息与对象的constructor属性值相似,但是class值是以字符串的形式提供这些信息的,而不是以构造函数的形式提供这些信息的,所以在特定的环境中是非常有用的。如果使用typeof运算符来检测,则所有对象的class值都为“Object”或“Function”,所以此时的class值不能够提供有效信息。

但是,要获取对象的class值的唯一方法是必须调用Object对象定义的默认toString方法,因为不同对象都会预定义自己的toString方法,所以不能直接调用对象的toString方法。例如,下面对象的toString方法返回的就是当前UTC时间字符串,而不是字符串“[object Date]”。


var d=new Date;

alert(d.toString);//当前UTC时间字符串


要调用Object对象定义的默认toString方法,可以先调用Object.prototype.toString对象的默认toString函数,再调用该函数的apply方法在想要检测的对象上执行。结合上面的对象d,具体实现代码如下:


var d=new Date;

var m=Object.prototype.toString;

alert(m.apply(d));///"[object Date]/"


下面是一个比较完整的数据类型安全检测方法。


//安全检测JavaScript基本数据类型和内置对象

//参数:o表示检测的值

/*返回值:返回字符串/"undefined/"、/"number/"、/"boolean/"、/"string/"、/"function/"、/"regexp/"、/"array/"、/"date/"、/"error/"、/"object/"或/"null/"*/

function typeOf(o){

var_toString=Object.prototype.toString;

//获取对象的toString方法引用

//列举基本数据类型和内置对象类型,可以进一步补充该数组的检测数据类型范围

var_type={

/"undefined/":/"undefined/",

/"number/":/"number/",

/"boolean/":/"boolean/",

/"string/":/"string/",

/"[object Function]/":/"function/",

/"[object RegExp]/":/"regexp/",

/"[object Array]/":/"array/",

/"[object Date]/":/"date/",

/"[object Error]/":/"error/"

}

return_type[typeof o]||_type[_toString.call(o)]||(o?/"object/":/"null/");

}


应用示例:


var a=Math.abs;

alert(typeOf(a));///"function/"


上述方法适用于JavaScript基本数据类型和内置对象,而对于自定义对象是无效的。这是因为自定义对象被转换为字符串后,返回的值是没有规律的,并且不同浏览器的返回值也是不同的。因此,要检测非内置对象,只能够使用constructor属性和instaceof运算符来实现。

4.避免误用parseInt

parseInt是一个将字符串转换为整数的函数,与parseFloat(将字符串转换为浮点数)对应,这两种函数是JavaScript提供的两种静态函数,用于把非数字的原始值转换为数字。

在开始转换时,parseInt会先查看位置0处的字符,如果该位置不是有效数字,则将返回NaN,不再深入分析。如果位置0处的字符是数字,则将查看位置1处的字符,并重复前面的测试,依此类推,直到发现非数字字符为止,此时parseInt函数将把前面分析合法的数字字符转换为数值并返回。


parseInt(/"123abc/");//123

parseInt(/"1.73/");//1

parseInt(/".123/");//NaN


浮点数中的点号对于parseInt来说属于非法字符,因此它不会被转换并返回,这样,在使用parseInt时,就存在潜在的误用风险。例如,我们并不希望parseInt(/"16/")与parseInt(/"16 tons/")产生相同的结果。如果该函数能够提醒我们出现额外文本就好了,但它不会那么做。

对于以0为开头的数字字符串,parseInt函数会把它作为八进制数字处理,先把它转换为数值,然后再转换为十进制的数字返回。对于以0x开头的数字字符串,parseInt函数则会把它作为十六进制数字处理,先把它转换为数值,然后再转换为十进制的数字返回。例如:


var d=/"010/";//八进制

var e=/"0x10/";//十六进制

parseInt(d);//8

parseInt(e);//16


如果字符串的第一个字符是0,那么该字符串将基于八进制而不是十进制来求值。在八进制中,8和9不是数字,所以parseInt(/"08/")和parseInt(/"09/")的结果为0,这个错误导致了在程序解析日期和时间时经常会出现问题。幸运的是,parseInt可以接受一个基数作为参数,这样parseInt(/"08/",10)结果为8,parseInt(/"09/",10)结果为9。因此,建议读者在使用parseInt时,一定要提供这个基数参数。

通过在parseInt中提供基数参数,可以把二进制、八进制、十六进制等不同进制的数字字符串转换为整数。例如,下面把十六进制数字字符串/"123abc/"转换为十进制整数。


parseInt(/"123abc/",16);//1194684


再如,把二进制、八进制和十进制数字字符串转换为整数:


parseInt(/"10/",2);//把二进制数字10转换为十进制整数为2

parseInt(/"10/",8);//把八进制数字10转换为十进制整数为8

parseInt(/"10/",10);//把十进制数字10转换为十进制整数为10