你所忽略的js隱式轉換

你有沒有在面試中遇到特別奇葩的js隱形轉換的面試題,第一反應是怎麼會是這樣呢?難以自信,js究竟是怎麼去計算獲得結果,你是否有深刻去了解其原理呢?下面將深刻講解其實現原理。es6

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

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

網上給出了不少不錯的解析過程,讀了下面內容,你將更深刻的瞭解其執行過程。bash

一、js數據類型

js中有7種數據類型,能夠分爲兩類:原始類型、對象類型:函數

基礎類型(原始值):ui

Undefined、 Null、 StringNumberBooleanSymbol (es6新出的,本文不討論這種類型)
複製代碼

複雜類型(對象值):this

object
複製代碼

二、三種隱式轉換類型

js中一個難點就是js隱形轉換,由於js在一些操做符下其類型會作一些變化,因此js靈活,同時形成易出錯,而且難以理解。es5

涉及隱式轉換最多的兩個運算符 + 和 ==。spa

+運算符便可數字相加,也能夠字符串相加。因此轉換時很麻煩。== 不一樣於===,故也存在隱式轉換。- * / 這些運算符只會針對number類型,故轉換的結果只能是轉換成number類型。prototype

既然要隱式轉換,那到底怎麼轉換呢,應該有一套轉換規則,才能追蹤最終轉換成什麼了。code

隱式轉換中主要涉及到三種轉換:

一、將值轉爲原始值,ToPrimitive()。

二、將值轉爲數字,ToNumber()。

三、將值轉爲字符串,ToString()。

2.一、經過ToPrimitive將值轉換爲原始值

js引擎內部的抽象操做ToPrimitive有着這樣的簽名:

ToPrimitive(input, PreferredType?)

input是要轉換的值,PreferredType是可選參數,能夠是Number或String類型。他只是一個轉換標誌,轉化後的結果並不必定是這個參數所值的類型,可是轉換結果必定是一個原始值(或者報錯)。

2.1.一、若是PreferredType被標記爲Number,則會進行下面的操做流程來轉換輸入的值。

1、若是輸入的值已是一個原始值,則直接返回它
2、不然,若是輸入的值是一個對象,則調用該對象的valueOf()方法,
   若是valueOf()方法的返回值是一個原始值,則返回這個原始值。
3、不然,調用這個對象的toString()方法,若是toString()方法返回的是一個原始值,則返回這個原始值。
4、不然,拋出TypeError異常。
複製代碼

2.1.二、若是PreferredType被標記爲String,則會進行下面的操做流程來轉換輸入的值。

1、若是輸入的值已是一個原始值,則直接返回它
2、不然,調用這個對象的toString()方法,若是toString()方法返回的是一個原始值,則返回這個原始值。
3、不然,若是輸入的值是一個對象,則調用該對象的valueOf()方法,
   若是valueOf()方法的返回值是一個原始值,則返回這個原始值。
4、不然,拋出TypeError異常。
複製代碼

既然PreferredType是可選參數,那麼若是沒有這個參數時,怎麼轉換呢?PreferredType的值會按照這樣的規則來自動設置:

1、該對象爲Date類型,則PreferredType被設置爲String
2、不然,PreferredType被設置爲Number
複製代碼

2.1.三、valueOf方法和toString方法解析

上面主要說起到了valueOf方法和toString方法,那這兩個方法在對象裏是否必定存在呢?答案是確定的。在控制檯輸出Object.prototype,你會發現其中就有valueOf和toString方法,而Object.prototype是全部對象原型鏈頂層原型,全部對象都會繼承該原型的方法,故任何對象都會有valueOf和toString方法。

先看看對象的valueOf函數,其轉換結果是什麼?對於js的常見內置對象:Date, Array, Math, Number, Boolean, String, Array, RegExp, Function

一、Number、Boolean、String這三種構造函數生成的基礎值的對象形式,經過valueOf轉換後會變成相應的原始值。如:

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

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

var bool = new Boolean('fd');
bool.valueOf(); // true
複製代碼

二、Date這種特殊的對象,其原型Date.prototype上內置的valueOf函數將日期轉換爲日期的毫秒的形式的數值。

var a = new Date();
a.valueOf(); // 1515143895500
複製代碼

三、除此以外返回的都爲this,即對象自己:(有問題歡迎告知)

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

var b = new Object({});
b.valueOf() === b; // true
複製代碼

再來看看toString函數,其轉換結果是什麼?對於js的常見內置對象:Date, Array, Math, Number, Boolean, String, Array, RegExp, Function

一、Number、Boolean、String、Array、Date、RegExp、Function這幾種構造函數生成的對象,經過toString轉換後會變成相應的字符串的形式,由於這些構造函數上封裝了本身的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兩個函數對對象的轉換能夠看出爲何對於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.二、經過ToNumber將值轉換爲數字

根據參數類型進行下面轉換:

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

2.三、經過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
複製代碼

三、== 運算符隱式轉換

== 運算符的規則規律性不是那麼強,按照下面流程來執行,es5文檔

比較運算 x==y, 其中 x 和 y 是值,返回 true 或者 false。這樣的比較按以下方式進行:

1、若 Type(x) 與 Type(y) 相同, 則

    1* 若 Type(x) 爲 Undefined, 返回 true2* 若 Type(x) 爲 Null, 返回 true3* 若 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)、返回 false4* 若 Type(x) 爲 String, 則當 x 和 y 爲徹底相同的字符序列(長度相等且相同字符在相同位置)時返回 true。 不然, 返回 false5* 若 Type(x) 爲 Boolean, 當 x 和 y 爲同爲 true 或者同爲 false 時返回 true。 不然, 返回 false6*  當 x 和 y 爲引用同一對象時返回 true。不然,返回 false2、若 x 爲 null 且 y 爲 undefined, 返回 true3、若 x 爲 undefined 且 y 爲 null, 返回 true4、若 Type(x) 爲 Number 且 Type(y) 爲 String,返回比較 x == ToNumber(y) 的結果。
5、若 Type(x) 爲 String 且 Type(y) 爲 Number,返回比較 ToNumber(x) == y 的結果。
6、若 Type(x) 爲 Boolean, 返回比較 ToNumber(x) == y 的結果。
7、若 Type(y) 爲 Boolean, 返回比較 x == ToNumber(y) 的結果。
8、若 Type(x) 爲 StringNumber,且 Type(y) 爲 Object,返回比較 x == ToPrimitive(y) 的結果。
9、若 Type(x) 爲 Object 且 Type(y) 爲 StringNumber, 返回比較 ToPrimitive(x) == y 的結果。
10、返回 false複製代碼

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

類型相同時,沒有類型轉換,主要注意NaN不與任何值相等,包括它本身,即NaN !== NaN。

類型不相同時,

一、x,y 爲null、undefined二者中一個 // 返回true

二、x、y爲Number和String類型時,則轉換爲Number類型比較。

三、有Boolean類型時,Boolean轉化爲Number類型比較。

四、一個Object類型,一個String或Number類型,將Object類型進行原始轉換後,按上面流程進行原始值比較。

3.一、== 例子解析

因此類型不相同時,能夠會進行上面幾條的比較,好比:

var a = {
  valueOf: function () {
     return 1;
  },
  toString: function () {
     return '123'
  }
}
true == a // true;
首先,x與y類型不一樣,x爲boolean類型,則進行ToNumber轉換爲1,爲number類型。
接着,x爲number,y爲object類型,對y進行原始轉換,ToPrimitive(a, ?),沒有指定轉換類型,默認number類型。
然後,ToPrimitive(a, Number)首先調用valueOf方法,返回1,獲得原始類型1。
最後 1 == 1, 返回true複製代碼

咱們再看一段很複雜的比較,以下:

[] == !{}
//
1、! 運算符優先級高於==,故先進行!運算。
2、!{}運算結果爲false,結果變成 [] == false比較。
3、根據上面第7條,等式右邊y = ToNumber(false) = 0。結果變成 [] == 04、按照上面第9條,比較變成ToPrimitive([]) == 0。
    按照上面規則進行原始值轉換,[]會先調用valueOf函數,返回this。
   不是原始值,繼續調用toString方法,x = [].toString() = ''。
   故結果爲 '' == 0比較。
5、根據上面第5條,等式左邊x = ToNumber('') = 0。
   因此結果變爲: 0 == 0,返回true,比較結束。
複製代碼

最後咱們看看文章開頭說的那道題目:

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

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

二、ToPrimitive(a, Number),按照上面原始類型轉換規則,會先調用valueOf方法,a的valueOf方法繼承自Object.prototype。返回a自己,而非原始類型,故會調用toString方法。

三、由於toString被重寫,因此會調用重寫的toString方法,故返回1,注意這裏是i++,而不是++i,它會先返回i,在將i+1。故ToPrimitive(a, Number) = 1。也就是1 == 1,此時i = 1 + 1 = 2。

四、執行完a == 1返回true,會執行a == 2,同理,會調用ToPrimitive(a, Number),同上先調用valueOf方法,在調用toString方法,因爲第一步,i = 2此時,ToPrimitive(a, Number) = 2, 也就是2 == 2, 此時i = 2 + 1。

五、同上能夠推導 a == 3也返回true。故最終結果 a == 1 && a == 2 && a == 3返回true

其實瞭解了以上隱形轉換的原理,你有沒有發現這些隱式轉換並無想象中那麼難。

參考文章:es5文檔

相關文章
相關標籤/搜索