33 個 JavaScript 核心概念系列(三): 顯式 (名義) 與 隱式 (鴨子)型別轉換

路某人發表於2019-01-05

原文地址:落明的部落格

顯示與隱式型別轉換

一、 前言

說實話,JavaScript 的型別轉換是個相當頭疼的問題,無論是對於初學者還是有經驗的老司機。它的難處並不在於概念多難理解,而是情況多且雜,看似相同的情況結果卻又出人意料,很少有人能保證時刻都能做出正確的判斷。

因此,這篇文章希望能講的足夠細緻和明確,讓大家能夠在日常使用中,能夠儘快的搞清楚型別轉換的順序和結果。

長文預警,建議先 mark, 分多次檢視。

二、型別轉換

1. 什麼叫型別轉換?

我們知道,JavaScript 中存在七種資料型別,在必要的時候,我們會對不同型別的值進行相互間的轉換。比如說,在進行條件判斷時,我們需要將其他型別的值轉為布林型別值,在使用 console.log() 列印內容時,需要將其轉為字串輸出。

2. JavaScript 中的型別轉換方式有哪些?

在 JavaScript 中,分為顯式型別轉換和隱式型別轉換。

其中,顯式型別轉換是我們為了功能需要,人為的將一種型別的值轉換為另一中型別,轉換的時機和結果都是我們預期的;而隱式型別轉換則是 JavaScript 在程式碼執行時,未經我們允許而進行的強制型別轉換。

三、顯式型別轉換

1. 其他型別轉換為字串( ToString )

值型別 例子 轉換後 呼叫法則
number 34 '34' String(34)
boolean true 'true' String(true)
boolean false 'false' String(false)
undefiend undefined 'undefined' String(undefined)
null null 'null' String(null)
object { a: 'fa' } "[object Object]" String({a: 'fa'})
object new String(45) '45' String(new String(45))
object [1, 2] '1,2' String([1,2])
object function() {var d;} "function() { var d; }" String(function() {var d;})

其他型別的值轉換為字串,是通過呼叫原生函式String()實現,但不同型別值的實現卻有明顯的差異。

對於基本型別的值,直接將其轉化為值的字串形式。而對於物件型別來說,便有些複雜了。

首先,每個物件內部都有一個 [[Class]] 屬性,我們通過Object.prototype.toString() 方法可以得到這個屬性的字串值。

對於物件(如{ a: 'ff'; })而言,除非自己定義 toString() 方法,否則,呼叫 String() 方法將返回和呼叫 Object.prototype.toString() 相同的值。(如 : "[object Object]")。

const obj_1 = {
  b: 'lalala'
};
const obj_2 = {
  toString() {
    return "fasfa";
  }
};
String(obj_1); // '[object Object]'
String(obj_2); // 'fasfa'
複製程式碼

其次, JavaScript 中,除了普通物件,還有以下幾種:

  1. 封裝物件

    對於基本型別值 stringnumberboolean 是沒有 .lengthtoString() 方法的,因此,JavaScript 提供了內建函式 String()Number()Boolean() ,通過 new 呼叫後會將基本型別值封裝為一個物件。

    如果想要取到封裝物件中的基本型別值,可以使用 valueOf() 方法。

    // string 型別
    const a = 'i am string';
    typeof a; //  'string'
    // string 封裝物件
    const b = new String('i am sringObject');
    typeof b; // 'object'
    // 拆封
    b.valueOf(); // i am sringObject
    複製程式碼

    那對於封裝物件,String() 會返回什麼值呢?

    事實上,封裝物件對於 toString() 方法進行了封裝,因此,對封裝物件呼叫 String() 方法,將會返回封裝物件呼叫toString() 方法返回的值。

    const numObj = new Number(false); // Number {0}
    numObj.toString(); // '0'
    String(numObj); // '0'
    複製程式碼
  2. 函式

    對於函式來說,它也包裝了自己的 toString()方法,因此,呼叫 String() 方法時將返回函式字串化後的值。

    function bar() {
      console.log('bar');
    }
    String(bar); // "function bar() {↵  console.log('bar');↵}"
    bar.toString(); // "function bar() {↵  console.log('bar');↵}"
    Object.prototype.toString.call(bar); // "[object Function]"
    複製程式碼

    從上例可以看到,String()toString() 方法呼叫的是函式自己封裝的toSring(),如果呼叫物件的 toString() 方法,則函式與普通物件一樣,返回的是函式物件內部的 [[Class]] 屬性。

  3. 陣列

    陣列同函式一樣,同樣包裝了自己的 toString() 方法。此方法會將陣列中的每一項用逗號連線成一個字串。

    const arr = [1,4,6];
    String(arr); // "1,4,6"
    arr.toString(); // "1,4,6"
    Object.prototype.toString.call(arr); // "[object Array]"
    複製程式碼

2. 其他型別值轉為數字( ToNumber )

同樣,先感受一下什麼叫絕望?~~

值型別 例子 轉換後 呼叫法則
string '34' 34 Number('34')
string '' 0 Number('')
string '34fad' NaN Number('34fad')
string '34fad'、'34.24'、'34' 34 parseInt('34fad')
string '34fad'、'34' 34 parseFloat(值)
string '34.34' 34.34 parseFloat(值)
boolean true 1 Number(true)
boolean false 0 Number(false)
undefiend undefined NaN Number(undefined)
null null 0 Number(null)
object { a: 'fa' } NaN Number({a: 'fa'})
object new String('fff') NaN Number(new String('fff'))
object [] 0 Number([])
object [1, 2] NaN Number([1,2])
object function() {var d;} NaN Number(function() {var d;})

看完一臉懵逼有沒有?!哈哈,不用害怕,乍看上去,大概會覺得異常混亂,其實稍加整理,不外乎以下幾種情況:

  1. 轉換後值為 NaN

    數字與字串不同,並不是任何型別值都能轉為數字,因此,就會有 NaN,意思就是 not a number

    諸如包含非數字的字串、undefined、非空陣列,部分物件,都是我們知道無法轉化為一個數字的。

  2. boolean 型別值

    對於 truefalsetrue 轉換為 1,false 轉為 0。

  3. 帶有數字的字串

    從上面我們可以看到,對於帶有數字的字串,有三種方法進行轉換,但規則不同。

    • Number() 方法會對字串整體進行轉換, 它會先判斷這個字串是否是個正確的數字字串,如果不是,則會返回 NaN

    • parseInt() 方法則會對字串從左往右依次解析,直到遇到第一個非數字字元(包括小數點),如果最左邊的字元是非數字字元,則返回 NaN

    • parseFloat() 方法解析順序同 parseInt() 相同,不同的是它遇到第一個小數點時會正常往右繼續解析,直至遇到非數字字元停止。

    其實嚴格來講,只有 Number() 方法是進行轉換操作,而後兩者屬於將字串解析 為數字,但為了講解方便,我將它們放在一起講述。

  4. 物件

    對於物件而言,會先將物件轉為基本型別值( ToPrimitive ),再對基本型別值呼叫 Number() 方法。

    那如何將物件轉為基本型別值?首先會呼叫物件的 valueOf() 方法,如果沒有此方法或者此方法返回值不是基本型別值,則會呼叫toString() 方法,如果 toString() 方法不存在或者返回值也不是基本型別值,會產生 TypeError 錯誤。

    // 普通物件
    const nomalObj = {
      a: '56'
    };
    nomalObj .valueOf(); // { a: '56'}
    nomalObj.toString(); // "[object Object]"
    // Number(nomalObj) 相當於Number("[object Object]") 
    Number("[object Object]"); // NaN
    Number(nomalObj); // NaN
    
    // valueOf() 返回基本型別值的物件
    const obj_1 = {
      a: '56',
      valueOf: function() {
        return '23';
      }  
    };
    obj_1.valueOf(); // '23'
    // Number(obj_1) 相當於 Number('23');
    Number('23'); // 23
    Number(obj_1); // 23
    
    // valueOf() 返回非基本型別值,toString() 返回基本型別值的物件
    const obj_2 = {
      a: '56',
      valueOf: function() {
        return {b: 34}
      },
      toString: function() {
        return false;
      }
    };
    obj_2.valueOf(); // {b: 34}
    obj_2.toString(); // false
    // Number(obj_2) 相當於 Number(false)
    Number(obj_2); // 0
    Number(false); // 0
    複製程式碼

    上面的規則,適用於我們所說的所有物件,比如陣列,封裝物件和函式。

3. 其他型別轉換為 boolean 值( ToBoolean )

我們可以通過 Boolean()方法 或!!運算子來顯式的將一個值轉換為布林值。

相對來說,判斷一個值是 true 還是 false 則比較容易,我們只需要記住以下幾種值會轉換為 false,而其他值,均為 true

  • undefined
  • null
  • false
  • +0、-0 和 NaN
  • ""

當我們看到 []、{} 甚至是 "''" 時,也一定要記住,它們是真值。


Boolean(false); // fasle
Boolean([]); //true
Boolean({}); //true
Boolean(''); // false
Boolean('""'); // true
Boolean('false'); // true
複製程式碼

四、隱式強制型別轉換

除了進行強制型別轉換,JavaScript 會在執行時根據需要,自動進行型別的轉換,儘管這個特點飽受爭議,但不得不承認,某些情況下我們仍舊更喜歡使用某些隱式轉換規則。

一旦某些隱式的規則被接受並廣泛使用,從某種意義上來講,這些規則便同顯式轉換一樣。

1. 奇怪的 +

先看一一個最常見的例子:

const a = 5;
const b = '6';
console.log(a+a); // 10
console.log(a+b); // '56'
console.log(b+b); // '66'
複製程式碼

之所以會產生上例中的狀況,原因就在於在JavaScript 中,+ 運算子既可以作用於number 型別值,也可以作用於 string 型別值。前者進行數字相加,後者則進行字串的拼接。

這就是為什麼5 + 5 = 10'6' + '6' = '66'。而當 + 號兩邊既有數字也有字串時,則會隱式的將數字轉換為字串,然後進行字串的拼接。

那兩邊沒有字串的情況呢?比如:

const a = [1,4];
const b = [2,3];
const c = 4;
console.log(a+c); // '1,44'
console.log(a+b); // '1,42,3'
複製程式碼

為什麼會這樣?原來只要+ 的其中一個運算元可以通過某種方式(toPrimitive)轉換為字串,就會進行字串的拼接。

我們知道,陣列[1,4] 可以通過 toString() 方法返回字串 '1,4',因此,[1,4] + 4 就相當於 '1,4' + 4

因為這個特性,我們在想將一個數字 a 轉換為字串時,便可以直接使用 a + '' 的形式即可。相對於顯式使用String(a),隱式轉換則更加簡潔。

從陣列的例子我們可以看到,除了數字,其他型別的值也可以通過 + ' ' 的形式轉化為字串。

const a = {b: '2'}
console.log( a+ ''); // "[object Object]"
複製程式碼

但有一點需要注意,對於物件而言,使用 String() 方法是直接取這個物件 toString() 方法的返回值,而 + ' ' ,則會對這個物件呼叫 valueOf 反法,然後對 valueOf 的返回值呼叫 toString(),將其轉換為字串。

const a = {
  toString: function() { return 45 },
  valueOf: function() { return 4}
 };
String(a); // '45'
a + ' '; //  // '4'
複製程式碼

好在除非我們特意去改變一個物件的 valueOf 及 'toString()' 方法,通過上述兩個方式的轉換後的結果都是一致的。

2. 有用的 -

+ 號不同的是,- 號只能用於數字的相減,對於它兩邊的運算元,都會經過隱式型別轉換轉為數字。

const a = '34';
const b = '4';
console.log(a - b); // 30
const c = 'dd';
console.log(a - c); // NaN
const d = [4];
console.log(a - d); // 30
複製程式碼

根據上例,我們可看到,如果 - 號兩邊是字串,則會將他們強制轉換為數字,如果 - 兩邊不是字串,則會先將其轉為字串,再將這個字串轉為數字。

3. 隱式轉換為布林值

將其他型別值隱式轉換為布林值是我們最常用的一種轉換。因為程式的編寫實質上就是不停的進行判斷。

在以下場景中,都是進行判斷,而只要傳入的值不是布林值,都會通過隱式型別轉換轉為布林值。

  1. if (..) {} 語句中的條件判斷表示式。
  2. for ( .. ; .. ; ..) 語句中的條件判斷表示式。
  3. while (..)do ... while ( ..) 中的條件判斷表示式。
  4. ? : 中的條件判斷表示式。
  5. 邏輯或 || 或邏輯與 && 左邊的運算元。

在這些情況下,都將會進行其他型別值到布林型別值的隱式轉換,規則同顯式呼叫 Boolean()

五、最後

上面就是不同資料型別直接顯式或隱式的轉換規則,我們不需要將每一種情況都牢記在心,但有必要對他們進行充分的瞭解,這可以保證我們在實際寫程式碼時避免不少奇怪又難以排查的 bug 。

相關文章