今天看到几道有关加号的题目,觉得很怪异如下
[] + {} // '[object Object]'
[] + [] // ''
{} + [] // 0
{} + {} // NaN
[] + {} == {} + [] // true
{} + [] != [] + {} // true
当时我就完全没有搞懂,所以决定探索一下 JavaScript
中的 +
号运算符,另外,对于 toString
和 valueOf
这两个方法一直搞不清会调用哪个,在探索 +
号运算符的过程中也一并搞懂了,我将会在下面仔细讲解。
一元运算符
+
既可以作为一元运算符,也可以作为二元运算符,首先我们先讲解较简单的一元运算符,如下表:
类型 | 转换规则 |
---|---|
undefiend |
NaN |
null |
0 |
boolean |
true => 1 false => 0 |
number |
原样返回,如 +1 => 1 |
string |
1. 如果字符串为纯数字组成,如 "5678" => 5678 2. 如果不为纯数字,那么返回 NaN 3. 空字符串会被转化为 0 ,即 "" => 0 |
symbol |
抛出 TypeError 异常 |
object |
分为两步: 1. 先进行 toPrimitive 转化为基本数据类型,得到返回值 ret 2. 然后对 ret 进行上面描述过程的转换,例如如果返回 true ,得到 1 |
下表是 ECMAScript
中的规范,上面的内容来自这里:
Argument | Result |
---|---|
Undefined |
NaN |
Null |
+0 |
Boolean |
The result is 1 if the argument is true. The result is +0 if the argument is false. |
Number |
The result equals the input argument (no conversion). |
String |
See grammar and note below. |
Object |
Apply the following steps: 1. Let primValue be ToPrimitive(input argument, hint Number). 2. Return ToNumber(primValue). |
下面来看几个例子,来验证上面的内容
console.log(+undefined); // NaN
console.log(+null); // 0
console.log(+true); // 1
console.log(+false); // 0
console.log(+2); // 2
console.log(+Symbol()); // Uncaught TypeError: Cannot convert a Symbol value to a number
console.log(+"123"); // 123
console.log(+"1aa1"); // NaN
对于对象首先要进行 toPrimitive
转化为原始类型,然后将原始类型转化为数字
let obj1 = {
valueOf() {
return 12;
}
};
console.log(+obj1); // 12
let obj2 = {};
console.log(+obj2); // NaN
对于空对象,在转化为原始值时得到的是 "[object Object]"
,将它转化为数字时,这个字符串不是纯数字,所以会被转化为 NaN
。
二元运算符
当把 +
作为二元运算符时,遵循以下过程:
- Let lref be the result of evaluating AdditiveExpression.
- Let lval be GetValue(lref).
- ReturnIfAbrupt(lval).
- Let rref be the result of evaluating MultiplicativeExpression.
- Let rval be GetValue(rref).
- ReturnIfAbrupt(rval).
- Let lprim be ToPrimitive(lval).
- ReturnIfAbrupt(lprim).
- Let rprim be ToPrimitive(rval).
- ReturnIfAbrupt(rprim).
- If Type(lprim) is String or Type(rprim) is String, then
- Let lstr be ToString(lprim).
- ReturnIfAbrupt(lstr).
- Let rstr be ToString(rprim).
- ReturnIfAbrupt(rstr).
- Return the String that is the result of concatenating lstr and rstr.
- Let lnum be ToNumber(lprim).
- ReturnIfAbrupt(lnum).
- Let rnum be ToNumber(rprim).
- ReturnIfAbrupt(rnum).
- Return the result of applying the addition operation to lnum and rnum. See the Note below 12.7.5.
上面是 ECMAScript
中的规范,如果英文不太熟的话,下面我将以中文简单翻译如下:
- 首先将
+
号两边的值通过toPrimitive
转化为基本数据类型(如果已经是基本数据类型,则原样返回) - 如果得到的两个基本数据类型中有字符串,那么将二者转化为字符串拼接起来,将结果返回
- 如果两个基本数据类型中没有字符串,那么就将两个值转化为数字,然后进行相加
下面来看几个例子:
// 两个值中有一个为字符串,则调用二者的 toString 方法,然后进行相加
"" + 2; // "2"
true + "abc"; // "trueabc"
// 两个值中没有字符串,那么转化为数字然后相加
2 + true; // => 2 + 1 = 3
null + 2; // => 0 + 2 = 2
[] + 2; // => "" + 2 = "2"
对于对象要转化为原始类型,然后进行相加,那么对象转化为原始类型的过程是什么? 下面是调用的过程:
- 如果有
[Symbol.toPrimitive]
方法,则调用[Symbol.toPrimitive]()
方法转化为原始类型,该方法的返回必须为原始类型 - 如果没有
[Symbol.toPrimitive]
方法,那么调用valueOf
方法,如果valueOf
返回的不是原始值(基本数据类型),那么就调用toString
方法,如果toString
返回的也不是原始值,那么就会报错
[] + 2; // "" + 2 = "2"
数组也是对象,因为数组没有 [Symbol.toPrimitive]
方法,所以首先会调用数组的 valueOf
方法,因为数组的 valueOf
方法返回的是数组本身,并不是基本数据类型,所以接着会调用数组的 toString
方法,得到一个空字符串 ""
,通过我们上面的讲解,如果两个值中有一个是字符串的话,则会将二者转化为字符串进行拼接,所以 [] + 2 => "" + 2 = "2"
。下面在来看一个例子:
let obj = {
[Symbol.toPrimitive](hint) {
return 20;
},
valueOf() {
return 0;
}
};
2 + obj; // 22
由于对象 obj
有 [Symbol.toPrimitive]
方法,所以在转化为基本数据类型时会调用该方法,得到的值为 20
,所以 2 + obj => 2 + 20 = 22
。
题目讲解
回到开头我们提出的几个例子:
[] + {} // '[object Object]'
首先将两个东西转化为基本数据类型,因为它们都没有 [Symbol.toPrimitive]
方法,所以接着会调用它们的 valueOf
方法,但是它们的 valueOf
方法返回的都是它们本身,所以接着会调用它们的 toString
方法,[]
的 toString
方法得到的是 ''
空字符串,{}
的 toString
方法得到的是 '[object Object]'
,二者都是字符串,将二者进行拼接,得到的结果是 '[object Object]'
。
[] + [] // ''
有上题的经验,[] => ''
,两个空字符串进行拼接得到的结果是 ''
。
{} + [] // 0
接着看 {} + []
,因为 JavaScript
会将以 {
开头的语句解析为代码块而不是一个空对象,所以 {} + []
相当于 +[]
,这时的 +
相当于是一个一元运算符,根据一元运算符上面讲解的内容,首先将 []
转化为基本数据类型,得到 ""
,接着将 ""
转化为数字,得到的结果为 0
。
{} + {} // NaN
根据上一道题的讲解,{} + {}
相当于 +{}
,{}
转换为基本数据类型得到的是 "[object Object]"
,该字符串转换为数字得到的 NaN
,所以结果是 NaN
。
[] + {} == {} + [] // true
[] + {}
得到的结果是 '[object Object]'
,{} + []
得到的结果并不是 0
,因为这时 {}
并不是在语句的开头,会被看做是空对象,所以 {} + []
得到的结果也是 '[object Object]'
,二者是相等的,结果是 true
{} + [] != [] + {} // true
{} + []
因为在开头,得到的结果是 0
,而 [] + {}
的结果是 [object Object]
,二者不相等,所以 {} + [] != [] + {}
的结果也是 true
。