你所忽略的js隱式轉(zhuǎn)換

你有沒有在面試中遇到特別奇葩的js隱形轉(zhuǎn)換的面試題,第一反應(yīng)是怎么會是這樣呢?難以自信,js到底是怎么去計算得到結(jié)果,你是否有深入去了解其原理呢?下面將深入講解其實現(xiàn)原理。

其實這篇文章初稿三個月前就寫好了,在我讀一些源碼庫時,遇到了這些基礎(chǔ)知識,想歸檔整理下,就有了這篇文章。由于一直忙沒時間整理,最近看到了這個比較熱的題,決定把這篇文章整理下。

const a = {
  i: 1,
  toString: function () {
    return a.i++;
  }
}
if (a == 1 && a == 2 && a == 3) {
  console.log('hello world!');
}

網(wǎng)上給出了很多不錯的解析過程,讀了下面內(nèi)容,你將更深入的了解其執(zhí)行過程。

1、js數(shù)據(jù)類型

js中有7種數(shù)據(jù)類型,可以分為兩類:原始類型、對象類型:

基礎(chǔ)類型(原始值):

Undefined、 Null、 String、 Number、 Boolean、 Symbol (es6新出的,本文不討論這種類型)

復雜類型(對象值):

object

2、三種隱式轉(zhuǎn)換類型

js中一個難點就是js隱形轉(zhuǎn)換,因為js在一些操作符下其類型會做一些變化,所以js靈活,同時造成易出錯,并且難以理解。

涉及隱式轉(zhuǎn)換最多的兩個運算符 + 和 ==。

+運算符即可數(shù)字相加,也可以字符串相加。所以轉(zhuǎn)換時很麻煩。== 不同于===,故也存在隱式轉(zhuǎn)換。- * / 這些運算符只會針對number類型,故轉(zhuǎn)換的結(jié)果只能是轉(zhuǎn)換成number類型。

既然要隱式轉(zhuǎn)換,那到底怎么轉(zhuǎn)換呢,應(yīng)該有一套轉(zhuǎn)換規(guī)則,才能追蹤最終轉(zhuǎn)換成什么了。

隱式轉(zhuǎn)換中主要涉及到三種轉(zhuǎn)換:

1、將值轉(zhuǎn)為原始值,ToPrimitive()。

2、將值轉(zhuǎn)為數(shù)字,ToNumber()。

3、將值轉(zhuǎn)為字符串,ToString()。

2.1、通過ToPrimitive將值轉(zhuǎn)換為原始值

js引擎內(nèi)部的抽象操作ToPrimitive有著這樣的簽名:

ToPrimitive(input, PreferredType?)

input是要轉(zhuǎn)換的值,PreferredType是可選參數(shù),可以是Number或String類型。他只是一個轉(zhuǎn)換標志,轉(zhuǎn)化后的結(jié)果并不一定是這個參數(shù)所值的類型,但是轉(zhuǎn)換結(jié)果一定是一個原始值(或者報錯)。

2.1.1、如果PreferredType被標記為Number,則會進行下面的操作流程來轉(zhuǎn)換輸入的值。

1、如果輸入的值已經(jīng)是一個原始值,則直接返回它
2、否則,如果輸入的值是一個對象,則調(diào)用該對象的valueOf()方法,
   如果valueOf()方法的返回值是一個原始值,則返回這個原始值。
3、否則,調(diào)用這個對象的toString()方法,如果toString()方法返回的是一個原始值,則返回這個原始值。
4、否則,拋出TypeError異常。

2.1.2、如果PreferredType被標記為String,則會進行下面的操作流程來轉(zhuǎn)換輸入的值。

1、如果輸入的值已經(jīng)是一個原始值,則直接返回它
2、否則,調(diào)用這個對象的toString()方法,如果toString()方法返回的是一個原始值,則返回這個原始值。
3、否則,如果輸入的值是一個對象,則調(diào)用該對象的valueOf()方法,
   如果valueOf()方法的返回值是一個原始值,則返回這個原始值。
4、否則,拋出TypeError異常。

既然PreferredType是可選參數(shù),那么如果沒有這個參數(shù)時,怎么轉(zhuǎn)換呢?PreferredType的值會按照這樣的規(guī)則來自動設(shè)置:

1、該對象為Date類型,則PreferredType被設(shè)置為String
2、否則,PreferredType被設(shè)置為Number

2.1.3、valueOf方法和toString方法解析

上面主要提及到了valueOf方法和toString方法,那這兩個方法在對象里是否一定存在呢?答案是肯定的。在控制臺輸出Object.prototype,你會發(fā)現(xiàn)其中就有valueOf和toString方法,而Object.prototype是所有對象原型鏈頂層原型,所有對象都會繼承該原型的方法,故任何對象都會有valueOf和toString方法。

先看看對象的valueOf函數(shù),其轉(zhuǎn)換結(jié)果是什么?對于js的常見內(nèi)置對象:Date, Array, Math, Number, Boolean, String, Array, RegExp, Function。

1、除了Number、Boolean、String這三種構(gòu)造函數(shù)生成的基礎(chǔ)值的對象形式,通過valueOf轉(zhuǎn)換后會變成相應(yīng)的原始值。如:

var num = new Number('123');
num.valueOf(); // 123

var str = new String('12df');
str.valueOf(); // '12df'

var bool = new Boolean('fd');
bool.valueOf(); // true

2、Date這種特殊的對象,其原型Date.prototype上內(nèi)置的valueOf函數(shù)將日期轉(zhuǎn)換為日期的毫秒的形式的數(shù)值。

var a = new Date();
a.valueOf(); // 1515143895500

3、除此之外返回的都為this,即對象本身:(有問題歡迎告知)

var a = new Array();
a.valueOf() === a; // true

var b = new Object({});
b.valueOf() === b; // true

再來看看toString函數(shù),其轉(zhuǎn)換結(jié)果是什么?對于js的常見內(nèi)置對象:Date, Array, Math, Number, Boolean, String, Array, RegExp, Function

1、除了Number、Boolean、String、Array、Date、RegExp、Function這幾種構(gòu)造函數(shù)生成的對象,通過toString轉(zhuǎn)換后會變成相應(yīng)的字符串的形式,因為這些構(gòu)造函數(shù)上封裝了自己的toString方法。如:

Number.prototype.hasOwnProperty('toString'); // true
Boolean.prototype.hasOwnProperty('toString'); // true
String.prototype.hasOwnProperty('toString'); // true
Array.prototype.hasOwnProperty('toString'); // true
Date.prototype.hasOwnProperty('toString'); // true
RegExp.prototype.hasOwnProperty('toString'); // true
Function.prototype.hasOwnProperty('toString'); // true

var num = new Number('123sd');
num.toString(); // 'NaN'

var str = new String('12df');
str.toString(); // '12df'

var bool = new Boolean('fd');
bool.toString(); // 'true'

var arr = new Array(1,2);
arr.toString(); // '1,2'

var d = new Date();
d.toString(); // "Wed Oct 11 2017 08:00:00 GMT+0800 (中國標準時間)"

var func = function () {}
func.toString(); // "function () {}"

除這些對象及其實例化對象之外,其他對象返回的都是該對象的類型,(有問題歡迎告知),都是繼承的Object.prototype.toString方法。

var obj = new Object({});
obj.toString(); // "[object Object]"

Math.toString(); // "[object Math]"

從上面valueOf和toString兩個函數(shù)對對象的轉(zhuǎn)換可以看出為什么對于ToPrimitive(input, PreferredType?),PreferredType沒有設(shè)定的時候,除了Date類型,PreferredType被設(shè)置為String,其它的會設(shè)置成Number。

因為valueOf函數(shù)會將Number、String、Boolean基礎(chǔ)類型的對象類型值轉(zhuǎn)換成 基礎(chǔ)類型,Date類型轉(zhuǎn)換為毫秒數(shù),其它的返回對象本身,而toString方法會將所有對象轉(zhuǎn)換為字符串。顯然對于大部分對象轉(zhuǎn)換,valueOf轉(zhuǎn)換更合理些,因為并沒有規(guī)定轉(zhuǎn)換類型,應(yīng)該盡可能保持原有值,而不應(yīng)該想toString方法一樣,一股腦將其轉(zhuǎn)換為字符串。

所以對于沒有指定PreferredType類型時,先進行valueOf方法轉(zhuǎn)換更好,故將PreferredType設(shè)置為Number類型。

而對于Date類型,其進行valueOf轉(zhuǎn)換為毫秒數(shù)的number類型。在進行隱式轉(zhuǎn)換時,沒有指定將其轉(zhuǎn)換為number類型時,將其轉(zhuǎn)換為那么大的number類型的值顯然沒有多大意義。(不管是在+運算符還是==運算符)還不如轉(zhuǎn)換為字符串格式的日期,所以默認Date類型會優(yōu)先進行toString轉(zhuǎn)換。故有以上的規(guī)則:

PreferredType沒有設(shè)置時,Date類型的對象,PreferredType默認設(shè)置為String,其他類型對象PreferredType默認設(shè)置為Number。

2.2、通過ToNumber將值轉(zhuǎn)換為數(shù)字

根據(jù)參數(shù)類型進行下面轉(zhuǎn)換:

參數(shù) 結(jié)果
undefined NaN
null +0
布爾值 true轉(zhuǎn)換1,false轉(zhuǎn)換為+0
數(shù)字 無須轉(zhuǎn)換
字符串 有字符串解析為數(shù)字,例如:‘324’轉(zhuǎn)換為324,‘qwer’轉(zhuǎn)換為NaN
對象(obj) 先進行 ToPrimitive(obj, Number)轉(zhuǎn)換得到原始值,在進行ToNumber轉(zhuǎn)換為數(shù)字

2.3、通過ToString將值轉(zhuǎn)換為字符串

根據(jù)參數(shù)類型進行下面轉(zhuǎn)換:

參數(shù) 結(jié)果
undefined 'undefined'
null 'null'
布爾值 轉(zhuǎn)換為'true' 或 'false'
數(shù)字 數(shù)字轉(zhuǎn)換字符串,比如:1.765轉(zhuǎn)為'1.765'
字符串 無須轉(zhuǎn)換
對象(obj) 先進行 ToPrimitive(obj, String)轉(zhuǎn)換得到原始值,在進行ToString轉(zhuǎn)換為字符串

講了這么多,是不是還不是很清晰,先來看看一個例子:

({} + {}) = ?
兩個對象的值進行+運算符,肯定要先進行隱式轉(zhuǎn)換為原始類型才能進行計算。
1、進行ToPrimitive轉(zhuǎn)換,由于沒有指定PreferredType類型,{}會使默認值為Number,進行ToPrimitive(input, Number)運算。
2、所以會執(zhí)行valueOf方法,({}).valueOf(),返回的還是{}對象,不是原始值。
3、繼續(xù)執(zhí)行toString方法,({}).toString(),返回"[object Object]",是原始值。
故得到最終的結(jié)果,"[object Object]" + "[object Object]" = "[object Object][object Object]"

再來一個指定類型的例子:

2 * {} = ?
1、首先*運算符只能對number類型進行運算,故第一步就是對{}進行ToNumber類型轉(zhuǎn)換。
2、由于{}是對象類型,故先進行原始類型轉(zhuǎn)換,ToPrimitive(input, Number)運算。
3、所以會執(zhí)行valueOf方法,({}).valueOf(),返回的還是{}對象,不是原始值。
4、繼續(xù)執(zhí)行toString方法,({}).toString(),返回"[object Object]",是原始值。
5、轉(zhuǎn)換為原始值后再進行ToNumber運算,"[object Object]"就轉(zhuǎn)換為NaN。
故最終的結(jié)果為 2 * NaN = NaN

3、== 運算符隱式轉(zhuǎn)換

== 運算符的規(guī)則規(guī)律性不是那么強,按照下面流程來執(zhí)行,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 為相等數(shù)值, 返回 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 時返回 true。 否則, 返回 false。
    6*  當 x 和 y 為引用同一對象時返回 true。否則,返回 false。
  
2、若 x 為 null 且 y 為 undefined, 返回 true。
3、若 x 為 undefined 且 y 為 null, 返回 true。
4、若 Type(x) 為 Number 且 Type(y) 為 String,返回比較 x == ToNumber(y) 的結(jié)果。
5、若 Type(x) 為 String 且 Type(y) 為 Number,返回比較 ToNumber(x) == y 的結(jié)果。
6、若 Type(x) 為 Boolean, 返回比較 ToNumber(x) == y 的結(jié)果。
7、若 Type(y) 為 Boolean, 返回比較 x == ToNumber(y) 的結(jié)果。
8、若 Type(x) 為 String 或 Number,且 Type(y) 為 Object,返回比較 x == ToPrimitive(y) 的結(jié)果。
9、若 Type(x) 為 Object 且 Type(y) 為 String 或 Number, 返回比較 ToPrimitive(x) == y 的結(jié)果。
10、返回 false。

上面主要分為兩類,x、y類型相同時,和類型不相同時。

類型相同時,沒有類型轉(zhuǎn)換,主要注意NaN不與任何值相等,包括它自己,即NaN !== NaN。

類型不相同時,

1、x,y 為null、undefined兩者中一個 // 返回true

2、x、y為Number和String類型時,則轉(zhuǎn)換為Number類型比較。

3、有Boolean類型時,Boolean轉(zhuǎn)化為Number類型比較。

4、一個Object類型,一個String或Number類型,將Object類型進行原始轉(zhuǎn)換后,按上面流程進行原始值比較。

3.1、== 例子解析

所以類型不相同時,可以會進行上面幾條的比較,比如:

var a = {
  valueOf: function () {
     return 1;
  },
  toString: function () {
     return '123'
  }
}
true == a // true;
首先,x與y類型不同,x為boolean類型,則進行ToNumber轉(zhuǎn)換為1,為number類型。
接著,x為number,y為object類型,對y進行原始轉(zhuǎn)換,ToPrimitive(a, ?),沒有指定轉(zhuǎn)換類型,默認number類型。
而后,ToPrimitive(a, Number)首先調(diào)用valueOf方法,返回1,得到原始類型1。
最后 1 == 1, 返回true。

我們再看一段很復雜的比較,如下:

[] == !{}
//
1、! 運算符優(yōu)先級高于==,故先進行!運算。
2、!{}運算結(jié)果為false,結(jié)果變成 [] == false比較。
3、根據(jù)上面第7條,等式右邊y = ToNumber(false) = 0。結(jié)果變成 [] == 0。
4、按照上面第9條,比較變成ToPrimitive([]) == 0。
    按照上面規(guī)則進行原始值轉(zhuǎn)換,[]會先調(diào)用valueOf函數(shù),返回this。
   不是原始值,繼續(xù)調(diào)用toString方法,x = [].toString() = ''。
   故結(jié)果為 '' == 0比較。
5、根據(jù)上面第5條,等式左邊x = ToNumber('') = 0。
   所以結(jié)果變?yōu)椋?0 == 0,返回true,比較結(jié)束。

最后我們看看文章開頭說的那道題目:

const a = {
  i: 1,
  toString: function () {
    return a.i++;
  }
}
if (a == 1 && a == 2 && a == 3) {
  console.log('hello world!');
}

1、當執(zhí)行a == 1 && a == 2 && a == 3 時,會從左到右一步一步解析,首先 a == 1,會進行上面第9步轉(zhuǎn)換。ToPrimitive(a, Number) == 1。

2、ToPrimitive(a, Number),按照上面原始類型轉(zhuǎn)換規(guī)則,會先調(diào)用valueOf方法,a的valueOf方法繼承自O(shè)bject.prototype。返回a本身,而非原始類型,故會調(diào)用toString方法。

3、因為toString被重寫,所以會調(diào)用重寫的toString方法,故返回1,注意這里是i++,而不是++i,它會先返回i,在將i+1。故ToPrimitive(a, Number) = 1。也就是1 == 1,此時i = 1 + 1 = 2。

4、執(zhí)行完a == 1返回true,會執(zhí)行a == 2,同理,會調(diào)用ToPrimitive(a, Number),同上先調(diào)用valueOf方法,在調(diào)用toString方法,由于第一步,i = 2此時,ToPrimitive(a, Number) = 2, 也就是2 == 2, 此時i = 2 + 1。

5、同上可以推導 a == 3也返回true。故最終結(jié)果 a == 1 && a == 2 && a == 3返回true

其實了解了以上隱形轉(zhuǎn)換的原理,你有沒有發(fā)現(xiàn)這些隱式轉(zhuǎn)換并沒有想象中那么難。

參考文章:es5文檔

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容