JS 不常见陷阱
- 我应该怎么强迫你(How should I coerce you? )
- 当布尔值与其他类型比较时(When Booleans were compared to others)
- 对象比较的荒谬实例(The absurd case of object comparison)
- 使用后增量预增(Pre-increment with post-incrment)
- 谨慎的使用"name"命名变量("name" must be named with caution)
- undefined 和 null, 相同也不同(undefined and null, same but different)
- 解构的一个警告(A caveat to deconstructuring)
- 避免 for 循环(Avoid for-loops)
在通读了《You-Dont-Know-JS》后,我很困惑 JavaScript 的奇怪。
渐渐地,我开始寻找方法去验证他的疯狂。
在这里,我简要描述了一些我遇到过的“陷阱”以及我在使用 JavaScript 时遇到的一些问题。
我应该怎么强迫你(How should I coerce you? )
我们经常提及的强迫转换难题是 [] + {} VS. {} + [],因为这两种计算方式出现了不同结果:
[] + {} // "[object Object]"
{} + [] // 0
那么,发生了什么?
-
当执行
+操作的操作数为对象时,该对象将会被强转为一个原始类型数值。为了实现这个强转,首先valueOf()会作用于该对象。如果它无法生成一个简单原始类型值,它将会执行toString()来获取结果。 -
如果任何
+操作的操作数是字符串时,结果都将会是字符串。
对于 [] + {} ,valueOf() 操作符作用于 [] 时将无法产生原始数值,所以 toString() 方法将会被调用,而 [] 它将会被强转为 ""(空字符串)。当一个操作数为字符串时,另一个也会被强转为字符串,因此 {} 将会被强转为 "[object object]"。
对于 {} + [] ,{} 被解释为一个独立的空块(不需要分号来终止),而 + [] 作为一个表达式,导致结果显式的将[] 强转为数字 :0。同样可以被写成:{}; +[] 。
当布尔值与其他类型比较时(When Booleans were compared to others)
当布尔值与其他原始类型进行比较时,将会出现不可预测的结果。
已知非空字符串与非零数字都是 真值。因此我们会预测判断 真值 == true 返回 true。而实际呢?
var x = true, y = "42";
x == y; // 假
y = "true";
x == y; // 假
因为,根据 ES5 规范:
如果 Type(x) 是 Boolean,返回 ToNumber(x) == y 的比较结果;
如果 Type(y) 是 Boolean,返回 x == ToNumber(y) 的比较结果;
如果 Type(x) 是数字,而 Type(y) 是字符串,返回 x == ToNumber(y) 的比较结果;
如果 Type(x) 是字符串,而 Type(y) 是数字,返回 ToNumber(x) == y 的比较结果;
所以,第一个实例中,x 的类型为布尔值,所以它将会转为 ToNumber(x),true 会强转为 1。现在,转为 1 == "42" 的计算,类型仍然不同,需要重新执行算法,根据 ES5 规范,字符串进行强转,转为 1 == 42 的计算,而 1 == 42 明显返回为 false 。
同样的,第二个示例中,x 被转为1,而 y 通过 ES5 规范,进行 ToNumber(y) 计算,"true" 强转为 NaN;而 1 == NaN 明显也返回 false。
对象比较的荒谬实例(The absurd case of object comparison)
我们经常需要检查两个对象引用是否指向同一个对象,并使用 == 操作符来实现该效果。但你是否想过 JavaScript 对两个对象间的其他逻辑比较如何处理?
var a = {b : 42};
var b = {b : 42};
a == b; // 假,检测a,b是否指向同一个对象。
a < b; // 假
a > b; // 假
a < b 强制两个对象转为他们各自的原始类型值。因为,这两者都没有定义 valueOf() 方法,他们的值会被 toString() 强制转换,而a 和 b 都会转为 [object object],因此,结果返回 false。
如果尝试:
a <= b; // 真
a >= b; // 真
哈?!发生了什么?
当 a == b,a < b,a > b 都为假时,a <= b,a >= b 怎么可能会是真?
因为 ES5 规范规定,计算 a <= b,实际是先计算 b < a,然后取反。而 b <a 肯定是 false,那么 取反,a <= b 就是真了。
Js 解析 <= 为不大于(!(a > b)),会视为 !(b < a) 进行处理。而 a >= b,Js 会根据以上方式,作为 b <= a 进行处理。
以上可能有点绕,弄清楚就会很清晰了~
使用后增量预增(Pre-increment with post-incrment)
var a = 42;
++a++ = ?
我们沉迷于偶尔使用的自增运算符,那 ++a++ 会是怎样的怪物呢?它是否合法?
如果你尝试过,将会返回一个 ReferenceError 的报错,那为什么呢?因为这类操作需要一个 变量引用 去定位他们的附加操作。对于 ++a++,a++ 将会先被计算(因为操作符优先级),再次增加前会返回变量 a 的值。而当它尝试计算 ++42 时,将会返回 ReferenceError 的报错。
这是因为 ++ 无法直接作用于数值,例如42。
谨慎的使用"name"命名变量(“name” must be named with caution)
让我阐述一下原因。在控制台运行一下代码:
//确保你在全局环境下
name = function(me) {
console.log("I'm ${me}! :)");
}
确认你没有因为为提前声明 “name” 变量而出现 ReferenceError 的报错。
运行:
name("Parama");
//TypeError : name is not a function
为什么会报错?
因为 name 和 window.name 是保留关键字。当变量被命名时,它会自动被字符串化,即 toString() 方法可被调用。
window.name 会获取/设置 window 的变量名。只有在 var 声明时会发生,let 声明不会,因为 let 声明的变量不能影响 window 对象。
undefined 和 null, 相同也不同(undefined and null, same but different)
当进行比较时,undefined 和 null 都会被强转并返回 真:
undefined == null; // 真
undefined +1 == null +1; //假, 为什么?
当进行相加操作时,他们会生成不同的结果:
undefined + 1 // NaN
null + 1 // 1
产生这种结果的原因是:当进行相加等运算操作时,undefined 被 ToNumber 抽象化强转为 NaN,而 null 则强转为 0。
解构的一个警告(A caveat to deconstructuring)
var { a : X, a : Y, a : [Z] } = { a : [1] };
如果你对解构熟悉,你会知道 X 和 Y 的结果是数组 [1],而 Z 的结果是 1。让我们把 X 和 Y 拧到一起:
X.push( 2 );
Y[0] = 10;
X; // [10, 2] 怎么会?!
Y; // [10, 2] 发生了什么?!
Z; // 1
在以上片段中,X 和 Y 以及对象属性(该实例中的数组)被解构并指向同一块内存空间。所以~奇怪的行为。
避免 for 循环(Avoid for-loops)
我有充分的理由发出这个警告,假设以下输出:
for( let i=(setTimeout( ()=>console.log(i)), 0); i<2; i++) {
i++;
}
如果你想法与我一样,你会假设结果输出为 1 ,是吗?
但是,正确答案却是 0。
让我们一步步的来解开谜题:
-
在给循环添加了
setTimeout()事件后,i被初始化为 0; -
给
for循环中的初始声明部分创建一个独立的词法空间。这块词法空间与for循环的主体不同,互相独立。因此,i的值在闭环中会被保存,即setTimeout()中的i与外部for循环中不同。 -
在第一次迭代中,当条件被满足时
for循环主体内容被执行,i变成 1。setTimeout()已被关闭,因此for循环中i的修改不会影响到setTimeout()中的i值。 -
for循环终止后,0将会被打印。
注意:只有在 let 声明的变量中才能观察到这个怪事。
后续将会持续更新…