数据类型转换集中营

465 阅读13分钟

在写代码的过程中经常需要考虑到类型转换,所以抽空总结出理论知识点,并把平时碰到的和现在能想到的情况列举出来。

此篇文章包含:

  • 基本类型
  • ToPrimitive转换机制
  • 重写toString和valueOf
  • Symbol.toPrimitive
  • 使用==比较时的类型转换
  • +、-、*、/、%的类型转换
  • 几道大厂的面试题

下面对于看到的知识进行总结和抽离

首先得知道基本类型和ToPrimitive

易忘点:

  1. 双等号和三等号的区别,其中一方为Boolean(true或者false,带有!运算符),另一方可以为引用类型或者基本类型?,则将Boolean转为Number再来比较【记住是将Boolean转为Number】
  2. 引用类型怎么转换成字符串和数字【重点看,容易忘记】
  3. null==false //false null只能和null或者undefined双等号才能为true
  4. NaN==NaN NaN===NaN 都是false
  5. 字符串转Number,应该多熟悉
  6. 将引用类型遵循ToNumber的转换形式来进行比较(比如+b),先执行valueOf(),再执行toString(),如果最终得到的不是基本数据类型,否则会报错
  7. 如果其中一方为Object,且另一方为String、Number或者Symbol,会将Object转换成字符串【先执行valueOf(),再执行toString()】,再进行比较
  8. 两方都为引用类型,则判断它们是不是指向同一个对象
  9. 一方为NaN(不是基本类型),根据以上规则判断,如果都是NaN,都为false

基本类型:

  • 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),再根据基本类型的规则进行转换比较。
此例子比较defaultnumberstring的区别
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.cn/post/684490…

juejin.cn/post/684490…