本文深入探讨JavaScript中parseInt函数在处理带前导0的字符串时返回0的常见陷阱。通过引用《JavaScript高级程序设计》中关于基模式和重定义方法的知识,提出一种基于参数个数动态选择处理逻辑的改造方案。该方案在单参数时默认按十进制解析,双参数时保留原有进制转换功能,从而兼顾易用性与兼容性,并提供了精简后的最终代码实现。
引言与问题背景
回想起来,当初得知这个问题背后的原理时,曾为自己掌握了这一经验而感到欣喜。
然而,随后又被同一问题反复折磨,最终不得不痛下决心寻求解决方案。
首先,我要感谢那些在明知此问题的情况下,依然反复犯下类似错误的人。
正是他们不断的犯错,促使我认真思考并寻找这个问题的解决之道。
其次,必须感谢《JavaScript高级程序设计》的作者及译者。
正是书中提供的启示,让我找到了解决该问题的思路,否则我将不得不反复强调使用parseInt时的注意事项。
同时,我希望这里留下的不仅仅是一个解决方案。
更重要的是解决问题的思路、想法以及举一反三的经验总结。
parseInt的进制转换陷阱
问题背景:
过去发生的问题令人痛苦,不愿再回忆。
这次遇到的问题相对简单:在比较两个月份时,由于月份是从字符串中提取的,因此使用了parseInt进行转换。
然而,parseInt("08")的结果竟然是0。
具体原因可参考《JavaScript高级程序设计》第19至20页对parseInt函数的详细讲解。
parseInt()方法支持基模式,可以将二进制、八进制、十六进制或其他进制的字符串转换为整数。
基数由parseInt()方法的第二个参数指定,因此解析十六进制值需如下调用parseInt()方法:
代码如下:
var iNum1 = parseInt("AF", 16); //返回 175当然,对二进制、八进制甚至十进制(默认模式),都可以这样调用parseInt()方法:
代码如下:
var iNum1 = parseInt("10", 2); //返回 2
var iNum2 = parseInt("10", 8); //返回 8
var iNum3 = parseInt("10", 10); //返回 10如果十进制数包含前导0,最好采用基数10,以免意外得到八进制的值。例如:
var iNum1 = parseInt("010"); //返回 8
var iNum2 = parseInt("010", 8); //返回 8
var iNum3 = parseInt("010", 10); //返回 10
在这段代码中,两行代码都将字符"010"解析为一个数字。
第一行代码将该字符串视为八进制值,其解析方式与第二行代码(声明基数为8)相同。最后一行代码声明基数为10,因此iNum3最终等于10。
初步尝试:重定义全局方法
初步尝试:
以往的解决方法是让大家放弃parseInt函数,全部改用parseFloat。
但人性使然,"孰能无忘"?
最好的办法莫过于保留parseInt的"形",废除其"神"。
于是我想到了《JavaScript高级程序设计》第87至88页关于"重定义已有方法"的说明。
3.6.2 重定义已有方法
就像能给已有的类定义新方法一样,也可重定义已有的方法。
如前一章所述,函数名只是指向函数的指针,因此可以轻易地使它指向其他函数。如果修改了本地方法,如toString(),会出现什么情况呢?
Function.prototype.toString = function () {
return "Function code hidden";
}
前面代码完全合法,运行结果完全符合预期:
function sayHi () {
alert("你好!");
}
alert(sayHi.toString()); //输出"Function code hidden"
也许你还记得,第2章中介绍过Function的toString()方法通常输出的是函数的源代码。
覆盖该方法,可以返回另一个字符串(在这个例子中,返回"Function code hidden")。
不过,toString()指向的原始函数怎样了呢?它将被无用存储单元回收程序回收,因为它被完全废弃了。
没能够恢复原始函数的办法,所以在覆盖原始方法前,存储它的指针比较安全,以便以后的使用。
你甚至可能在某种情况下在新方法中调用原始方法:
Function.prototype.originalToString = Function.prtotype.toString;
Function.prototype.toString = function () {
if(this.originalToString().length >100) {
return "Function too leng to display."
} else {
return this.originalToString();
}
在这段代码中,第一行代码把对当前toString()方法的引
用保存在属性originalTo- String中。然后用定制的方法覆盖了toString()方法。
新方法将检查该函数源代码的长度是否大于100。
如果是,就返回错误消息,说明该函数代码太长,否则调用originalToString()方法,返回函数的源代码。
根据这个例子,只要照葫芦画瓢写一行
Global.prototype.parseInt = Global.prototype.parseFloat;
那么parseInt函数真的就变成徒有其表,肚子里面干的却是parseFloat勾当的函数了。
但是,同时一个致命的问题点也摆在眼前。
那就是JavaScript中的Global对象,就跟神一样,只是个概念。
说明请参见下面《JavaScript高级程序设计》第70页关于"内置对象"的说明。
Global对象是ECMAScript中最特别的对象,因为实际上它根本不存在。
如果尝试编写下面的代码,将得到错误:
var pointer = Global;
错误消息显示Global不是对象,但刚才不是说Global是对象吗?
没错。这里需要理解的主要概念是,在ECMAScript中,不存在独立的函数,所有函数都必须是某个对象的方法。
本书前面介绍的函数,如isNaN()、isFinite()、parseInt()和parseFloat()等,看起来都像独立的函数。
实际上,它们都是Global对象的方法。
于是,上网大查怎样获取Global对象,或怎么使用Global.prototype来改变parseInt函数。
结果可想而知,神就是神,就连著名的"搜神网"Google也查不出来。
欲放弃之时,果然应了那句"死地而后生"。突然想到parseInt就像个全局函数一样,根本不用什么对象调用。
那是不是说,只要把上面那句改成parseInt = parseFloat;就可以了?
果然,神,无处不在!!!好用了!!!
深度考究:基于参数个数的动态处理
深度考究:
问题基本上解决了。只有一点需要注意的,就是JavaScript加载出错的时候,后面的语句就不加载执行了。
所以这句一定要放在第一句执行。现在正好建一个JavaScript通用方法库,要求以后每个页面必须引入该库文件。
所以这一句放在该JavaScript通用方法库的第一行,从此便可高枕无忧。
但是当我在为该段代码写注释,特别是列举如何应用时,发现如下代码的问题
alert(parseInt("010", 2)); //10
alert(parseInt("010", 8)); //10
alert(parseInt("010", 10)); //10
每一个处理的返回值都是10,也就是说可以处理二进制,八进制,十六进制的parseInt从此消失了。
如果说单个参数的parseInt惹出了不少麻烦,我们对于没有惹祸的两个参数的parseInt还是希望保留其特异功能的。
于是需要进一步的改进。
那么就要根据使用parseInt函数时,传递参数的个数来进行判断处理。
如果只有一个参数,那么就调用parseFloat返回结果。
如果有两个以上的参数,那么就调用parseInt两个参数的处理,返回结果。
这里判断参数个数用到arguments对象,参见《JavaScript高级程序设计》第53至54页关于arguments对象的说明。
在函数代码中,使用特殊对象arguments,开发者无需明确指出参数名,就能访问它们。
例如,在函数sayHi()中,第一个参数是message。
用arguments[0]也可以访问这个值,即第一个参数的值(第一个参数位于位置0,第二个参数位于位置1,依此类推)。
因此,无需明确命名参数,就可以重写函数:
function sayHi() {
if (arguments[0] == "bye") {
return;
}
alert(arguments[0]);
}
于是就有了如下代码:
originalparseInt = parseInt;
parseInt = function (){
if(arguments.length == 1){
return parseFloat(arguments[0]);
} else {
return originalparseInt(arguments[0], arguments[1]);
}
这段代码里我们改造了parseInt,让其通过参数个数的不同进行不同的处理。
用一个新的变量originalparseInt保留了parseInt的原型。
这样我们即便改造了parseInt,依然能通过保留的原型变量originalparseInt使用parseInt的原始功能。
最终方案与代码优化
返璞归真:
代码写到这本以为一切都OK了,却被人说彻底抹杀了parseInt函数对2进制,8进制的处理。
想想也是。处理的过于极端,只想着用parseFloat彻底替换掉讨厌的parseInt函数。
如果我们正的用到2进制或8进制的数字转换,还得用我们费劲保留的parseInt函数的原型新变量originalparseInt。
其实我们的愿望很简单。
parseInt函数中只有一个参数的时候就想让它简单的处理10进制的转换,别再因为首位的0来产生一些头疼的bug。
当我们用到第二个参数,想让它处理2进制,8进制的时候,我还依然能用parseInt既存的功能。
于是有了下面最终的代码:
考虑到js文件体积的问题,尽量减少代码量。于是把originalparseInt换成了$parseInt。
另外把超级长的内置对象名arguments直接换成了一个字母a这样该对象用了4次节省的代码量就非常可观了。
举一反三:其他函数的类似改造
举一反三:
对parseInt函数的再造就完成了。
那么其实我们可以根据这次改造的经验,改造与parseInt具有类似的烦人特性的JavaScript方法。
譬如,escape,unescape这种已经被W3C组织不推荐使用的方法就可以用被推荐的方法替换掉
escape = encodeURI;
unescape = decodeURI;
那么基于这次的经验,今后遇到类似的问题就可以考虑到用这种乾坤大挪移的方法去解决了。