JS的{} + {}与{} + []的结果是什么?
JanWadswort
8年前
<p>在JS中的运算符共同的情况中,(+)符号是很常见的一种,它有以下的使用情况:</p> <ul> <li> <p>数字的加法运算,二元运算</p> </li> <li> <p>字符串的连接运算,二元运算,最高优先</p> </li> <li> <p>正号,一元运算,可延伸为强制转换其他类型的运算元为数字类型</p> </li> </ul> <p>当然,如果考虑多个符号一起使用时,(+=)与(++)又是另外的用途。</p> <p>另一个常见的是花括号({}),它有两个用途也很常见:</p> <ul> <li> <p>对象的字面文字定义</p> </li> <li> <p>区块语句</p> </li> </ul> <p>所以,要能回答这个问题,要先搞清楚重点是什么?</p> <p>第一个重点是:</p> <p>加号(+)运算在JS中在使用上的规定是什么。</p> <p>第二个重点则是:</p> <p>对象在JS中是怎么转换为原始数据类型的值的。</p> <h2>加号运算符(+)</h2> <p>除了上面说明的常见情况外,在标准中转换的规则还有以下几个,要注意它的顺序:</p> <p>operand + operand = result</p> <ol> <li> <p>使用 ToPrimitive 运算转换左与右运算元为原始数据类型值(primitive)</p> </li> <li> <p>在第1步转换后,如果有运算元出现原始数据类型是"字符串"类型值时,则另一运算元作强制转换为字符串,然后作字符串的连接运算(concatenation)</p> </li> <li> <p>在其他情况时,所有运算元都会转换为原始数据类型的"数字"类型值,然后作数学的相加运算(addition)</p> </li> </ol> <h2>ToPrimitive内部运算</h2> <p>因此,加号运算符只能使用于原始数据类型,那么对于对象类型的值,要如何转换为原始数据类型?下面说明是如何转换为原始数据类型的。</p> <p>在 <a href="/misc/goto?guid=4959733684788649496" rel="nofollow,noindex">ECMAScript 6th Edition #7.1.1</a> ,有一个抽象的 ToPrimitive 运算,它会用于对象转换为原始数据类型,这个运算不只会用在加号运算符,也会用在关系比较或值相等比较的运算中。下面有关于 ToPrimitive 的说明语法:</p> <p>ToPrimitive(input, PreferredType?)</p> <p>input 代表代入的值,而 PreferredType 可以是数字(Number)或字符串(String)其中一种,这会代表"优先的"、"首选的"的要进行转换到哪一种原始类型,转换的步骤会依这里的值而有所不同。但如果没有提供这个值也就是预设情况,则会设置转换的 hint 值为 "default" 。这个首选的转换原始类型的指示( hint 值),是在作内部转换时由JS视情况自动加上的,一般情况就是预设值。</p> <p>而在JS的 Object 原型的设计中,都一定会有两个 valueOf 与 toString 方法,所以这两个方法在所有对象里面都会有,不过它们在转换有可能会交换被调用的顺序。</p> <h3>当PreferredType为数字(Number)时</h3> <p>当 PreferredType 为数字(Number)时, input 为要被转换的值,以下是转换这个 input 值的步骤:</p> <ol> <li> <p>如果 input 是原始数据类型,则直接返回 input 。</p> </li> <li> <p>否则,如果 input 是个对象时,则调用对象的 valueOf() 方法,如果能得到原始数据类型的值,则返回这个值。</p> </li> <li> <p>否则,如果 input 是个对象时,调用对象的 toString() 方法,如果能得到原始数据类型的值,则返回这个值。</p> </li> <li> <p>否则,抛出TypeError错误。</p> </li> </ol> <h3>当PreferredType为字符串(String)时</h3> <p>上面的步骤2与3对调,如同下面所说:</p> <ol> <li> <p>如果 input 是原始数据类型,则直接返回 input 。</p> </li> <li> <p>否则,如果 input 是个对象时,调用对象的 toString() 方法,如果能得到原始数据类型的值,则返回这个值。</p> </li> <li> <p>否则,如果 input 是个对象时,则调用对象的 valueOf() 方法,如果能得到原始数据类型的值,则返回这个值。</p> </li> <li> <p>否则,抛出TypeError错误。</p> </li> </ol> <h3>PreferredType没提供时,也就是hint为"default"时</h3> <p>与 PreferredType 为数字(Number)时的步骤相同。</p> <p>数字其实是预设的首选类型,也就是说在一般情况下,加号运算中的对象要作转型时,都是先调用 valueOf 再调用 toString 。</p> <p>但这有两个异常,一个是 Date 对象,另一是 Symbol 对象,它们覆盖了原来的 PreferredType 行为, Date 对象的预设首选类型是字符串(String)。</p> <p>因此你会看到在一些教程文件上会区分为两大类对象,一类是 Date 对象,另一类叫 非Date(non-date) 对象。因为这两大类的对象在进行转换为原始数据类型时,首选类型恰好相反。</p> <h3>模拟代码说明</h3> <p>以简单的模拟代码来说明,加号运算符(+)的运行过程就是像下面这个模拟码一样,我想这会很容易理解:</p> <pre> <code class="language-javascript">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))</code></pre> <p>步骤简单来说就是,运算元都用 ToPrimitive 先转换为原始数据类型,然后其一是字符串时,使用 ToString 强制转换另一个运算元,然后作字符串连接运算。要不然,就是都使用 ToNumber 强制转换为数字作加法运算。</p> <p>而 ToPrimitive 在遇到对象类型时,预设调用方式是先调用 valueOf 再调用 toString ,一般情况数字类型是首选类型。</p> <p>上面说的 ToString 与 ToNumber 这两个也是JS内部的抽象运算。</p> <h2>valueOf与toString方法</h2> <p>valueOf 与 ToString 是在Object中的两个必有的方法,位于Object.prototype上,它是对象要转为原始数据类型的两个姐妹方法。从上面的内容已经可以看到, ToPrimitive 这个抽象的内部运算,会依照设置的首选的类型,决定要先后调用 valueOf 与 toString 方法的顺序,当数字为首选类型时,优先使用 valueOf ,然后再调用 toString 。当字符串为首选类型时,则是相反的顺序。预设调用方式则是如数字首选类型一样,是先调用 valueOf 再调用 toString 。</p> <h3>JS对于Object与Array的设计</h3> <p>在JS中所设计的 Object 纯对象类型的 valueOf 与 toString 方法,它们的返回如下:</p> <ul> <li> <p>valueOf 方法返回值: 对象本身。</p> </li> <li> <p>toString 方法返回值: "[object Object]"字符串值,不同的内建对象的返回值是"[object type]"字符串,"type"指的是对象本身的类型识别,例如Math对象是返回"[object Math]"字符串。但有些内建对象因为覆盖了这个方法,所以直接调用时不是这种值。(注意: 这个返回字符串的前面的"object"开头英文是小写,后面开头英文是大写)</p> </li> </ul> <p>你有可能会看过,利用Object中的toString来进行各种不同对象的判断语法,这在以前JS能用的函数库或方法不多的年代经常看到,不过它需要配合使用函数中的 call 方法,才能输出正确的对象类型值,例如:</p> <pre> <code class="language-javascript">> Object.prototype.toString.call([]) "[object Array]" > Object.prototype.toString.call(new Date) "[object Date]"</code></pre> <p>所以,从上面的内容就可以知道,下面的这段代码的结果会是调用到 toString 方法(因为 valueOf 方法的返回并不是原始的数据类型):</p> <pre> <code class="language-javascript">> 1 + {} "1[object Object]"</code></pre> <p>一元正号(+),具有让首选类型(也就是hint)设置为数字(Number)的功能,所以可以强制让对象转为数字类型,一般的对象会转为:</p> <pre> <code class="language-javascript">> +{} //相当于 +"[object Object]" NaN</code></pre> <p>当然,对象的这两个方法都可以被覆盖,你可以用下面的代码来观察这两个方法的运行顺序,下面这个都是先调用 valueOf 的情况:</p> <pre> <code class="language-javascript">let obj = { valueOf: function () { console.log('valueOf'); return {}; // object }, toString: function () { console.log('toString'); return 'obj'; // string } } console.log(1 + obj); //valueOf -> toString -> '1obj' console.log(+obj); //valueOf -> toString -> NaN console.log('' + obj); //valueOf -> toString -> 'obj'</code></pre> <p>先调用 toString 的情况比较少见,大概只有 Date 对象或强制要转换为字符串时才会看到:</p> <pre> <code class="language-javascript">let obj = { valueOf: function () { console.log('valueOf'); return 1; // number }, toString: function () { console.log('toString'); return {}; // object } } alert(obj); //toString -> valueOf -> alert("1"); String(obj); //toString -> valueOf -> "1";</code></pre> <p>而下面这个例子会造成错误,因为不论顺序是如何都得不到原始数据类型的值,错误消息是"TypeError: Cannot convert object to primitive value",从这个消息中很明白的告诉你,它这里面会需要转换对象到原始数据类型:</p> <pre> <code class="language-javascript">let obj = { valueOf: function () { console.log('valueOf'); return {}; // object }, toString: function () { console.log('toString'); return {}; // string } } console.log(obj + obj); //valueOf -> toString -> error!</code></pre> <p>Array(数组)很常用到,虽然它是个对象类型,但它与Object的设计不同,它的 toString 有覆盖,说明一下数组的 valueOf 与 toString 的两个方法的返回值:</p> <ul> <li> <p>valueOf 方法返回值: 对象本身。(与Object一样)</p> </li> <li> <p>toString 方法返回值: 相当于用数组值调用 join(',') 所返回的字符串。也就是 [1,2,3].toString() 会是 "1,2,3" ,这点要特别注意。</p> </li> </ul> <p>Function对象很少会用到,它的 toString 也有被覆盖,所以并不是Object中的那个 toString ,Function对象的 valueOf 与 toString 的两个方法的返回值:</p> <ul> <li> <p>valueOf 方法返回值: 对象本身。(与Object一样)</p> </li> <li> <p>toString 方法返回值: 函数中包含的代码转为字符串值</p> </li> </ul> <h3>Number、String、Boolean三个包装对象</h3> <p>包装对象是JS为原始数据类型数字、字符串、布尔专门设计的对象,所有的这三种原始数据类型所使用到的属性与方法,都是在这上面所提供。</p> <p>包装对象的 valueOf 与 toString 的两个方法在原型上有经过覆盖,所以它们的返回值与一般的Object的设计不同:</p> <ul> <li> <p>valueOf 方法返回值: 对应的原始数据类型值</p> </li> <li> <p>toString 方法返回值: 对应的原始数据类型值,转换为字符串类型时的字符串值</p> </li> </ul> <p>toString 方法会比较特别,这三个包装对象里的 toString 的细部说明如下:</p> <ul> <li> <p>Number包装对象的 toString 方法: 可以有一个传参,可以决定转换为字符串时的进位(2、8、16)</p> </li> <li> <p>String包装对象的 toString 方法: 与String包装对象中的 valueOf 相同返回结果</p> </li> <li> <p>Boolean包装对象的 toString 方法: 返回"true"或"false"字符串</p> </li> </ul> <p>另外,常被搞混的是直接使用 Number() 、 String() 与 Boolean() 三个强制转换函数的用法,这与包装对象的用法不同,包装对象是必须使用 new 关键字进行对象实例化的,例如 new Number(123) ,而 Number('123') 则是强制转换其他类型为数字类型的函数。</p> <p>Number() 、 String() 与 Boolean() 三个强制转换函数,所对应的就是在ECMAScript标准中的 ToNumber 、 ToString 、 ToBoolean 三个内部运算转换的对照表。而当它们要转换对象类型前,会先用上面说的 ToPrimitive 先转换对象为原始数据类型,再进行转换到所要的类型值。</p> <p>不管如何,包装对象很少会被使用到,一般我们只会直接使用原始数据类型的值。而强制转换函数因为也有替换的语法,它们会被用到的机会也不多。</p> <h2>实例</h2> <h3>字符串 + 其他原始类型</h3> <p>字符串在加号运算有最高的优先运算,与字符串相加必定是字符串连接运算(concatenation)。所有的其他原始数据类型转为字符串,可以参考ECMAScript标准中的 <a href="/misc/goto?guid=4959733684877760279" rel="nofollow,noindex">ToString</a> 对照表,以下为一些简单的例子:</p> <pre> <code class="language-javascript">> '1' + 123 "1123" > '1' + false "1false" > '1' + null "1null" > '1' + undefined "1undefined"</code></pre> <h3>数字 + 其他的非字符串的原始数据类型</h3> <p>数字与其他类型作相加时,除了字符串会优先使用字符串连接运算(concatenation)的,其他都要依照数字为优先,所以除了字符串之外的其他原始数据类型,都要转换为数字来进行数学的相加运算。如果明白这项规则,就会很容易的得出加法运算的结果。</p> <p>所有转为数字类型可以参考ECMAScript标准中的 <a href="/misc/goto?guid=4959733684962414059" rel="nofollow,noindex">ToNumber</a> 对照表,以下为一些简单的例子:</p> <pre> <code class="language-javascript">> 1 + true //true转为1, false转为0 2 > 1 + null //null转为0 1 > 1 + undefined //null转为NaN NaN</code></pre> <h3>数字/字符串以外的原始数据类型作加法运算</h3> <p>所以,当数字与字符串以外的其他原始数据类型直接使用加号运算时,就是转为数字再运算,这与字符串无关。</p> <pre> <code class="language-javascript">> true + true 2 > true + null 1 > undefined + null NaN</code></pre> <h3>空数组 + 空数组</h3> <pre> <code class="language-javascript">> [] + [] ""</code></pre> <p>两个数组相加,依然按照 valueOf -> toString 的顺序,但因为 valueOf 是数组本身,所以会以 toString 的返回值才是原始数据类型,也就是空字符串,所以这个运算相当于两个空字符串在相加,依照加法运算规则第2步骤,是字符串连接运算(concatenation),两个空字符串连接最后得出一个空字符串。</p> <h3>空对象 + 空对象</h3> <pre> <code class="language-javascript">> {} + {} "[object Object][object Object]"</code></pre> <p>两个空对象相加,依然按照 valueOf -> toString 的顺序,但因为 valueOf 是对象本身,所以会以 toString 的返回值才是原始数据类型,也就是"[object Object]"字符串,所以这个运算相当于两个"[object Object]"字符串在相加,依照加法运算规则第2步骤,是字符串连接运算(concatenation),最后得出一个"object Object"字符串。</p> <p>但是这个结果有异常,上面的结果只是在Chrome浏览器上的结果,怎么说呢?</p> <p>有些浏览器例如Firefox、Edge浏览器会把 {} + {} 直译为相当于 +{} 语句,因为它们会认为以花括号开头( { )的,是一个区块语句的开头,而不是一个对象字面量的语句,所以会认为略过第一个 {} ,把它认为是个 +{} 的语句,也就是相当于强制求出数字值的 Number({}) 运算,相当于 Number("[object Object]") 运算,最后得出的是 NaN 。</p> <p>特别注意: {} + {} 在不同的浏览器有不同结果</p> <p>如果在第一个(前面)的空对象加上圆括号( () ),这样JS就会认为前面是个对象,就可以得出同样的结果:</p> <pre> <code class="language-javascript">> ({}) + {} "[object Object][object Object]"</code></pre> <p>或是分开来先定义对象的变量值,也可以得出同样的结果,像下面这样:</p> <pre> <code class="language-javascript">> let foo = {}, bar = {}; > foo + bar;</code></pre> <p>注: 上面说的行为这与加号运算的第一个(前面)的对象字面值是不是个空对象无关,就算是里面有值的对象字面,例如 {a:1, b:2} ,也是同样的结果。</p> <h3>空对象 + 空数组</h3> <p>上面同样的把 {} 当作区块语句的情况又会发生,不过这次所有的浏览器都会有一致结果,如果 {} (空对象)在前面,而 [] (空数组)在后面时,前面那个会被认为是区块而不是对象。</p> <p>所以 {} + [] 相当于 +[] 语句,也就是相当于强制求出数字值的 Number([]) 运算,相当于 Number("") 运算,最后得出的是 0 数字。</p> <pre> <code class="language-javascript">> {} + [] 0 > [] + {} "[object Object]"</code></pre> <p>特别注意: 所以如果第一个(前面)是 {} 时,后面加上其他的像数组、数字或字符串,这时候加号运算会直接变为一元正号运算,也就是强制转为数字的运算。这是个陷阱要小心。</p> <h3>Date对象</h3> <p>Date对象的 valueOf 与 toString 的两个方法的返回值:</p> <ul> <li> <p>valueOf 方法返回值:</p> </li> <li> <p>toString 方法返回值:</p> </li> </ul> <p>Date对象上面有提及是首选类型为"字符串"的一种异常的对象,这与其他的对象的行为不同(一般对象会先调用 valueOf 再调用 toString ),在进行加号运算时时,它会优先使用 toString 来进行转换,最后必定是字符串连接运算(concatenation),例如以下的结果:</p> <pre> <code class="language-javascript">> 1 + (new Date()) > "1Sun Nov 27 2016 01:09:03 GMT+0800 (CST)"</code></pre> <p>要得出Date对象中的 valueOf 返回值,需要使用一元加号(+),来强制转换它为数字类型,例如以下的代码:</p> <pre> <code class="language-javascript">> +new Date() 1480180751492</code></pre> <h3>Symbols类型</h3> <p>ES6中新加入的Symbols数据类型,它不算是值也不是对象,所以完全不能直接用于加法运算,它并没有内部自动转型的设计,使用时会报错。</p> <h2>总结</h2> <p>{} + {} 的结果是会因浏览器而有不同结果,Chrome中是 [object Object][object Object] 字符串连接,但其它的浏览器则是认为相当于 +{} 运算,得出 NaN 数字类型。</p> <p>{} + {} 的结果是相当于 +[] ,结果是 0 数字类型。</p> <p> </p> <p>来自:https://segmentfault.com/a/1190000008038678</p> <p> </p>