65.9K
CodeProject 正在变化。 阅读更多。
Home

JavaScript 陷阱集合

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (163投票s)

2011年4月15日

CPOL

14分钟阅读

viewsIcon

452945

JavaScript 中不寻常和意外特性的集合

引言

本文旨在介绍 JavaScript 中一些奇特的地方,它确实有奇特的一面!那些通常用其他语言编写代码的软件开发者,在开始使用世界上最广泛使用的语言编写代码时,会发现许多令人着迷的“特性”。希望即使是经验丰富的 JavaScript 开发者也能在本篇文章中找到一些新的需要注意的陷阱。祝您阅读愉快!

目录

函数和运算符

双等号

== 运算符会进行类型强制转换的比较。这意味着您可以比较两种不同类型的对象,它会尝试在进行比较之前将它们转换为同一类型。例如:

"1" == 1 //true

然而,这通常会产生误导,而且从不需要。在上面的例子中,您可以将 String 转换为 Number 并使用区分类型的三个等号

Number("1") === 1; //true

或者,更好的做法是,首先确保您的操作数类型正确。

由于其类型强制转换的行为,双等号经常会破坏传递性规则,这有点令人担忧。

"" == 0 //true - empty string is coerced to Number 0.
0 == "0" //true - Number 0 is coerced to String "0"
"" == "0" //false - operands are both String so no coercion is done.

当使用三个等号时,上述所有比较都将产生 false

parseInt 不假定为十进制

如果您在调用 parseInt 时省略第二个参数,那么基数将根据以下规则确定:

  • 默认情况下,假定基数为 10。
  • 如果数字以 0x 开头,则假定基数为 16。
  • 如果数字以 0 开头,则假定基数为 8。

常见的错误是允许用户输入或类似的内容以 0 开头。此时将使用基数 8(八进制),我们看到了这种效果:

parseInt("8"); //8
parseInt("08"); //0

因此,请务必包含第二个参数。

parseInt("8", 10); //8
parseInt("08", 10); //8

ECMAScript5 旁注:ES5 不再包含基数 8 的假定。此外,省略第二个参数会引发 JSLint 警告,这很有帮助。

字符串替换

字符串 replace 函数只替换第一个匹配项,而不是您可能期望的所有匹配项。

"bob".replace("b", "x"); // "xob"
"bob".replace(/b/, "x"); // "xob" (regular expression version)

要替换所有匹配项,您必须使用正则表达式,并为其添加全局修饰符。

"bob".replace(/b/g, "x"); // "xox"
"bob".replace(new RegExp("b", "g"), "x"); // "xox" (alternate explicit RegExp)

全局修饰符确保替换不会在第一个匹配项后停止。

'+' 运算符既能相加也能拼接

PHP,另一种弱类型语言,使用 '.' 运算符进行 string 拼接。JavaScript 没有 - 因此“a + b”当任一操作数为 string 时,结果总是拼接。当您尝试将数字添加到,例如,输入元素的内容(它将是 string )时,这可能会让您措手不及,因此您需要先转换为 Number

1 + document.getElementById("inputElem").value; // Concatenates
1 + Number(document.getElementById("inputElem").value); // Adds

请注意,减法运算符会尝试将操作数转换为 Number

"3" - "1"; // 2

然而,如果您试图用一个 string 减去另一个 string ,那可能说明您的逻辑有问题。

有时人们试图通过与空 string 拼接来将 Number 转换为 string

3 + ""; // "3"

但这并不好,所以请使用 String(3) 代替。

typeof

返回基本类型实例的类型。Array 实际上不是基本类型,所以 typeof 数组对象的类型是 Object

typeof {} === "object" //true
typeof "" === "string" //true
typeof [] === "array"; //false

当您对自定义对象实例使用此运算符时,您将获得相同的结果(typeof = "object")。

顺便说一句,“typeof null”返回“object”,这有点奇怪。

instanceof

返回对象(或其原型链中的对象)是否是通过指定的构造函数构造的,这在尝试检查您自己定义的某种对象类型的类型时很有用。然而,当您使用字面量语法创建内置类型实例时,它会产生误导。

"hello" instanceof String; //false
new String("hello") instanceof String; //true

由于 Array 并不是真正的内置类型之一(它只是伪装成这样 - 因此 typeof 不如预期那样工作),instanceof 的工作方式符合预期。

["item1", "item2"] instanceof Array;  //true
new Array("item1", "item2") instanceof Array;  //true

好了!总而言之,如果您想测试 BooleanStringNumberFunction 的类型,您可以使用 typeof。对于其他任何类型,您都可以使用 instanceof

哦,还有一件事。在函数内部,有一个预定义的变量“arguments”,它给出了传递给函数的参数列表。然而,它实际上并不是一个数组,而是一个类数组对象,它有一个 length 属性,以及从 0length 的属性。非常奇怪……但您可以使用这个常见的技巧将其转换为真正的数组。

var args = Array.prototype.slice.call(arguments, 0);

同样适用于 DOM 调用(如 getElementsByTagName)返回的 NodeList 对象 - 您也可以使用上述代码将它们转换为标准的数组。

eval

evalstring 解释为代码,但其使用通常不被推荐。它很慢 - 当 JavaScript 加载到浏览器时,它会被编译成原生代码;然而,每当执行过程中遇到 eval 语句时,编译引擎就必须重新启动,这相当昂贵。它看起来也很丑陋,因为在大多数情况下它会被滥用。此外,eval 的代码在当前作用域中执行,因此它可以修改局部变量并向您的作用域添加内容,这可能是非预期的。

解析 JSON 是一个常见的用法;通常人们会使用“var obj = eval(jsonText);”,但几乎所有浏览器现在都支持本地 JSON 对象,您可以使用它代替:“var obj = JSON.parse(jsonText);”。还有一个用于相反用法的 JSON.stringify 函数。更好的是,您可以使用 jQuery.parseJSON 函数,它总是有效的。

setTimeoutsetInterval 函数可以将 string 作为第一个参数,该参数将被解释,所以也不应该这样做。请使用实际的函数作为该参数。

最后,Function 构造函数与 eval 非常相似 - 唯一的区别是它将在全局上下文中执行。

with

with 语句提供了一种访问对象属性的简写方式,关于是否应该使用它存在相互冲突的观点。Douglas Crockford 不喜欢它。John Resig 在他的书中发现了一些巧妙的使用方法,但也承认它会影响性能,并且可能有点令人困惑。单独查看 with 块,无法确切了解发生了什么。例如:

with (obj) {
    bob = "mmm";
    eric = 123;
}

我只是修改了一个名为“bob”的局部变量,还是设置了 obj.bob?嗯,如果 obj.bob 已经定义,那么它将被重置为“mmm”。否则,如果作用域中还有另一个 bob 变量,则会更改它。否则,将设置全局变量 bob。最终,写清楚您想做什么会更清晰。

obj.bob = "mmm";
obj.eric = 123;

ECMAScript5 旁注:ES5 严格模式将不支持 with 语句。

Math.min 和 Math.max

这两个是实用函数,它们给出参数的最大值或最小值。然而,如果您在没有任何参数的情况下使用它们,您会得到:

Math.max(); // -Infinity
Math.min(); // Infinity 

这实际上是有道理的,因为 Math.max() 返回的是空数字列表中最大的那个。但如果您这样做,您可能想要的是 Number.MAX_VALUENumber.MIN_VALUE

类型和构造函数

使用 'new' 关键字构造内置类型

JavaScript 具有 ObjectArrayBooleanNumberStringFunction 类型。每种类型都有其自己的字面量语法,因此显式构造函数永远是必需的。

显式(错误) 字面量(正确)
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

typeof new Number(123); // "object"
typeof Number(123); // "number"
typeof 123; // "number"

第三种选择,即字面量语法,在构造这些类型中的任何一种时都应该始终使用,以避免任何混淆。

不使用 'new' 关键字构造其他任何东西

如果您编写了自己的构造函数,并忘记包含 new 关键字,那么就会发生糟糕的事情。

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 关键字调用函数会创建一个新对象,然后使用该新对象作为上下文来调用该函数。然后返回该对象。反之,在没有 'new' 的情况下调用函数将导致上下文为全局对象(如果该函数不是在对象上调用的,而如果它被用作构造函数,它就不会!)。

意外忘记包含 'new' 的危险意味着出现了一些替代的对象构造模式,这些模式完全消除了对该关键字的要求,尽管这超出了本文的范围,因此我建议 进一步阅读

没有整数

数值计算相对较慢,因为没有整数类型,只有 Number - 而 Number 是 IEEE 浮点双精度(64 位)类型。这意味着 Number 会出现浮点舍入误差。

0.1 + 0.2 === 0.3 //false

由于不像 C# 或 Java 那样区分整数和浮点数,这是真的。

0.0 === 0; //true

最后是一个 Number 谜题。您如何实现以下目标?

a === b; //true
1/a === 1/b; //false

答案是 Number 的规范允许正零和负零值。正零等于负零,但正无穷大不等于负无穷大。

var a = 0 * 1; // This simple sum gives 0
var b = 0 * -1; // This simple sum gives -0 (you could also just
                // do "b = -0" but why would you do that?)
a === b; //true: 0 equals -0
1/a === 1/b; //false: Infinity does not equal -Infinity

作用域

没有块级作用域

正如您可能在上一条中注意到的,不存在块级作用域的概念,只有函数作用域。尝试以下代码:

for(var i=0; i<10; i++) {
    console.log(i);
}
var i;
console.log(i); // 10

ifor 循环中声明时,它在循环退出后仍然在作用域内。因此,最后一次调用 console.log 将输出 10。有一个 JSLint 警告旨在避免这种混淆:强制所有变量在函数开头声明,这样就可以清楚地了解发生了什么。

可以通过编写一个立即执行的函数来创建作用域。

(function (){
    for(var i=0; i<10; i++) {
        console.log(i);
    }
}());
var i;
console.log(i); // undefined

当您在内部函数 *之前* 声明一个 var,然后在该函数 *内部* 重新声明它时,会出现另一种扭曲。请看这个例子:

var x = 3;
(function (){
    console.log(x + 2); // 5
    x = 0; //No var declaration
}());

然而,如果您在内部函数中将 x 重新声明为 var,则会出现奇怪的行为:

var x = 3;
(function (){
    console.log(x + 2); //NaN - x is not defined
    var x = 0; //var declaration
}());

这是因为“x”在函数内部被重新定义了。这意味着解释器会将 var 语句移到函数顶部,我们最终会执行这个:

var x = 3;
(function (){
    var x;
    console.log(x + 2); //NaN - x is not defined
    x = 0;
}());

这完全说得通!

全局变量

JavaScript 有一个全局作用域,您应该谨慎使用,通过命名空间来组织代码。全局变量会给您的应用程序带来性能损失,因为在访问它们时,运行时必须逐级向上遍历每个作用域直到找到它们。它们可以被您自己的代码和其他库有意或无意地访问和修改。这导致了另一个更严重的问题——跨站脚本攻击。如果一个坏人设法在您的页面上执行某些代码,那么他们也可以通过修改全局变量来轻松干扰您的应用程序本身。新手开发人员经常会无意中将变量添加到全局作用域,并且在本文中会有一些例子说明这种情况是如何发生的。

我见过以下代码,它试图声明两个具有相同值的局部变量:

var a = b = 3;

这正确地导致 a = 3b = 3,但是 a 在局部作用域,而 b 在全局作用域。“b = 3”首先执行,然后该全局操作的结果 3 被赋值给局部变量 a

此代码具有所需的效果,即声明两个值为 3 的局部变量:

var a = 3,
b = a;

'this' 和内部函数

关键字 'this' 始终引用当前函数被调用的对象。然而,如果函数不是在对象上调用的,就像内部函数那样,那么 'this' 将被设置为全局对象(window)。

var obj = {
    doSomething: function () {
        var a = "bob";
        console.log(this); // obj
        (function () {
            console.log(this); // window - "this" is reset
            console.log(a); // "bob" - still in scope
        }());
    }
};
obj.doSomething();

然而,JavaScript 中有闭包,所以如果我们想在内部函数中引用 'this',我们可以获取它的一个引用。

ECMAScript5 旁注:ES5 严格模式引入了此功能的一项更改,即 'this' 将为 undefined,而不是被设置为全局对象。

杂项

数据的缺失:'null' 和 'undefined'

有两个对象状态表示缺少值:nullundefined。这对于来自 C# 等其他语言的程序员来说相当令人困惑。您可能期望以下值为 true

var a;
a === null; //false 
a === undefined; //true

实际上 'a' 是 undefined(尽管如果您使用双等号与 null 进行比较,它将为 true,但这只是另一个导致代码“看起来”正常工作的错误)。

如果您想检查一个变量是否真的有一个值,并且遵循从不使用双等号的规则,您需要这样做:

if(a !== null && a !== undefined) {
    ...
}

或者,您可以这样做,这完全相同(尽管 JSLint 会不幸地抱怨您):

if (a != null) {
    ...
} 

“啊哈!”您可能会说。nullundefined 都是假值(即,被强制转换为 false),所以您可以这样做:

if(a) {
    ...
}

但是,当然,零也是假值,空 string 也是。如果其中任何一个是 'a' 的有效值,那么您将不得不使用前面更冗长的选项。较短的选项始终可用于对象、数组和布尔值。

重新定义 undefined

没错,您可以重新定义 undefined,因为它不是保留字。

undefined = "surprise!";

然而,您可以通过赋值一个未定义变量或使用“void”运算符(否则它相当无用)来恢复它。

undefined = void 0;

……这就是为什么 jQuery 库的第一行是:

(function ( window, undefined ) {
    ... // jQuery library!
}(window));

该函数以一个参数调用,确保第二个参数 undefined 确实是未定义的。

顺便说一句,您不能重新定义 null - 但您可以重新定义 NaNInfinity 以及内置类型的构造函数。试试这个:

Array = function (){ alert("hello!"); }
var a = new Array();

当然,无论如何您都应该使用字面量语法来声明一个 Array

可选的分号

分号是可选的,以便初学者更容易编写,但省略它们非常糟糕,而且实际上并没有让任何人变得更容易。结果是,当解释器遇到错误时,它会回溯并尝试猜测分号应该放在哪里。

这是一个经典的例子,来自 Douglas Crockford:

return
{
    a: "hello"
};

上面的代码不返回一个对象,它返回 undefined - 并且没有抛出错误。分号会自动插入到 return 语句之后。其余的代码是完全有效的,即使它什么都不做,这应该足以证明在 JavaScript 中,开头的花括号应该放在当前行的末尾,而不是新的一行。这不仅仅是风格问题!这段代码返回一个带有一个属性“a”的对象:

return {
    a: "hello"
};

NaN

NaN(不是数字)的类型是……Number

typeof NaN === "number" //true

此外,NaN 与任何东西比较都为 false。

NaN === NaN; // false

由于您无法进行 NaN 比较,因此测试一个数字是否等于 NaN 的唯一方法是使用辅助函数 isNaN

顺便说一句,我们还有辅助函数 isFinite,当参数是 NaNInfinity 时,它返回 false

'arguments' 对象

在函数内部,您可以引用 arguments 对象来检索参数列表。第一个陷阱是此对象不是 Array,而是类数组对象(即具有 length 属性的对象,以及 [length-1] 处的值)。要转换为标准数组,您可以使用数组 splice 函数从 arguments 中的属性创建数组。

(function(){
console.log(arguments instanceof Array); // false 
var argsArray = Array.prototype.slice.call(arguments);
console.log(argsArray instanceof Array); // true 
}());

第二个陷阱是,当函数在其签名中有显式参数时,可以重新分配它们,并且 arguments 对象也会发生变化。这表明 arguments 对象指向变量本身而不是它们的值;您不能依赖 arguments 来获取它们的值。

(function(a){
    alert(arguments[0]); //1
    a = 2;
    alert(arguments[0]); //2
}(1));

在 ES5 严格模式下,arguments 将肯定指向原始输入参数值,而不是它们当前的值。

结束!

以上就是我收集的陷阱列表。我敢肯定还有更多,我期待看到大量富有见地的评论!

附注:我确实 *喜欢* JavaScript。

参考文献

© . All rights reserved.