1. 程式人生 > 前端設計 >資料型別轉換集中營

資料型別轉換集中營

在寫程式碼的過程中經常需要考慮到型別轉換,所以抽空總結出理論知識點,並把平時碰到的和現在能想到的情況列舉出來。

此篇文章包含:

  • 基本型別
  • ToPrimitive轉換機制
  • 重寫toString和valueOf
  • Symbol.toPrimitive
  • 使用==比較時的型別轉換
  • +、-、*、/、%的型別轉換
  • 幾道大廠的面試題

下面對於看到的知識進行總結和抽離

首先得知道基本型別和ToPrimitive

基本型別:

  • Number
  • String
  • Boolean
  • null
  • undefinded
  • Symbool
  • BigInt

說到null、undefinded,想起寫程式碼過程中的使用場景,比如定義變數,判斷變數

引用型別

  • 物件Object (複雜資料型別)
  • 函式Function
  • 日期Date
  • 陣列Array

ToPrimitive函式語法:ToPrimitive(input,PreferredType?)

物件轉原始值

Number和String都可以用toPrimitive的原理來思考,看到這個想到如果是Boolean呢,進一步思考得到布林值不會轉換成字串或者數字,所以考慮的僅僅是Number和String?

valueOf()和toString()

引用型別執行valueOf()方法,除了日期型別,其它情況都是返回它本身

誰可以呼叫toString()?

  • 除了null、undefined的其它基本資料型別還有物件都可以呼叫它,通常情況下它的返回結果和String一樣。
  • 在使用一個數字呼叫toString()的時候會報錯,除非這個數字是一個小數或者是用了一個變數來盛放這個數字然後呼叫。(1.1.toString()或者var a = 1; a.toString();)

Object.prototype.toString.call()是做什麼用的?

  • 返回某個資料的內部屬性[[class]],能夠幫助我們準確的判斷出某個資料型別
  • 比typeof判斷資料型別更加的準確

不同資料型別呼叫toString()

  • 原始資料型別呼叫時,把它的原始值換成了字串
  • 陣列的toString方法是將每一項轉換為字串然後再用","連線
  • 普通的物件(比如{name: 'obj'}這種)轉為字串都會變為"[object Object]"
  • 函式(class)、正則會被轉為原始碼字串
  • 日期會被轉為本地時區的日期字串
  • 原始值的包裝物件呼叫toString會返回原始值的字串
  • 擁有Symbol.toStringTag內建屬性的物件在呼叫時會變為對應的標籤"[object Map]"

型別轉換

1. 轉字串

先上幾道易錯題:

console.log(String(NaN))   //'NaN'
console.log(String(null))  //'null'
console.log(String(undefinded)) // 'undefinded'
console.log(String([])) // ''
console.log(String('10n'))  //'10n'
複製程式碼
  • 1.1 基本型別轉字串

1)Undefined :"undefined"

2)Null:"null"

3)Boolean:如果引數是 true,返回 "true"。引數為 false,返回 "false"

4)String:返回與之相等的值

5)Symbol:"Symbol()"

6)Number:

console.log(String(0)) // '0'
console.log(String(1)) // '1'
console.log(String(100)) // '100'
console.log(String(NaN)) // 'NaN'
console.log(String(10n)) // '10'
console.log(String(10n) === '10') // true
複製程式碼
  • 1.2 引用型別轉字串

1)陣列:不為空,得到以逗號隔開的字串;為空,得到空字串''

為什麼用逗號隔開,裡面隱式呼叫了join方法

所以需要考慮數組裡面子元素是什麼?根據各自元素型別的規則轉換

console.log(String([true,false])) // "true,false"
console.log(String([NaN,1])) // "NaN,1"

console.log(String([function () {},1])) // "function () {},1"

console.log(String([{ name: 'obj' },{ name: 'obj2' }])) 
// "[object Object],[object Object]"
裡面是物件,把物件轉化為字串
複製程式碼

2) 物件:也就是呼叫String()函式,總結幾點: - 如果物件具有 toString方法,則呼叫這個方法。如果他返回一個原始值,JavaScript 將這個值轉換為字串,並返回這個字串結果。 - 如果物件沒有 toString方法(找出??),或者這個方法並不返回一個原始值,那麼 JavaScript 會呼叫valueOf方法。如果存在這個方法,則JavaScript呼叫它。如果返回值是原始值,JS將這個值轉換為字串,並返回這個字串的結果。 - 否則,JavaScript無法從toString或者valueOf獲得一個原始值,這時它將丟擲一個型別錯誤異常。

其實走的是toPrimitive(object,'string')
複製程式碼

3)日期:轉化為本地地區的字串

console.log(String(new Date())) // Sat Mar 28 2020 23:49:45 GMT+0800 (中國標準時間)
console.log(String(new Date('2020/12/09'))) // Wed Dec 09 2020 00:00:00 GMT+0800 (中國標準時間)

複製程式碼

只有空陣列轉化成字串為空,其他都是直接輸出本身,例子中列舉了平時特殊的例項子

4) 函式轉字串 輸出原始碼

2. 轉布林值

  • 2.1 數字轉布林值

只需要記住:除了0,-0,NaN這三種轉換為false,其他的一律為true。

console.log(Boolean(0)) // false
console.log(Boolean(-0)) // false
console.log(Boolean(NaN)) // false

console.log(Boolean(1)) // true
console.log(Boolean(Infinity)) // true
console.log(Boolean(-Infinity)) // true
console.log(Boolean(100n)) // true
console.log(Boolean(BigInt(100))) // true
複製程式碼
  • 2.2 字串轉布林值

只需要記住:除了空字串""都為true。

  • 2.3 引用型別轉布林值

只需要記住:物件,陣列,類陣列,日期,正則都為true

  • 2.4 其他值轉布林值

undefined、null 都為false

-document.all是一個例外,它在非IE下用typeof檢測型別為undefined,所以會被轉為false。(考的不多)

console.log(Boolean(null))  //false
console.log(Boolean(undefined)) // false
複製程式碼

總之:初false、undefined、null、+0、-0、NaN、""、document.all之外,其他都是true

3. 轉數字

  • 3.1 基本型別轉數字

1)Undefined :NaN

2)Null:+0

3)Boolean:如果引數是 true,返回 1。引數為 false,返回 +0

4)String:純數字的字串(包括小數和負數、各進位制的數),會被轉為相應的數字,否則為NaN

5)Symbol:使用Number()轉會報錯

6)Number:返回與之相等的值

console.log(Number("1")) // 1
console.log(Number("1.1")) // 1.1
console.log(Number("-1")) // -1
console.log(Number("0x12")) // 18
console.log(Number("0012")) // 12

console.log(Number(null)) // 0
console.log(Number("1a")) // NaN
console.log(Number("NaN")) // NaN
console.log(Number(undefined)) // NaN
console.log(Number(Symbol(1))) // TypeError: Cannot convert a Symbol value to a number

複製程式碼
  • 3.2 其他轉數字的方法

1) parsetInt,將結果轉換為整數

2) parseFloat,將結果轉換為整數或者浮點數

它們在轉換為數字的時候是有這麼幾個特點的:

如果字串以0x或者0X開頭的話,parseInt會以十六進位制數轉換規則將其轉換為十進位制,而parseFloat會解析為0

它們兩在解析的時候都會跳過開頭任意數量的空格,往後執行

執行過程中會盡可能多的解析數值字元,如果碰到不能解析的字元則會跳出解析忽略後面的內容

如果第一個不是非空格,或者開頭不是0x、-的數字字面量,將最終返回NaN

console.log(parseInt('10')) // 10
console.log(parseFloat('1.23')) // 1.23

console.log(parseInt("0x11")) // 17
console.log(parseFloat("0x11")) // 0

console.log(parseInt("  11")) // 11
console.log(parseFloat("  11")) // 11

console.log(parseInt("1.23a12")) // 1
console.log(parseFloat("1.23a12")) // 1.23

console.log(parseInt("  11")) // 11
console.log(parseFloat("  11")) // 11

console.log(parseInt("1a12")) // 1
console.log(parseFloat("1.23a12")) // 1.23

console.log(parseInt("-1a12")) // -1
console.log(parseFloat(".23")) // 0.23

複製程式碼
  • 3.3 引用型別轉數字

    1)物件轉數字: 物件轉數字就是toPrimitive(object,'number'),先呼叫valueOf()後呼叫toString()

    2)陣列轉數字:同對象

    3)日期轉數字:返回毫秒數

console.log(Number(new Date())) // 1585413652137
複製程式碼

總之:

console.log(Number(NaN))   //NaN
console.log(Number(null))  //0
console.log(Number(undefined)) // NaN
console.log(Number({})) // NaN
console.log(Number([])) // 0
console.log(Number([0])) // 0
console.log(Number([1,2])) // NaN
複製程式碼

4. 轉物件

  • 1.1 基本型別轉物件

  • String、Number、Boolean有兩種用法,配合new使用和不配合new使用,但是ES6規範不建議使用new來建立基本型別的包裝類。

  • 現在更加推薦用new Object()來建立或轉換為一個基本型別的包裝類。

基本型別的包裝物件的特點:

  • 使用typeof檢測它,結果是object,說明它是一個物件
  • 使用toString()呼叫的時候返回的是原始值的字串(題6.8.3中會提到)

重寫toString或者valueOf

為什麼會想到重寫toString或者valueOf方法呢?

大部分物件都會通過原型鏈找到Object.prototype上的toString或者valueOf方法,可是自己重寫就可以不用去原型鏈上查詢。

使用型別轉換,就會使用toPrimitive規則,如果重寫,toString或者valueOf方就會被覆蓋。

Symbol.toPrimitive

這個基本上不會用到吧?

  • 可以數接收一個字串引數hint,它表示要轉換到的原始值的預期型別,一共有'number'、'string'、'default'三種選項

  • 重寫toPrimitive方法,Symbol.toPrimitive的優先順序是最高的,所以不會執行toString ()和valueOf (),而是判斷它的返回值,如果是基礎資料型別(也就是原始值)那就返回,否則就丟擲錯誤。

  • 如果不傳參,hint呼叫的時候就確定了,使用String()呼叫時,hint為'string';使用Number()時,hint為'number'

var b = {
  toString () {
    console.log('toString')
    return '1'
  },valueOf () {
    console.log('valueOf')
    return [1,2]
  },[Symbol.toPrimitive] (hint) {
    console.log('symbol')
    if (hint === 'string') {
      console.log('string')
      return '1'
    }
    if (hint === 'number') {
      console.log('number')
      return 1
    }
    if (hint === 'default') {
      console.log('default')
      return 'default'
    }
  }
}
console.log(String(b)) //有hint === 'string,但是如果沒有返回值,得到 'undefined'
console.log(Number(b)) //有hint === 'number',但是如果沒有返回值,會得到NaN

'string'
'1'

'number'
1


如果沒有toString和valueOf方法,會得到:
// 'symbol'
// 'undefined'

// 'symbol'
// NaN

複製程式碼

==型別轉換

經常使用的部分來了,以上講的都是為了此做鋪墊

此處立馬會想到全等===,它不會進行型別轉換,所以寫程式碼的時候基本上用此型別,除非程式碼中型別判斷不區分字串還是數字。

看幾個常錯的結果:

console.log([] == ![]) // true
console.log({} == true) // false
console.log({} == "[object Object]") // true

複製程式碼

一臉茫然,直接上轉換規則,如下:

  1. 比較的雙方都為基本資料型別:
  • 若是一方為null、undefined,則另一方必須為null或者undefined才為true,也就是null == undefined為true或者null == null為true,因為undefined派生於null
console.log(null===undefined) 
> false
console.log(null==undefined)  
> true

console.log(null==null)
> true
console.log(null===null)
> true

console.log(undefined===undefined)
> true
console.log(undefined==undefined)
> true

console.log(null == 0) 
> false
console.log(null == false) 
> false
console.log(null == {}) 
> false

console.log(undefined == 0) 
> false
console.log(undefined == false) 
> false
console.log(undefined == {}) 
> false
複製程式碼
  • 其中一方為String,則把String轉為Number再來比較
console.log('131' == 131) // true
console.log('1b' == 12) // false
console.log('131n' == 131) // false

console.log('0x11' == 17) // true
console.log('false' == 0) // false
console.log('NaN' == NaN) // false

進一步複習知識點:
console.log(NaN == NaN) // false
console.log(NaN === NaN) // false
console.log(Object.is(NaN,NaN)) // true
複製程式碼
  • 其中一方為Boolean(true或者false,帶有!運算子),另一方可以為引用型別或者基本型別?,則將Boolean轉為Number再來比較
console.log(true == 1) // true
console.log(false == 0) // true


console.log(true == 'false') // false
console.log(false == null) // false

var b = {
  valueOf: function () {
    console.log('b.valueOf')
    return '1'
  },toString: function () {
    console.log('b.toString')
    return '2'
  }
}
console.log(!b == 1)  //false
console.log(!b == 0)  //true

!b它在轉換的過程中並沒有經過valueOf或者toString,而是直接轉為了false

console.log(!{} == {}) // false
console.log(!{} == []) // true
console.log(!{} == [0]) // true

複製程式碼
  • 如果一方是Boolean,另一方為String,都轉換為Number再來比較
console.log(true == '1') // true
console.log(false == '0') // true
console.log(true == '0') // false
要明確這裡'0'是轉化成Number而不是Boolean,所以
if ('0') {
    console.log('我會被執行') //這裡被執行是因為轉化為用Boolean
}

複製程式碼
  • 如果雙方型別相等(雙方可以為String、Boolean、Number),值相等毫無疑問是true
  1. 比較的一方有引用型別:
  • 將引用型別遵循ToNumber的轉換形式來進行比較,也就是先執行valueOf(),再執行toString(),如果最終得到的不是基本資料型別,否則會報錯,(實際上它的hint是default,也就是toPrimitive(obj,'default'),但是default的轉換規則和number很像,都是先執行判斷有沒有valueOf,有的話執行valueOf,然後判斷valueof後的返回值,若是是引用型別則繼續執行toString),再根據基本型別的規則進行轉換比較。
此例子比較default和number、string的區別
var b = {
  valueOf () {
    console.log('valueOf')
    return {}
  },toString () {
    console.log('toString')
    return 1
  },}
console.log(+b) // number
console.log(b + 1) // default
console.log(String(b)) // string

'valueOf'
'toString'
1
'valueOf'
'toString'
2
'toString'
'1'

複製程式碼

(換句話說:如果其中一方為Object,且另一方為String、Number或者Symbol,會將Object轉換成字串,再進行比較)

var b = {
  valueOf: function () {
    console.log('b.valueOf')
    return '1'
  },toString: function () {
    console.log('b.toString')
    return '2'
  }
}
var c = {
  valueOf: function () {
    console.log('c.valueOf')
    return {}
  },toString: function () {
    console.log('c.toString')
    return '2'
  }
}
console.log(b == 1)
console.log(c == 2)

'b.valueOf'
true
'c.valueOf'
'c.toString'
true

複製程式碼
function f () {
  var inner = function () {
    return 1
  }
  inner.valueOf = function () {
    console.log('valueOf')
    return 2
  }
  inner.toString = function () {
    console.log('toString')
    return 3
  }
  return inner
}
console.log(f() == 1)
'valueOf' 進行了型別轉換
false

console.log(f()() == 1)  //true

複製程式碼
  • 兩方都為引用型別,則判斷它們是不是指向同一個物件(可以從地址考慮到地址),得而外考慮轉換成NaN的情況
console.log([] == [])   //false   不是指向同一個物件

console.log({} == {})  //false
{}轉為字串其實是"[object Object]",再轉化成Number,為NaN

console.log([{},{}] == '[object Object][object Object]')
[{},{}] == '[object Object][object Object]'
// [{},{}]陣列中的每一項也就是{}轉為字串為'[object Object]',然後進行拼接
'[object Object],[object Object]' == '[object Object],[object Object]'
// true

console.log([] == Symbol('1'))
[] 得到'',所以false
// false
複製程式碼
  1. 一方為NaN(不是基本型別),根據以上規則判斷,如果都是NaN,都為false

以上轉換規則,圖例化,方便記憶:

總結,直接出流程圖:

+、-、*、/、%的型別轉換

  • -、*、/、%這四種都會把符號兩邊轉成數字來進行運算
console.log({} + "" * 1) // "[object Object]0"
console.log({} - []) // NaN
console.log({} + []) // "[object Object]"
console.log([2] - [] + function () {}) // "2function () {}"
需要考慮+號是字串的連線符規則
複製程式碼
  • +由於不僅是數字運算子,還是字串的連線符,所以分為兩種情況:
    • 兩端都是數字則進行數字計算
    • 有一端是字串,就會把另一端也轉換為字串進行連線
var b = {}
console.log(+b)  //NaN  相當於轉為數字
console.log(b + 1)  //'[object Object]1'   轉換為字串
console.log(1 + b)  //'1[object Object]'   轉換為字串
console.log(b + '') //'[object Object]'    轉換為字串
複製程式碼

由此想到'+'運算子與String()的區別?重寫toPrimitive來看結果

var b = {
  [Symbol.toPrimitive] (hint) {
    if (hint === 'default') {
      console.log('default')
      return '我是預設'
    }
    if (hint === 'number') {
      console.log('number')
      return 1
    }
    if (hint === 'string') {
      console.log('string')
      return '2'
    }
  }
}
console.log(+b) // number
console.log(b + 1) // default
console.log(1 + b) // default
console.log(b + '') // default
console.log(String(b)) // string
對於b + 1這種字串連線的情況,走的卻不是string,而是default


'number'
1
'default'
'我是預設1'
'default'
'1我是預設'
'default'
'我是預設'
'string'
'2'

複製程式碼

以上結果,可以看到b + 1和String(b)這兩種促發的轉換規則是不一樣的

  • {} + 1字串連線時hint為default
  • String({})時hint為string

幾道綜合題

涉及到等號的可以考慮重寫toString和valueOf

  1. 讓if(a == 1 && a == 2 && a == 3)條件成立的辦法?
var a = {
  value: 0,toString () {
    return ++this.value
  }
}
if (a == 1 && a == 2 && a == 3) {
  console.log('成立')
}
複製程式碼
  1. 控制檯輸入{}+[]會怎樣?
console.log({}+[]) // "[object Object]"

{}+[]   
//0   控制檯直接列印,不用console.log
複製程式碼

也就是說{}被忽略了,直接執行了+[],結果為0。

知道原因的我眼淚掉了下來,原來它和之前提到的1.toString()有點像,也是因為JS對於程式碼解析的原因,在控制檯或者終端中,JS會認為大括號{}開頭的是一個空的程式碼塊,這樣看著裡面沒有內容就會忽略它了。

{}被認為是程式碼塊而不是一個物件

  1. 實現f函式程式碼,實現cconsole.log中的值
function f () {
  let args = [...arguments]
  var add = function () {
    args.push(...arguments)
    return add
  }
  add.valueOf = function () {
    return args.reduce((cur,pre) => {
      return cur + pre
    })
  }
  return add
}
console.log(f(1) == 1)
console.log(f(1)(2) == 3)
console.log(f(1)(2)(3) == 6)
複製程式碼
  1. 讓if (a === 1 && a === 2 && a === 3)條件成立? 此條件是全等
var value = 1;
Object.defineProperty(window,"a",{
  get () {
    return this.value++;
  }
})
if (a === 1 && a === 2 && a === 3) {
  console.log('成立')
}
這裡實際就做了這麼幾件事情:

使用Object.defineProperty()方法劫持全域性變數window上的屬性a
當每次呼叫a的時候將value自增,並返回自增後的值

(我可以試著用Proxy來進行資料劫持,代理一下window,將它用new Proxy()處理一下,但是對於window物件好像沒有效果...)

複製程式碼

附參考連結

juejin.im/post/5e7f83…

juejin.im/post/5e86e7…