JavaScript系列文章:自動類型轉換-續


上一篇文章中,我們詳細講解了JavaScript中的自動類型轉換,由於篇幅限制,沒能覆蓋到所有的轉換規則,這次准備詳細講解一下。

上次我們提到了對象類型參與運算時轉換規則:

1). 在邏輯環境中執行時,會被轉換為true

2). 在字符串環境和數字環境中,它的valueOf()方法和toString()方法會依次被調用,然后根據返回值進行再次轉換。首先,valueOf()方法會被調用,如果其返回值是基礎類型,則將這個返回值轉為目標類型,如果返回值不是基礎類型,則再試圖調用toString()方法,然后將返回值轉型。如果最終的返回值不是基礎類型,則轉型會拋出一個異常,如果是基礎類型,則會相應的轉為字符串或數字。

接着上次的講,當加號“+”作為一元操作符應用在對象類型上面時,valueOf()和toString()方法,將會有機會被調用,最終返回值會被轉為數字類型,我們因而會得到一個數字或NaN。先來看看valueOf()和toString()的調用順序:

var o = {
valueOf:
function() {
return '3';
},
toString:
function() {
return '5';
}
};

var foo = +o;

console.log(foo);
// 3

可以看到,valueOf()方法被調用,返回了字符串類型的'3',然后被轉為數字類型的3,而toString()方法並沒有被調用,我們再次移除valueOf()方法:

var o = {
toString:
function() {
return '5';
}
};

var foo = +o;

console.log(foo);
// 5

這時候toString()方法就被調用了,根據其返回值'5',對象被成功轉為了數字5。

估計很多初學者都會覺得,如果定義了valueOf()方法,就去調用valueOf()方法,如果沒定義,就去調用toString()方法,其實是不准確的。

實際上,valueOf()方法總會在第一時間被調用,至於toString()方法的調用與否,那得看valueOf()方法的返回值了,我們上面也提到了,如果其返回值是基礎類型,那么toString()方法根本沒有機會被調用,而如果其返回值是引用類型,則會再試圖調用toString()方法得到最終值。

通常,對象原型中的valueOf()方法返回其自身引用,拿上面的例子來講:

var o = {
toString:
function() {
return '5';
}
};

console.log(o.valueOf()
=== o);  // true

我們用了全等(===)操作符來比較其valueOf()返回值和其自身,發現是完全相同的,證明對象原型中的valueOf()的返回值的確是其自身,上面結果等同於下面這段代碼:

// 重寫實例中的valueOf()方法,其返回值是對象自身

var o = {
valueOf:
function() {
return this;
},
toString:
function() {
return '5';
}
};

console.log(o.valueOf()
=== o);  // true

現在我們稍加修改,就可以看出在類型轉換過程中,到底發生了什么:

var o = {};

Object.prototype.valueOf
= function() {

console.log(
'valueOf() called');

return [];
};

Object.prototype.toString
= function() {

console.log(
'toString() called')

return '5';
}

var a = +o;

// output: valueOf() called
//
output: toString() called

console.log(a);
// 5

var b = o + '';

// output: valueOf() called
//
output: toString() called

console.log(b);
// '5'

上面的代碼中,我們改為修改原型方法valueOf()和toString(),分別在方法內部添加了控制台輸出語句,另外,在valueOf()內部我們返回了一個數組對象。在對象參與運算時可以看到,兩個方法依次被調用,不管是數字環境還是字符串環境,都先調用了valueOf()方法,由於返回值不是基礎類型,所以還需再調用toString()方法,得到一個最終的返回值,然后將其轉為目標類型。如果我們將valueOf()中的數組返回值替換為一個基礎類型,toString()方法將不會有機會執行,大家可以親自試一下。

上面也提到,對象原型的valueOf()方法默認是返回對象自身的,實際上,常見對象類型的valueOf()方法都會返回其自身:

var o = {};

var fn = function(){};

var ary = [];

var regex = /./;


o.valueOf()
=== o; // true

fn.valueOf()
=== fn; // true

ary.valueOf()
=== ary; // true

regex.valueOf()
=== regex; // true

不過有個特殊的例外,Date類型的valueOf()會返回一個毫秒數:

var date = new Date(2017, 1, 1);

var time = date.valueOf();

console.log(time);   
// 1485878400000

console.log(time
=== date.getTime()); // true

所以我們就會很容易明白,在Date實例上應用一元加號操作符,是如何返回一個數字的:

var date = new Date(2017, 1, 1);

var time = +date;

console.log(time);
// 1485878400000

不過Date真是個神奇的物種,如果我們直接跟拿它和一個時間毫秒數相加,並不會得到期望的結果:

var date = new Date(2017, 1, 1);

var time = date + 10000;

console.log(time);
// 'Wed Feb 01 2017 00:00:00 GMT+0800 (CST)10000'

它竟然轉為了字符串,然后與數字進行了字符串連接操作!為什么會是這樣的呢?原因在於,對於一元加號操作符運算,目的很明確,就是求正操作,因此引擎調用了其valueOf()方法,返回時間戳數字,而對於后者的二元加號加號操作運算,其存在加法和連接符這樣的二義性,所以引擎可以有選擇地將操作數轉為不同的目標類型,與其他對象不同的是,Date類型更傾向於轉為字符串類型,所以toString()會被先行調用。下面這段話是ECMA規范中關於Date類型轉為基礎類型的描述

大概的意思就是,對象在轉為基礎類型時,通常都會調用toPrimitive(hint)這樣的方法,傳入一個提示參數,指定其目標類型,如果不指定,其他對象的默認值都是number,而Date類型與眾不同,它的默認值是string。

我們上面也提到了,一元加號操作符是求正運算,所以引擎能夠識別並為其指定number目標類型,而二元加號操作符存在二義性,引擎使用了default作為提示參數,Date類型將默認值認為是string,所以我們也理解了上面的例子,即使是Date對象和數字相加,它也不會先調用valueOf()方法得到數字,而是先調用toString()得到一個字符串。

上面講解了這么多,相信大家對於對象類型的轉型規則都熟悉了,那么對於常見的對象,究竟是如何轉為基礎類型的呢?舉個例子:

var foo = +[];            // 0 ( [] -> '' -> 0 )

var foo = +[3]; // 3 ( [3] -> '3' -> 3 )

var foo = +[3, 5]; // NaN ( [3, 5] -> '3,5' -> NaN )

從上面的代碼可以看出,對於數組對象來說,要轉為數字,就要遵循對象類型的轉型規則,因為數組原型的valueOf()方法會返回其自身引用,所以最終會再試圖調用其toString()方法,而它的toString()會返回一個字符串,這個字符串是由逗號分隔的數組元素集,那很容易理解了,對於空數組,必然返回一個空字符串,然后這個空字符串轉型為數字之后就會變為0,而對於非空數組,如果只有一個元素並且元素可以轉為數字,則結果第一個元素對應的數字,如果又多個元素,因為toString()返回的結果中存在逗號,所以無法轉型成功,會返回一個NaN。

但如果我們嘗試數組和一個數字相加,則還是會得到一個字符串的結果:

var foo = [] + 3;          // '3'

var foo = [3] + 3;   // '33'

var foo = [3, 5] + 3; // '3,53'

你也許會說,這不是很像上面的Date類型嗎?是的,結果看上去很相似,但其內部的執行過程還是有差異的,它的valueOf()會先執行,出現上面的結果,是由於valueOf()返回了this,然后再次調用toString()返回了字符串,加號操作符在這里成了字符串連接符了。

類似的還有字面量對象,看下面例子:

var foo = {} + 0;        // '[object Object]0'

var foo = {} + []; // '[object Object]'

var foo = {} + {}; // '[object Object][object Object]'

不過如果是在命令行直接輸入下面表達式,結果會有所出入:

{} + 0;        // 0

{}
+ []; // 0

{}
+ {}; // NaN

其原因是,前面的字面量對象被解釋成了代碼塊,沒有參與運算,只有后面的一部分會返回最終的結果,后面的轉換過程可以參照以上我們講解的內容。

對象的類型轉換規則,就先講到這里,下面來講一下比較操作符中的類型轉換

比較操作符有以下幾種:>, >=, <, <=, ==, ===。除了最后的全等操作符以外,其他幾個在比較不同類型的數據時,均存在值的類型轉換。

對於前四種來說,都遵循着以下規則:

1). 當兩個操作數都為字符串類型時,不進行數據類型轉換,直接比較每個字符

2). 當兩個操作數不同時為字符串時,將操作數轉為數字類型,然后進行比較

3). 如果操作數中存在對象類型,先將對象轉為基礎類型,然后再根據上面兩條進行值的比較。

而對於“==”操作符,則是多了一條特殊的規則:null和undefined在比較時不進行數據轉換,null和自身比較、null和undefined比較都會返回true,和其他值比較都會返回false;undefined和自身比較、undefined和null比較都會返回true,和其他值比較都會返回false。

以下比較操作不存在數據類型轉換:

'3a' < '3b';          // true

'' == '0'; // false

null == undefined; // true

null == 0; // false

undefined
== false; // false

需要注意的是最后兩個個表達式,由於我們在上一篇文章中講到,null值在數字環境下會轉型為0,很多人覺得這個表達式結果為true,但是不要忽略了上面關於null和undefined的規則,這里是不會有類型轉換發生的,同樣的,undefined在比較操作符中也只會識別其自身和null值,並且不會發生數據類型轉換。

在下面幾個表達式中,操作數不全為字符串,所以要將操作數轉為數字后再進行比較:

3 == '3';             // true

3 < '5';  // true

0 == '0';        // true

0 == '';        // true

0 == false;     // true

1 <= true; // true

null >= 0; // true

注意,最后一個表達式中的null,在遇到>、>=、<、<=這幾個操作符時會被轉為數字0的,這與上面的規則有所不同。

最后,對象在參與邏輯運算時,同樣會遵循前面的轉型規則:

var o = {
toString:
function() {
return '3';
},
valueOf:
function() {
return '5';
}
}

o
> 4; // true

特別注意的是,前面我們介紹到,對象在條件語句中是視為true的,但要避免下面這樣的比較:

if ([]) {
// todo
}

if ([] == true) {
// todo
}

第二個條件語句塊是不會執行的,原因在於空的數組對象被轉為數字0了,而true被轉為數字1,比較結果為false,所以里面的代碼永遠無法得到執行,開發時要警惕這樣的寫法。

寫了這么多關於自動類型轉換的內容,大家也可以體會到JS有多么的靈活,想要駕馭好這門語言,不是件容易的事,還需細細體會,好好研究才行。

本文完。

 

參考資料:

http://es5.github.io/#x11.8.5

http://es5.github.io/#x11.9.3

http://www.2ality.com/2012/01/object-plus-object.html

http://www.2ality.com/2013/04/quirk-implicit-conversion.html

https://www.united-coders.com/matthias-reuter/all-about-types-part-2/

http://www.adequatelygood.com/Object-to-Primitive-Conversions-in-JavaScript.html


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
  © 2014-2022 ITdaan.com 联系我们: