首先从一系列让JavaScript初学者抓狂的运算说起。1 + {}{} + 1[] + {}{} + [][] + []{} + {}能全部答对上面的运算结果,不必浪费时间继续阅读本文了。如果对某一些的结果还不确定,请慢慢往下看。上面列的所有运算,需要理清以下两点:+和{}的解析规则;JavaScript是如何进行类型转换的;+和{}的解析规则+符号数字的加法运算,二元运算符字符串的连接运算,二元运算符正号,一元运算符,强制转换其他类型的运算元为数字类型{}符号对象的字面量区块语句加法运算规则首先,我们来了解,+符号作为加号二元运算符的运算规则使用ToPrimitive转换左右运算元为原始数据类型值;在第1步转换后,如果有运算元出现原始数据类型为“字符串”类型时,则另一运算元强制转换为字符串,然后做字符串连接运算;其他情况时,所有运算元都转换为原始数据类型为“数字”类型,然后做数字相加运算;ToPrimitive运算从上图总结ToPrimitive运算的语法说明:ToPrimitive(input, PreferredType?)input代表代入的值,而PreferredType可以是数字(Number)或字符串(String)其中一种,这会代表“优先的”、“首选的”的要进行转换到哪一种原始类型,转换的步骤会依这里的值而有所不同。但如果没有提供这个值也就是预设情况,则会设置转换的hint值为default。这个首选的转换原始类型的指示(hint值),是在作内部转换时由JS视情况自动加上的,一般情况就是预设值。当PreferredType为数字(Number)时转换步骤为:如果input是原始数据类型,则直接返回input。否则,如果input是个对象时,则调用对象的valueOf()方法,如果能得到原始数据类型的值,则返回这个值。否则,如果input是个对象时,调用对象的toString()方法,如果能得到原始数据类型的值,则返回这个值。否则,抛出TypeError错误。当PreferredType为字符串(String)时上面的步骤2与3对调,转换步骤为:如果input是原始数据类型,则直接返回input。否则,如果input是个对象时,则调用对象的toString方法,如果能得到原始数据类型的值,则返回这个值。否则,如果input是个对象时,调用对象的valueOf()方法,如果能得到原始数据类型的值,则返回这个值。否则,抛出TypeError错误。有几点值得注意:数字是预设的PreferredType;在一般情况下,加号运算中的对象要作转型时,都是先调用valueOf再调用toString;例外:Date 对象、Symbol 对象Date 对象的预设首选类型是字符串(String);Symbol 能显示转为字符串 String(Symbol) 和布尔值,不能转为数字;模拟代码a + b: pa = ToPrimitive(a) pb = ToPrimitive(b) if(pa is string || pb is string) return concat(ToString(pa), ToString(pb)) else return add(ToNumber(pa), ToNumber(pb))V8实现源码// ECMA-262, section 9.1, page 30. Use null/undefined for no hint,// (1) for number hint, and (2) for string hint.function ToPrimitive(x, hint) { if (!IS_SPEC_OBJECT(x)) return x; if (hint == NO_HINT) hint = (IS_DATE(x)) ? STRING_HINT : NUMBER_HINT; return (hint == NUMBER_HINT) ? DefaultNumber(x) : DefaultString(x);}可以看出,Date类型的对象预设值为字符串(String)。DefaultNumber和DefaultString方法// ECMA-262, section 8.6.2.6, page 28.function DefaultString(x) { if (!IS_SYMBOL_WRAPPER(x)) { if (IS_SYMBOL(x)) throw MakeTypeError(kSymbolToString); var toString = x.toString; if (IS_SPEC_FUNCTION(toString)) { var s = % _CallFunction(x, toString); if (IsPrimitive(s)) return s; } var valueOf = x.valueOf; if (IS_SPEC_FUNCTION(valueOf)) { var v = % _CallFunction(x, valueOf); if (IsPrimitive(v)) return v; } } throw MakeTypeError(kCannotConvertToPrimitive);}// ECMA-262, section 8.6.2.6, page 28.function DefaultNumber(x) { var valueOf = x.valueOf; if (IS_SPEC_FUNCTION(valueOf)) { var v = % _CallFunction(x, valueOf); if (IS_SYMBOL(v)) throw MakeTypeError(kSymbolToNumber); if (IS_SIMD_VALUE(x)) throw MakeTypeError(kSimdToNumber); if (IsPrimitive(v)) return v; } var toString = x.toString; if (IS_SPEC_FUNCTION(toString)) { var s = % _CallFunction(x, toString); if (IsPrimitive(s)) return s; } throw MakeTypeError(kCannotConvertToPrimitive);}至此,我们弄清楚了ToPrimitive内部运算规则。原始数据类型转换规则表对于原始数据类型直接的转换规则就不一一解释了,可以参看下表:运算例子解析1 + {}解析为数字1与空对象{}进行加运算,按照上面的规则,空对象{}按照valueOf -> toString顺序,valueOf返回对象本身,所以调用toString返回"[object Object]“为字符串,根据规则,有一个为字符串,则进行字符串连接运算,数字1强制转换为字符串"1”,最终结果为"1[object Object]"。{} + 1这个例子有坑,因为对于浏览器引擎来说,它们会认为以花括号开头{的,是一个 区块语句 的开头,而不是一个对象字面量的语句,所以会认为略过第一个{}。所以这个例子相当于+1,加号运算会直接变为一元正号运算,对数字1强制转换为数字类型,结果为1。[] + {}将[]和{}分别调用ToPrimitive方法,返回"“和”[object Object]",然后做字符串拼接得到"[object Object]"。{} + []开始的{}被解析为区块语句而略过,相当于+[],而[]转为原始数据类型结果为"",强制转换"“为数字类型,结果为0;[] + []将[]和{}分别调用ToPrimitive方法,返回"“和”",然后做字符串拼接得到”"。{} + {}这个例子有争议,对于Firefox和Edge浏览器来说,他们一以贯之的将第一个{}作为区块语句略过,而对于V8引擎系列(Chrome、Node.js等)则将第一个{}解析为对象字面量。这样导致结果不一致,Firefox等解析语句为+{},对空对象{}强制转为数字类型,即为+"[object Object]",将非空字符串转换为数字类型,结果为NaN。Chrome等解析语句为两个空对象做加运算,结果也比较好理解:"[object Object][object Object]"。复杂一点的例子[] == ![]这是一个不严格等于运算,我们来看转换过程。// 第一步,转换右边,根据上述原始数据类型转换规则表,// 所有对象强制转 Boolean 类型都是 true,所以 ![] 为 falseToPrimitive(![]) >> ToPrimitive(!ToBoolean([])) >>ToPrimitive(!true) >> ToPrimitive(false) >> 0// 第二步,转换左边ToPrimitive([]) >> “”// 第三步,判断"" == 0 >> ToNumber("") == 0 >> 0 == 0 >> return true++[[]][+[]] + [+[]]进一步,来看这个例子。很明显,根据运算符优先级,这个表达式可以用+分隔为左右两个部分++[[]][+[]]和[+[]]。先看右边[+[]]可以看出这是一个数组里面有一个元素+[],而+[]即将[]强制转换为数字类型,所以等于+"",结果为0。综上,右边表达式转换为[0]。再看左边++[[]][+[]]我们来一步步拆解, 根据对右边表达式的转换,这个表达式可以等同看做++([[]][0]),++后面又可以看做数组去第1个元素,表达式转换为++[]。但是当我们去浏览器执行++[]时,报错了:<font color=red>Uncaught ReferenceError: Invalid left-hand side expression in prefix operation</font>。吓得我赶紧去看++的语法,原来++的运算是一种引用运算,即++[]应该转换为:var ref = []ref = ref + 1所以++[]转换的正确姿势为[] + 1。加法运算左右进行相加得到:[] + 1 + [0]。根据ToPrimitive运算规则,[] + 1 + [0] === "" + 1 + [0] === “1” + [0] === “10”。