如果你是一个Web开发者,应该也写过这种接受用户输入的JavaScript代码:
表面上看,这行代码不存在任何问题,但实际运行一段时间后,却发现它偶尔莫名其妙地罢工。
这行代码的问题是,用户不一定输入标准的年龄整数,他可能输入“23.5”,也可能输入“二十三”。改正的办法是加入一些转换过程,例如:
JavaScript是一种脚本语言,它的设计目标之一就是尽可能地简化使用,所以它处理字符串和数值的方式极其灵活,许多操作都是隐含执行、不需要开发者干预。那么,它到底灵活到哪种程度?高度灵活和高度自动化的底下是否隐藏着陷阱?是否可以定义拥有3.5个元素的数组?1加上1000000是否一定等于1000001,而不是等于1.000E6?如何保证这个数值正确显示?
我们知道以前的许多浏览器存在数值处理方面的BUG,现在是否可以高枕无忧,或者仍然必须小心翼翼?本文为你介绍脚本引擎的数值处理限制,主要针对目前流行的浏览器,包括5.x以上的IE、4.7x以上的Netscape、1.x的Mozilla。读完本文,你将知道在浏览器中执行哪些数值操作是安全的,哪些操作是不安全的。
一种解释:科学就是描述现实世界
从表面上看,数值是一个很容易理解的概念。最常用的数值类型是整数和实数,想必你一定已经熟悉它们了。简单的例子如2和23.45;但也有复杂的例子,如互联网最大梅森素数搜索的结果已超过4百万位数字。我们知道PI(圆周率)是一个无限不循环小数,如何写出比3.141592654精度更高的PI值?一言蔽之,你根本办不到。
无论是用纸张记录数值,还是用计算机记录数值,都必须用某种编码方案来表达数值。必须理解的是,用编码表达的数值不是数值本身,而只是数值的一种人类或计算机可理解的描述。任何编码方案都有其局限,要么是表达范围(精度)方面的限制,要么是其他复杂性方面的制约。例如,“2”究竟是一个整数还是实数?或者两者都是?
从数学课上,我们知道循环的数值可以用一个句点表示,例如我们不必写1.9999……(后面加上无数个9),只要写1.9再在9上面加一个点。另外我们知道,“1点9的循环”实际上等于2,因为1+(0.90.09+0.009+…)收敛的极限就是2。也就是说,同一个数值“2”有两种表达方法。如果算上2.0000,那就有三种表达方法。所以说,并非每一个数值都只有唯一的表达方式,即使在纸上写也一样!这样一来,处理数值就很不方便、甚至难以令人接受了!
二套标准:化繁为简的折衷方案
科学理论固然美妙,不过现实生活中不可能(也没有必要)严格坚持科学理论的精密。怎么办呢?订立工程标准。例如,IEEE 754标准《二进制浮点数算法》(www.ieee.org)就是一个对实数进行计算机编码的标准。
JavaScript的数值处理就建立在这类标准之上。按照ECMA的JavaScript标准,它的Number类型就是IEEE 754的双精度数值。绝对完美的数值编码方案是不存在的,为了处理方便,这个标准引入了大量的折衷和妥协,建立在这种表达方式上的算法(例如除法运算)也一样。由于数值表达方式存在“缺陷”,运算结果不可避免地堆聚起越来越多的误差——大多数情况下,这类误差不至于引起问题,但这并不意味着你可以完全忽视,请看下面两个例子:
⑴ IEEE 754除法错误曾经使Intel的Pentium付出昂贵的代价,后来Intel不得不为发布出去的芯片BUG公开道歉,累计损失约5亿美元。
⑵ 日常的利息计算不可避免地涉及浮点运算,没有人会愿意因为计算问题而多付利息或减少收入,因此数值标准必须足够精确。
按IEEE 754格式保存的浮点数精度相当于带有15、16或17位小数位数的十进制小数,由于存在二进制和十进制的转换问题,具体的位数会发生变化。要获得最高的转换精度,必须指定17位的小数——此时可以相信前15位的精度。
你可以做个试验,在JavaScript中输出下面这些数值(注意不能作为字符串输出):0.1000000000000000000000000001(28位小数)、0.100000000000000000000000001(27位小数)、0.1000000000000000000000000456(28位小数)、0.09999999999999999999999(23位小数),显示出来的结果都是数值0.1。又如,如果输出1/3的有理数表达式,结果是0.3333333333333333。
三种格式:JavaScript的Number
实际使用中JavaScript数值还有第二种标准,它是许多计算机语言都用到的常见格式,即32位的整数,相当于C语言中的int类型。它由4个8 bit的字节构成,可以保存较小的整数。可能你已经在无意之中用过这种格式,但JavaScript不允许显式声明int,它是一种隐含的类型。
因此,JavaScript数值有三种保存方式:字符串形式的数值内容,IEEE双精度浮点数,或者是整数。
为什么JavaScript要提供一种类似int的隐含数值类型?原因有三:第一,便于执行位操作,如or(|)和移位(>>)等;二,用于数组处理;三,出于精确方面的考虑。
考虑一下数组的情况。ECMA的JavaScript标准允许每个数组有4294967295个数组元素——32 bit能保存的最大值减1,剩下的一个用来保存长度数据。当我们指定数组的索引时,JavaScript解释器判断该变量是否为一个32 bit的整数。例如,假设有一个数组var,一个变量item,如果item=2,则arr[item] = 5相当于arr[2] = 5;但是,如果索引的值看起来象是一个浮点数(或者不象数值),如item=2.0,则第三个语句相当于:arr["2.0"] = 5;。
这种表达方式很容易引起误解,似乎JavaScript支持负数和小数形式的索引值,如arr[-6]和arr[2.35]。实际上,这类特殊的索引值会被自动转换成字符串。
再举一个例子。假设索引变量slot1的初值等于1.1,arr[slot1]=10,现在我们让also11 = Math.pow(Math.pow(1.1,1/20),20),从理论上看,slot1应该仍是1.1,其实不然,浮点运算的误差将使slot1变成1.100019,如果我们再访问arr[alot1],这时的arr[slot1]就不再是原来的arr[alot1]了。
可以看到,一涉及浮点数,事情就要复杂多了。不过你也不必太担心,JavaScript会优先考虑整数。也就是说,当JavaScript遇到一个数值时,它会首先尝试按照整数来处理该数值,如行得通,则把数值保存为31 bit的整数;如果该数值不能视为整数,或超出31 bit的范围,则把数值保存为64位的IEEE 754浮点数。
在浏览器环境中,数值数据可能来自多种数据源,包括XML、XHTML、DOM以及CSS标准等都具备向JavaScript提供数据的能力。不管数据来自何处,这些数值要么被视为String,要么被视为Number,如果是后则,内部保存格式可能是整数。
四个数值:必须密切注意的临界点
聪明的读者一定想到了这样一个问题:什么时候规规矩矩的整数会突然变成捉摸不定的双精度浮点数?答案是:当它们的值变得非常庞大时,或者进入1和0之间时。所以说,1和0是首先必须注意的二个数值。
接下来,最大的Unicode值是1114111(7位数字,相当于/x41777777),而最大的RGB颜色值是16777215(8位数字,相当于#FFFFFF)。最大的32 bit带符号整数是2147483647(10位数字),-2147483648最小,所以JavaScript内部会以整数的形式保存所有Unicode值和RGB颜色。2147483647是第三个必须注意的数值,任何大于该值的数据将保存为双精度格式。
9007199254740992(16位数字)是最大的浮点数,输出时类似整数,所有Date对象(按毫秒计算)都小于该值,因此总是模拟整数的格式输出。它是第四个必须注意的数值。
最后,最大的双精度数值是1.7976931348623157e+308,超出这个范围就要算作无穷大了。
五条原则:远离误差和烦恼
■ 大多数Web页面不需要小数
避免使用小数,尽量设法使用整数。确保数组的索引都是整数。按分(而不是元)计算金额。百分比放大100倍计算以避免出现小数。尽可能不用除法(/)和模(%)运算,因为大多数情况下它们直接导致出现浮点数。如果必须使用除法,立即用Math.round方法回归整数运算。
■ 如果必须使用浮点数,则尽可能引入冗余小数位——即在程序要求的运算精度之外,再增加小数位
如果程序需要5位数字的小数精度,则在运算中至少保留6位的小数,8位更好。冗余位越多,累计误差的影响越小。
■ 避免在同一个表达式中使用相差太大或太小的数值
对两个非常接近的数值执行减法或比较操作很容易出错。将很小的数值和很大数值相加无异于浪费时间,小的数值很可能被当作0。不过,很小的数值乘以很大的数值一般不会出现问题,例如2 * 12345678会得到正确的结果24691356。但是,0.1 - 0.09的结果是0.010000000000000009。
■ 用isFinite()和isNaN()检查运算结果
通过表单提交任何数值运算结果之前,一定要先检查数据的合法性。
■ 慎用数值运算
程序涉及的数值运算越少,引入误差的可能就越小。视浮点数为贵客,不可任意驱使。
<input type="text" name="age" οnchange="return (this.value>0)"> |
表面上看,这行代码不存在任何问题,但实际运行一段时间后,却发现它偶尔莫名其妙地罢工。
这行代码的问题是,用户不一定输入标准的年龄整数,他可能输入“23.5”,也可能输入“二十三”。改正的办法是加入一些转换过程,例如:
<input type="text" name="age" οnchange="return parseFloat( this.value)"> |
JavaScript是一种脚本语言,它的设计目标之一就是尽可能地简化使用,所以它处理字符串和数值的方式极其灵活,许多操作都是隐含执行、不需要开发者干预。那么,它到底灵活到哪种程度?高度灵活和高度自动化的底下是否隐藏着陷阱?是否可以定义拥有3.5个元素的数组?1加上1000000是否一定等于1000001,而不是等于1.000E6?如何保证这个数值正确显示?
我们知道以前的许多浏览器存在数值处理方面的BUG,现在是否可以高枕无忧,或者仍然必须小心翼翼?本文为你介绍脚本引擎的数值处理限制,主要针对目前流行的浏览器,包括5.x以上的IE、4.7x以上的Netscape、1.x的Mozilla。读完本文,你将知道在浏览器中执行哪些数值操作是安全的,哪些操作是不安全的。
一种解释:科学就是描述现实世界
从表面上看,数值是一个很容易理解的概念。最常用的数值类型是整数和实数,想必你一定已经熟悉它们了。简单的例子如2和23.45;但也有复杂的例子,如互联网最大梅森素数搜索的结果已超过4百万位数字。我们知道PI(圆周率)是一个无限不循环小数,如何写出比3.141592654精度更高的PI值?一言蔽之,你根本办不到。
无论是用纸张记录数值,还是用计算机记录数值,都必须用某种编码方案来表达数值。必须理解的是,用编码表达的数值不是数值本身,而只是数值的一种人类或计算机可理解的描述。任何编码方案都有其局限,要么是表达范围(精度)方面的限制,要么是其他复杂性方面的制约。例如,“2”究竟是一个整数还是实数?或者两者都是?
从数学课上,我们知道循环的数值可以用一个句点表示,例如我们不必写1.9999……(后面加上无数个9),只要写1.9再在9上面加一个点。另外我们知道,“1点9的循环”实际上等于2,因为1+(0.90.09+0.009+…)收敛的极限就是2。也就是说,同一个数值“2”有两种表达方法。如果算上2.0000,那就有三种表达方法。所以说,并非每一个数值都只有唯一的表达方式,即使在纸上写也一样!这样一来,处理数值就很不方便、甚至难以令人接受了!
二套标准:化繁为简的折衷方案
科学理论固然美妙,不过现实生活中不可能(也没有必要)严格坚持科学理论的精密。怎么办呢?订立工程标准。例如,IEEE 754标准《二进制浮点数算法》(www.ieee.org)就是一个对实数进行计算机编码的标准。
JavaScript的数值处理就建立在这类标准之上。按照ECMA的JavaScript标准,它的Number类型就是IEEE 754的双精度数值。绝对完美的数值编码方案是不存在的,为了处理方便,这个标准引入了大量的折衷和妥协,建立在这种表达方式上的算法(例如除法运算)也一样。由于数值表达方式存在“缺陷”,运算结果不可避免地堆聚起越来越多的误差——大多数情况下,这类误差不至于引起问题,但这并不意味着你可以完全忽视,请看下面两个例子:
⑴ IEEE 754除法错误曾经使Intel的Pentium付出昂贵的代价,后来Intel不得不为发布出去的芯片BUG公开道歉,累计损失约5亿美元。
⑵ 日常的利息计算不可避免地涉及浮点运算,没有人会愿意因为计算问题而多付利息或减少收入,因此数值标准必须足够精确。
按IEEE 754格式保存的浮点数精度相当于带有15、16或17位小数位数的十进制小数,由于存在二进制和十进制的转换问题,具体的位数会发生变化。要获得最高的转换精度,必须指定17位的小数——此时可以相信前15位的精度。
你可以做个试验,在JavaScript中输出下面这些数值(注意不能作为字符串输出):0.1000000000000000000000000001(28位小数)、0.100000000000000000000000001(27位小数)、0.1000000000000000000000000456(28位小数)、0.09999999999999999999999(23位小数),显示出来的结果都是数值0.1。又如,如果输出1/3的有理数表达式,结果是0.3333333333333333。
三种格式:JavaScript的Number
实际使用中JavaScript数值还有第二种标准,它是许多计算机语言都用到的常见格式,即32位的整数,相当于C语言中的int类型。它由4个8 bit的字节构成,可以保存较小的整数。可能你已经在无意之中用过这种格式,但JavaScript不允许显式声明int,它是一种隐含的类型。
因此,JavaScript数值有三种保存方式:字符串形式的数值内容,IEEE双精度浮点数,或者是整数。
为什么JavaScript要提供一种类似int的隐含数值类型?原因有三:第一,便于执行位操作,如or(|)和移位(>>)等;二,用于数组处理;三,出于精确方面的考虑。
考虑一下数组的情况。ECMA的JavaScript标准允许每个数组有4294967295个数组元素——32 bit能保存的最大值减1,剩下的一个用来保存长度数据。当我们指定数组的索引时,JavaScript解释器判断该变量是否为一个32 bit的整数。例如,假设有一个数组var,一个变量item,如果item=2,则arr[item] = 5相当于arr[2] = 5;但是,如果索引的值看起来象是一个浮点数(或者不象数值),如item=2.0,则第三个语句相当于:arr["2.0"] = 5;。
这种表达方式很容易引起误解,似乎JavaScript支持负数和小数形式的索引值,如arr[-6]和arr[2.35]。实际上,这类特殊的索引值会被自动转换成字符串。
再举一个例子。假设索引变量slot1的初值等于1.1,arr[slot1]=10,现在我们让also11 = Math.pow(Math.pow(1.1,1/20),20),从理论上看,slot1应该仍是1.1,其实不然,浮点运算的误差将使slot1变成1.100019,如果我们再访问arr[alot1],这时的arr[slot1]就不再是原来的arr[alot1]了。
可以看到,一涉及浮点数,事情就要复杂多了。不过你也不必太担心,JavaScript会优先考虑整数。也就是说,当JavaScript遇到一个数值时,它会首先尝试按照整数来处理该数值,如行得通,则把数值保存为31 bit的整数;如果该数值不能视为整数,或超出31 bit的范围,则把数值保存为64位的IEEE 754浮点数。
在浏览器环境中,数值数据可能来自多种数据源,包括XML、XHTML、DOM以及CSS标准等都具备向JavaScript提供数据的能力。不管数据来自何处,这些数值要么被视为String,要么被视为Number,如果是后则,内部保存格式可能是整数。
四个数值:必须密切注意的临界点
聪明的读者一定想到了这样一个问题:什么时候规规矩矩的整数会突然变成捉摸不定的双精度浮点数?答案是:当它们的值变得非常庞大时,或者进入1和0之间时。所以说,1和0是首先必须注意的二个数值。
接下来,最大的Unicode值是1114111(7位数字,相当于/x41777777),而最大的RGB颜色值是16777215(8位数字,相当于#FFFFFF)。最大的32 bit带符号整数是2147483647(10位数字),-2147483648最小,所以JavaScript内部会以整数的形式保存所有Unicode值和RGB颜色。2147483647是第三个必须注意的数值,任何大于该值的数据将保存为双精度格式。
9007199254740992(16位数字)是最大的浮点数,输出时类似整数,所有Date对象(按毫秒计算)都小于该值,因此总是模拟整数的格式输出。它是第四个必须注意的数值。
最后,最大的双精度数值是1.7976931348623157e+308,超出这个范围就要算作无穷大了。
五条原则:远离误差和烦恼
■ 大多数Web页面不需要小数
避免使用小数,尽量设法使用整数。确保数组的索引都是整数。按分(而不是元)计算金额。百分比放大100倍计算以避免出现小数。尽可能不用除法(/)和模(%)运算,因为大多数情况下它们直接导致出现浮点数。如果必须使用除法,立即用Math.round方法回归整数运算。
■ 如果必须使用浮点数,则尽可能引入冗余小数位——即在程序要求的运算精度之外,再增加小数位
如果程序需要5位数字的小数精度,则在运算中至少保留6位的小数,8位更好。冗余位越多,累计误差的影响越小。
■ 避免在同一个表达式中使用相差太大或太小的数值
对两个非常接近的数值执行减法或比较操作很容易出错。将很小的数值和很大数值相加无异于浪费时间,小的数值很可能被当作0。不过,很小的数值乘以很大的数值一般不会出现问题,例如2 * 12345678会得到正确的结果24691356。但是,0.1 - 0.09的结果是0.010000000000000009。
■ 用isFinite()和isNaN()检查运算结果
通过表单提交任何数值运算结果之前,一定要先检查数据的合法性。
■ 慎用数值运算
程序涉及的数值运算越少,引入误差的可能就越小。视浮点数为贵客,不可任意驱使。