原文地址:codeburst.io/does-js-really-do-that-43f17bc01e9b
在通读了《You-Dont-Know-JS》后,我很困惑 JavaScript 的奇怪。
渐渐地,我开始寻找方法去验证他的疯狂。
在这里,我简要描述了一些我遇到过的“陷阱”以及我在使用 JavaScript 时遇到的一些问题。
我们经常提及的强迫转换难题是 [] + {}
VS. {} + []
,因为这两种计算方式出现了不同结果:
[] + {} // "[object Object]"
{} + [] // 0
那么,发生了什么?
当执行 +
操作的操作数为对象时,该对象将会被强转为一个原始类型数值。为了实现这个强转,首先 valueOf()
会作用于该对象。如果它无法生成一个简单原始类型值,它将会执行 toString()
来获取结果。
如果任何 +
操作的操作数是字符串时,结果都将会是字符串。
对于 [] + {}
,valueOf()
操作符作用于 []
时将无法产生原始数值,所以 toString()
方法将会被调用,而 []
它将会被强转为 ""
(空字符串)。当一个操作数为字符串时,另一个也会被强转为字符串,因此 {}
将会被强转为 "[object object]"
。
对于 {} + []
,{}
被解释为一个独立的空块(不需要分号来终止),而 + []
作为一个表达式,导致结果显式的将[]
强转为数字 :0。同样可以被写成:{}; +[]
。
当布尔值与其他原始类型进行比较时,将会出现不可预测的结果。
已知非空字符串与非零数字都是 真值。因此我们会预测判断 真值 == 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。
我们经常需要检查两个对象引用是否指向同一个对象,并使用 ==
操作符来实现该效果。但你是否想过 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 肯定是
false
,那么 取反,a <= b
就是真了。
Js 解析 <=
为不大于(!(a > b)
),会视为 !(b < a)
进行处理。而 a >= b
,Js 会根据以上方式,作为 b <= a
进行处理。
以上可能有点绕,弄清楚就会很清晰了~
var a = 42;
++a++ = ?
我们沉迷于偶尔使用的自增运算符,那 ++a++
会是怎样的怪物呢?它是否合法?
如果你尝试过,将会返回一个 ReferenceError
的报错,那为什么呢?因为这类操作需要一个 变量引用 去定位他们的附加操作。对于 ++a++
,a++
将会先被计算(因为操作符优先级),再次增加前会返回变量 a
的值。而当它尝试计算 ++42
时,将会返回 ReferenceError
的报错。
这是因为 ++
无法直接作用于数值,例如42。
让我阐述一下原因。在控制台运行一下代码:
//确保你在全局环境下
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 == null; // 真
undefined +1 == null +1; //假, 为什么?
当进行相加操作时,他们会生成不同的结果:
undefined + 1 // NaN
null + 1 // 1
产生这种结果的原因是:当进行相加等运算操作时,undefined
被 ToNumber
抽象化强转为 NaN
,而 null
则强转为 0。
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( 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
声明的变量中才能观察到这个怪事。
后续将会持续更新…