js 1+'2' == '1'+'2'
前言
非常深入地講解了包含隱式轉換時js計算過程,全篇幹貨。本文由@keenjaan授權分享。
本文由@仙人掌推薦分享
正文從這裏開始
你有沒有在面試中遇到特別奇葩的js隱形轉換的面試題,第一反應是怎麽會是這樣呢?難以自信,js到底是怎麽去計算得到結果,你是否有深入去了解其原理呢?下面將深入講解其實現原理。
其實這篇文章初稿三個月前就寫好了,在我讀一些源碼庫時,遇到了這些基礎知識,想歸檔整理下,就有了這篇文章。由於一直忙沒時間整理,最近看到了這個比較熱的題,決定把這篇文章整理下。
const a = {
i: 1,
toString: function () {
return a.i++;
}
}
if (a == 1 && a == 2 && a == 3) {
console.log(‘hello world!‘);
}
網上給出了很多不錯的解析過程,讀了下面內容,你將更深入的了解其執行過程。
1、js數據類型
js中有7種數據類型,可以分為兩類:原始類型、對象類型:
基礎類型(原始值):
Undefined、 Null、 String、 Number、 Boolean、 Symbol (es6新出的,本文不討論這種類型)
復雜類型(對象值):
object
2、三種隱式轉換類型
js中一個難點就是js隱形轉換,因為js在一些操作符下其類型會做一些變化,所以js靈活,同時造成易出錯,並且難以理解。
涉及隱式轉換最多的兩個運算符 + 和 ==。
+運算符即可數字相加,也可以字符串相加。所以轉換時很麻煩。== 不同於===,故也存在隱式轉換。- * / 這些運算符只會針對number類型,故轉換的結果只能是轉換成number類型。
既然要隱式轉換,那到底怎麽轉換呢,應該有一套轉換規則,才能追蹤最終轉換成什麽了。
隱式轉換中主要涉及到三種轉換:
1、將值轉為原始值,ToPrimitive()。
2、將值轉為數字,ToNumber()。
3、將值轉為字符串,ToString()。
2.1、通過ToPrimitive將值轉換為原始值
js引擎內部的抽象操作ToPrimitive有著這樣的簽名:
ToPrimitive(input, PreferredType?)
input是要轉換的值,PreferredType是可選參數,可以是Number或String類型。他只是一個轉換標誌,轉化後的結果並不一定是這個參數所值的類型,但是轉換結果一定是一個原始值(或者報錯)。
2.1.1、如果PreferredType被標記為Number,則會進行下面的操作流程來轉換輸入的值。
1、如果輸入的值已經是一個原始值,則直接返回它
2、否則,如果輸入的值是一個對象,則調用該對象的valueOf()方法,
如果valueOf()方法的返回值是一個原始值,則返回這個原始值。
3、否則,調用這個對象的toString()方法,如果toString()方法返回的是一個原始值,則返回這個原始值。
4、否則,拋出TypeError異常。
2.1.2、如果PreferredType被標記為String,則會進行下面的操作流程來轉換輸入的值。
1、如果輸入的值已經是一個原始值,則直接返回它
2、否則,調用這個對象的toString()方法,如果toString()方法返回的是一個原始值,則返回這個原始值。
3、否則,如果輸入的值是一個對象,則調用該對象的valueOf()方法,
如果valueOf()方法的返回值是一個原始值,則返回這個原始值。
4、否則,拋出TypeError異常。
既然PreferredType是可選參數,那麽如果沒有這個參數時,怎麽轉換呢?PreferredType的值會按照這樣的規則來自動設置:
1、該對象為Date類型,則PreferredType被設置為String2、否則,PreferredType被設置為Number
2.1.3、valueOf方法和toString方法解析
上面主要提及到了valueOf方法和toString方法,那這兩個方法在對象裏是否一定存在呢?答案是肯定的。在控制臺輸出Object.prototype,你會發現其中就有valueOf和toString方法,而Object.prototype是所有對象原型鏈頂層原型,所有對象都會繼承該原型的方法,故任何對象都會有valueOf和toString方法。
先看看對象的valueOf函數,其轉換結果是什麽?對於js的常見內置對象:Date, Array, Math, Number, Boolean, String, Array, RegExp, Function
。
1、Number、Boolean、String這三種構造函數生成的基礎值的對象形式,通過valueOf轉換後會變成相應的原始值。如:
var num = newNumber(‘123‘);
num.valueOf(); // 123var str = newString(‘12df‘);
str.valueOf(); // ‘12df‘var bool = newBoolean(‘fd‘);
bool.valueOf(); // true
2、Date這種特殊的對象,其原型Date.prototype上內置的valueOf函數將日期轉換為日期的毫秒的形式的數值。
var a = newDate();
a.valueOf(); // 1515143895500
3、除此之外返回的都為this,即對象本身:(有問題歡迎告知)
var a = newArray();
a.valueOf() === a; // truevar b = newObject({});
b.valueOf() === b; // true
再來看看toString函數,其轉換結果是什麽?對於js的常見內置對象:Date, Array, Math, Number, Boolean, String, Array, RegExp, Function
。
1、Number、Boolean、String、Array、Date、RegExp、Function這幾種構造函數生成的對象,通過toString轉換後會變成相應的字符串的形式,因為這些構造函數上封裝了自己的toString方法。如:
Number.prototype.hasOwnProperty(‘toString‘); // trueBoolean.prototype.hasOwnProperty(‘toString‘); // trueString.prototype.hasOwnProperty(‘toString‘); // trueArray.prototype.hasOwnProperty(‘toString‘); // trueDate.prototype.hasOwnProperty(‘toString‘); // trueRegExp.prototype.hasOwnProperty(‘toString‘); // trueFunction.prototype.hasOwnProperty(‘toString‘); // truevar num = newNumber(‘123sd‘);
num.toString(); // ‘NaN‘var str = newString(‘12df‘);
str.toString(); // ‘12df‘var bool = newBoolean(‘fd‘);
bool.toString(); // ‘true‘var arr = newArray(1,2);
arr.toString(); // ‘1,2‘var d = newDate();
d.toString(); // "Wed Oct 11 2017 08:00:00 GMT+0800 (中國標準時間)"var func = function () {}
func.toString(); // "function () {}"
除這些對象及其實例化對象之外,其他對象返回的都是該對象的類型,(有問題歡迎告知),都是繼承的Object.prototype.toString方法。
var obj = newObject({});
obj.toString(); // "[object Object]"Math.toString(); // "[object Math]"
從上面valueOf和toString兩個函數對對象的轉換可以看出為什麽對於ToPrimitive(input, PreferredType?),PreferredType沒有設定的時候,除了Date類型,PreferredType被設置為String,其它的會設置成Number。
因為valueOf函數會將Number、String、Boolean基礎類型的對象類型值轉換成 基礎類型,Date類型轉換為毫秒數,其它的返回對象本身,而toString方法會將所有對象轉換為字符串。顯然對於大部分對象轉換,valueOf轉換更合理些,因為並沒有規定轉換類型,應該盡可能保持原有值,而不應該想toString方法一樣,一股腦將其轉換為字符串。
所以對於沒有指定PreferredType類型時,先進行valueOf方法轉換更好,故將PreferredType設置為Number類型。
而對於Date類型,其進行valueOf轉換為毫秒數的number類型。在進行隱式轉換時,沒有指定將其轉換為number類型時,將其轉換為那麽大的number類型的值顯然沒有多大意義。(不管是在+運算符還是==運算符)還不如轉換為字符串格式的日期,所以默認Date類型會優先進行toString轉換。故有以上的規則:
PreferredType沒有設置時,Date類型的對象,PreferredType默認設置為String,其他類型對象PreferredType默認設置為Number。
2.2、通過ToNumber將值轉換為數字
根據參數類型進行下面轉換:
參數結果undefinedNaNnull+0布爾值true轉換1,false轉換為+0數字無須轉換字符串有字符串解析為數字,例如:‘324’轉換為324,‘qwer’轉換為NaN對象(obj)先進行 ToPrimitive(obj, Number)轉換得到原始值,在進行ToNumber轉換為數字
2.3、通過ToString將值轉換為字符串
根據參數類型進行下面轉換:
參數結果undefined’undefined’null’null’布爾值轉換為’true’ 或 ‘false’數字數字轉換字符串,比如:1.765轉為’1.765’字符串無須轉換對象(obj)先進行 ToPrimitive(obj, String)轉換得到原始值,在進行ToString轉換為字符串
講了這麽多,是不是還不是很清晰,先來看看一個例子:
({} + {}) = ?
兩個對象的值進行+運算符,肯定要先進行隱式轉換為原始類型才能進行計算。
1、進行ToPrimitive轉換,由於沒有指定PreferredType類型,{}會使默認值為Number,進行ToPrimitive(input, Number)運算。
2、所以會執行valueOf方法,({}).valueOf(),返回的還是{}對象,不是原始值。
3、繼續執行toString方法,({}).toString(),返回"[object Object]",是原始值。
故得到最終的結果,"[object Object]" + "[object Object]" = "[object Object][object Object]"
再來一個指定類型的例子:
2 * {} = ?
1、首先*運算符只能對number類型進行運算,故第一步就是對{}進行ToNumber類型轉換。
2、由於{}是對象類型,故先進行原始類型轉換,ToPrimitive(input, Number)運算。
3、所以會執行valueOf方法,({}).valueOf(),返回的還是{}對象,不是原始值。
4、繼續執行toString方法,({}).toString(),返回"[object Object]",是原始值。
5、轉換為原始值後再進行ToNumber運算,"[object Object]"就轉換為NaN。
故最終的結果為 2 * NaN = NaN
3、== 運算符隱式轉換
== 運算符的規則規律性不是那麽強,按照下面流程來執行,es5文檔
比較運算 x==y, 其中 x 和 y 是值,返回 true 或者 false。這樣的比較按如下方式進行:
1、若 Type(x) 與 Type(y) 相同, 則
1* 若 Type(x) 為 Undefined, 返回 true。
2* 若 Type(x) 為 Null, 返回 true。
3* 若 Type(x) 為 Number, 則
(1)、若 x 為 NaN, 返回 false。
(2)、若 y 為 NaN, 返回 false。
(3)、若 x 與 y 為相等數值, 返回 true。
(4)、若 x 為 +0 且 y 為 ?0, 返回 true。
(5)、若 x 為 ?0 且 y 為 +0, 返回 true。
(6)、返回 false。
4* 若 Type(x) 為 String, 則當 x 和 y 為完全相同的字符序列(長度相等且相同字符在相同位置)時返回 true。 否則, 返回 false。
5* 若 Type(x) 為 Boolean, 當 x 和 y 為同為 true 或者同為 false