写在最前面
这篇英文来自codeproject,原文A Collection of JavaScript Gotchas。我看了以后颇受启发,就把他翻译过来了,呵呵,第一次翻译这么长的技术文章,有什么错误请大家指出哦。另外,译文允许转载,但请在页面明显处标明以下信息:
英文原文:A Collection of JavaScript Gotchas
译文作者:王国峰
译文标题:Javascript中的陷阱大集合【译】
本文主要介绍怪异的Javascript,毋庸置疑,它绝对有怪异的一面。当软件开发者开始使用世界上使用最广泛的语言编写代码时,他们会在这个过程中发现很多有趣的“特性”。即便是老练的Javascript开发者也可以在本文找到一些有趣的新陷阱,请留意这些陷阱,当然也可以尽情享受由这些陷阱带来的“乐趣”!
函数和操作符
双等号
==操作符比较时会进行类型的强制转换,这意味着它可以比较两个不同类型的对象,在执行比较之前它将会尝试把这两个对象转换成同一个类型,举一个例子:
1
|
"1" == 1 //true |
1
|
Number( "1" ) === 1; //true |
由于双等号具有强制类型转换的行为,所以它会打破一般的传递性规则,这点有点吓人,请看下面的列子:
1
2
3
|
"" == 0 //true - 空字符串会被强制转换为数字0. 0 == "0" //true - 数字0会被强制转换成字符串"0" "" == "0" //false - 两操作数都是字符串所以不执行强制转换 |
parseInt不把10作为数字基数
如果你忽略parseInt的第二个参数,那么数字的基数将由下面的规则所决定:
1
2
|
parseInt( "8" ); //8 parseInt( "08" ); //0 |
1
2
|
parseInt( "8" , 10); //8 parseInt( "08" , 10); //8 |
ECMAScript5方面的说明:ECMAScript已不再支持8进制的解析假设,另外,如果忽略parseInt的第二个参数将会引起JSLint的警告。
字符串替换
字符串替换函数仅仅会替换第一个匹配项,并不能替换你所期望的全部匹配项。如下代码:
1
2
|
"bob" .replace( "b" , "x" ); // "xob" "bob" .replace(/b/, "x" ); // "xob" (使用了正则表达式) |
1
2
|
"bob" .replace(/b/g, "x" ); // "xox" "bob" .replace( new RegExp( "b" , "g" ), "x" ); // "xox" (alternate explicit RegExp) |
“+"操作符会执行相加操作和字符串连接操作
php作为另一种弱类型语言,可以使用”.“操作符对字符串进行连接。Javascript却不是这样的 - 所以当操作数是字符串的时候”a+b“通常是执行连接操作。如果你想执行数字相加那你就要引起注意了,因为输入的内容可能是字符串类型的,所以你在执行相加操作前需要先将其转换成数字类型,代码如下:
1
2
|
1 + document.getElementById( "inputElem" ).value; // 连接操作 1 + Number(document.getElementById( "inputElem" ).value); // 相加操作 |
1
|
"3" - "1" ; // 2 |
很多时候我们用数字和空串相加来实现数字转换成字符串的操作,代码如下:
1
|
3 + "" ; // "3" |
typeof
typeof这会返回一个javascript基本类型的实例的类型。Array实际上不是基本类型,所以typeof Array对象将返回Object,代码如下:
1
2
3
|
typeof {} === "object" //true typeof "" === "string" //true typeof [] === "array" ; //false |
当你对自己的对象的实例使用这个操作符时将会得到相同的结果(typeof = "object")。
另外说明一点,”typeof null“将返回”object“,这个有点诡异。
instanceof
instanceof返回指定对象是否是由某个类构造的实例,这个对我们检查指定对象是否是自定义类型之一很有帮助,但是,如果你是用文本语法创建的内置类型那可能会得出错误的结果,代码如下:
1
2
|
"hello" instanceof String; //false new String( "hello" ) instanceof String; //true |
1
2
|
[ "item1" , "item2" ] instanceof Array; //true new Array( "item1" , "item2" ) instanceof Array; //true |
唉,不爽!总的来说,如果你想测试Boolean, String, Number, 或者Function的类型,你可以使用typeof,对于其他的任何类型,你可以使用instanceof测试。
哦,还有一点,在一个function中,有一个预定义变量叫“arguments”,它以一个array的形式传递给function。然而,它并不是真正的array,它只是一个类似array的对象,带有长度属性并且属性值从0-length。非常奇怪...你可以用下面的小伎俩将它转换成真正的数组:
1
|
var args = Array.prototype.slice.call(arguments, 0); |
这个对由getElementsByTagName返回的NodeList对象也是一样的 - 它们都可以用以上的代码转换成合适的数组。
eval
eval可以将字符串以javascript代码的形式来解析执行,但是一般来说我们不建议这么做。因为eval非常慢 - 当javascript被加载到浏览器中时,它会被编译成本地代码;然而执行的过程中每次遇到eval表达式,编译引擎都将重新启动执行编译,这样做的代价太大了。而且这样做也丑陋无比,有很多eval被滥用的例子。另外,在eval中的代码会在当前范围内执行,因此它可以修改局部变量,以及在你的范围内添加一些让你意想不到的东西。
JSON转换是我们经常要做的;通常我们使用“var obj = eval(jsonText);”来进行转换。然而现在几乎所有的浏览器都支持本地JSON对象,你可以使用“var obj = JSON.parse(jsonText);”来替代前面的代码。相反你也可以用“JSON.stringify”将JSON对象转换成字符串。更妙的是,你可以使用“jQuery.parseJSON”来完成上述的工作。
setTimeout和setInterval函数的第一个参数可以用字符串作为函数体来解析执行,当然,我们也不建议这样做,我们可以用实际的函数来替代。
最后,Function的构造函数和eval非常像,唯一不同的是,Function构造函数是在全局范围内执行的。
with
with表达式将为你提供访问对象属性的速记方式,但我们是否应该使用它,仍然存在矛盾的观点。Douglas Crockford不太喜欢它。John Resig在他的书中有找了很多with的巧妙用法,但是他也承认这将会影响性能并且会产生一点混乱。来看看我们分离出来的with代码块,他不能准确地告诉我们现在正在执行什么,代码如下所示:
1
2
3
4
|
with (obj) { bob = "mmm" ; eric = 123; } |
1
2
|
obj.bob = "mmm" ; obj.eric = 123; |
类型和构造函数
使用“new”关键字构造内置类型
Javascript中有Object, Array, Boolean, Number, String, 和Function这些类型,他们各自都有各自的文字语法,所以就不需要显式构造函数了。
显式构造(不建议) | 文字语法(推荐) |
---|---|
var a = new Object(); a.greet = "hello"; |
var a = { greet: "hello" }; |
var b = new Boolean(true); |
var b = true; |
var c = new Array("one", "two"); |
var c = ["one", "two"]; |
var d = new String("hello"); |
var d = "hello" |
var e = new Function("greeting", "alert(greeting);"); |
var e = function(greeting) { alert(greeting); }; |
然而,如果你使用new关键字来构造上面其中的一种类型,你实际上将会得到一个类型为Object并且继承自你要构造的类型的原型的对象(Function类型除外)。所以尽管你用new关键字构造了一个Number类型,它也将是一个Object类型,如下代码:
1
2
3
|
typeof new Number(123); // "object" typeof Number(123); // "number" typeof 123; // "number" |
使用“new”关键字来构造任何东西
如果你自写构造函数并且忘记了new关键字,那么悲剧就发生了:
1
2
3
4
5
6
7
8
9
10
|
var Car = function (colour) { this .colour = colour; }; var aCar = new Car( "blue" ); console.log(aCar.colour); // "blue" var bCar = Car( "blue" ); console.log(bCar.colour); // error console.log(window.colour); //"blue" |
偶然忘记使用new关键字意味着很多可选择的对象构造模式已经出现可以完全删除使用这个关键字的需求的情况,尽管这超出了本文的范围,但我还是建议你去进一步阅读。
没有Integer类型
数值计算是相对缓慢的,因为没有Integer类型。只有Number类型 - Number是IEEE标准中双精度浮点运算(64位)类型。这就意味着Number会引起下面的精度舍入错误:
1
|
0.1 + 0.2 === 0.3 //false |
1
|
0.0 === 0; //true |
1
2
|
a === b; //true 1/a === 1/b; //false |
1
2
3
4
|
var a = 0 * 1; // 这个结果为0 var b = 0 * -1; // 这个结果为-0 (你也可以直接"b=-0",但是你为何要这样做?) a === b; //true: 0等于-0 1/a === 1/b; //false: 正无穷大不等于负无穷大 |
作用域
没有块作用域
因为你可能已经注意到上一个观点,javascript中没有块作用域的概念,只有函数作用域。可以试试下面的代码:
1
2
3
4
5
|
for ( var i=0; i<10; i++) { console.log(i); } var i; console.log(i); // 10 |
我们有可能通过写一个立即执行的function来创建一个作用域:
1
2
3
4
5
6
7
|
( function (){ for ( var i=0; i<10; i++) { console.log(i); } }()); var i; console.log(i); // undefined |
1
2
3
4
5
|
var x = 3; ( function (){ console.log(x + 2); // 5 x = 0; //No var declaration }()); |
1
2
3
4
5
|
var x = 3; ( function (){ console.log(x + 2); //NaN - x is not defined var x = 0; //var declaration }()); |
1
2
3
4
5
6
|
var x = 3; ( function (){ var x; console.log(x + 2); //NaN - x is not defined x = 0; }()); |
这个实在是太有意义了!
全局变量
Javascript有一个全局作用域,在为你的代码创建命名空间时一定要小心谨慎。全局变量会给你的应用增加一些性能问题,因为当你访问它们时,运行时不得不通过每一个作用域来建立知道找到它们为止。他们会因你的有意或者无意而被访问或者修改,这将导致另外一个更加严重的问题 - 跨站点脚本攻击。如果一个不怀好意的家伙在你的页面上找出了如何执行那些代码的方法,那么他们就可以通过修改全局变量非常容易地扰乱你的应用。缺乏经验的开发者在无意中会不断的将变量添加到全局作用域中,通过本文,将会告诉大家这样会发生什么意外的事情。
我曾经看到过下面的代码,它将尝试声明两个值相等的局部变量:
1
|
var a = b = 3; |
这样非常正确的得到了a=3和b=3,但是a在局部作用域中而b在全局作用域中,”b=3“将会被先执行,全局操作的结果,3,再被分配给局部变量a。
下面的代码声明了两个值为3的变量,这样能达到预期的效果:
1
2
|
var a = 3, b = a; |
”this“和内部函数
”this“关键字通常指当前正在执行的函数所在的对象,然而,如果函数并没有在对象上被调用,比如在内部函数中,”this“就被设置为全局对象(window),如下代码:
1
2
3
4
5
6
7
8
9
10
11
|
var obj = { doSomething: function () { var a = "bob" ; console.log( this ); // 当前执行的对象 ( function () { console.log( this ); // window - "this" is reset console.log(a); // "bob" - still in scope }()); } }; obj.doSomething(); |
杂项
数据不存在:”null“和”undefined“
有两种对象状态来表明数据不存在:null和undefined。这会让那些从其他编程语言比如C#转过来的程序员变得相当混乱。也许你会期望下面的代码返回true:
1
2
3
|
var a; a === null ; //false a === undefined; //true |
”a“实际上是undefined的(尽管你用双等号==来与null比较会得出true的结果,但这只是表面上看起来正确的另一个错误)。
如果你想检查一个变量是否真的存在值,那你不能用双等号==去判断,要用下面的方法:
1
2
3
|
if (a !== null && a !== undefined) { ... } |
1
2
3
|
if (a) { ... } |
重定义undefined
非常正确,你可以重定义undefined,因为它不是一个保留字:
1
|
undefined = "surprise!" ; |
1
|
undefined = void 0; |
1
2
3
|
( function ( window, undefined ) { ... // jQuery library! }(window)); |
顺便说一下,你不能重定义null - 但是你可以重定义NaN,Infinity和带构造函数的内置类型。可以这样尝试一下:
1
2
|
Array = function (){ alert( "hello!" ); } var a = new Array(); |
可选的分号
Javascript代码中分号是可选的,所以初学者写代码就简单多了。但是很不幸的是如果忽略了分号并不会给任何人带来方便。结果是当解释器遇到错误时,必须追溯并尝试去猜测因为哪些分号漏写导致的问题。
这里有一个经典的例子:
1
2
3
4
|
return { a: "hello" }; |
1
2
3
|
return { a: "hello" }; |
NaN的类型是...Number
1
|
typeof NaN === "number" //true |
1
|
NaN === NaN; // false |
从另一个方面可以说明,我们也可以用函数isFinite,当其中一个操作数为NaN或者InFinity时返回false。
arguments对象
在一个函数中,我们可以引用arguments对象来遍历传入的参数列表,第一个比较怪异的地方是这个对象并不是Array,而是一个类似Array的对象(有一个length属性,其值在0-length-1之间)。为了将其转换成array,我们可以array的splice函数来创建其对应的array数组:
1
2
3
4
5
|
( function (){ console.log(arguments instanceof Array); // false var argsArray = Array.prototype.slice.call(arguments); console.log(argsArray instanceof Array); // true }()); |
1
2
3
4
5
|
( function (a){ alert(arguments[0]); //1 a = 2; alert(arguments[0]); //2 }(1)); |
这样我就总结完了这些javascript陷阱。我肯定还会有更多这样的陷阱,期待大家更多的意见和点评。
PS - 我真的很喜欢Javascript。
参考资料