数据类型转换

171 阅读10分钟

注,此文中一定要关注 [Symbol.toPrimitive],valueOf,toString 的调用顺序。

把其他类型转换为 number

基本有四种规则:

  • Number(val) 显式转 number
  • 隐式转 number
  • parseInt(val, [radix])
  • parseFloat(val)

显式转换成 number

Number 显式转换数字类型有以下几种情况:

  1. 字符串转换为数字:空字符串变 0,非有效数字变 NaN,
  2. 布尔转数字: true -> 1,false -> 0
  3. null -> 0, undefined => NaN
  4. 把 Symbol 转数字: 报错 Cannot convert a Symbol value to a number
  5. 把 BigInt 转数字: 10n -> 10, 如果超过安全数字,则按照科学记数法处理,比如 Number(666666665555555556666n) -> 666666665555555500000
  6. 把对象显式转换成数字:

1. 先调用对象的 Symbol.toPrimitive 这个方法获取原始值,如果不存在这个方法,↓ 2. 再调用对象的 valueOf 获取原始值,如果获取的值不是原始值,↓ 3. 再调用对象的 toString 获取原始值 还没取到原始值则报错,取到基于 Number 转换成数字,返回即可。

Number({ name: 'xx' }) // NaN
// 先调用 ({ name: 'xx' })[Symbol.toPrimitive] 方法,接收一个 hint 参数,此方法不存在于普通对象上
// 再调用 ({ name: 'xx' }).valueOf 得到 { name: 'xx' },不是原始值
// 再调用 ({ name: 'xx' }).toString 得到字符串 "[object Object]"
// Number("[object Object]");  返回 NaN

Number([10]) // 10 
// 先调用 [10][Symbol.toPrimitive] 方法,此方法不存在于数组对象上
// 再调用 [10].valueOf 得到 [10],不是原始值
// 再调用 [10].toString 得到字符串 "10"
// Number("10");  返回 10


let time = new Date();

Number(time); // 1621757451241
// 先调用 time[Symbol.toPrimitive] 方法,发现有,就会调用该方法,传入 'number' 类型
// time[Symbol.toPrimitive]('number'); 返回 1621757451241

Number(new Number(10)) // 10
// 先调用 (new Number(10))[Symbol.toPrimitive] 方法,此方法不存在于数字对象上
// 再调用 new Number(10).valueOf 得到 10,是原始值
// Number(10);  返回 10

var obj = {
    [Symbol.toPrimitive]() {
      return '2'
    },
    valueOf() {
      console.log('valueof');
      return '10'
    },
    toString() {
      console.log('tostring');
      return '11'
    }
}

+obj // 2 相当于 Number 调用

isNaN 会使用 Number 做显式类型转换。

隐式转换成 number

原始值隐式转数字: 原始值类型的隐式转换为数字,跟 Number 行为基本一致,唯一区别在 10n + 1 会报错,而不会先把 10n 转 10 与 1 相加(这是因为 js 不允许大数与普通数字进行运算而做的特殊处理)。 把对象隐式转换成数字: 1. 先调用对象的 Symbol.toPrimitive 这个方法获取原始值,如果不存在这个方法,↓ 2. 再调用对象的 valueOf 获取原始值,如果获取的值不是原始值,↓ 3. 再调用对象的 toString 获取原始值 还没取到原始值则报错,取到返回即可,不用 Number 包装。

  • 数学运算中「除了一方为字符串的字符串拼接」,都会隐式转换成 number
  • == 隐式转换成 number
  • 等,下面再罗列这些规则。
10n == 1 // true


var obj = {
    [Symbol.toPrimitive]() {
      return false
    },
    valueOf() {
      console.log('valueof');
      return '10'
    },
    toString() {
      console.log('tostring');
      return '11'
    }
}

1 + obj //  => 1 + false => 1 + 0 = 1

let time = new Date();

time + 1 
// Date 实例比较例外,不管是隐式转 number 还是 string, 都是会传入 hint 为 string 的类型
// 先调用 time[Symbol.toPrimitive] 方法,发现有,就会调用该方法,传入 'string' 类型
// time[Symbol.toPrimitive]('string'); 返回 "Thu May 27 2021 23:00:45 GMT+0800 (中国标准时间)1"

通过 parseInt、parseFloat 转换成数字

这两种方法一般用于手动转换。

  • parseInt(val, [radix])
  • parseFloat(val)

规则:val 值必须是一个字符串,如果不是则先转换成字符串,然后从字符串首位开始查找,把找到的有效数字字符最后转换成数字「如果一个有效数字都没找到,返回 NaN」; 遇到一个非有效数字字符,不论后面是否还有有效数字字符,都不再查找了; 两种方法的区别在于 parseFloat 可以多识别一个小数点,而且 parseInt 有第二个参数,我们下面细说;

// 小试牛刀
parseInt('10'); // 10

parseInt('10px'); // 10
Number('10px'); // NaN

parseInt('10px10'); // 10
parseInt('10.5px'); // 10
parseFloat('10.5px'); // 10.5

Number(null); // 0
parseInt(null); // -> parseInt('null') -> NaN
parseFloat(null); // NaN

一道面试题之 parseInt 的第二个参数

先来看一道经典面试题

let arr = [27.2, 0, '0013', '14px', 123];

arr = arr.map(parseInt);

console.log(arr);

parseInt 传递的第二个值是一个 radix 进制

  1. radix 不写或者写0,默认是10进制,「如果字符串是以 '0x' 开始的」,那么默认是16进制的。
  2. radix 取值范围:2 ~ 36,不在这个范围内,处理的结果都是 NaN。
  3. 在传递的字符串中,从左到右,找到符合 radix 进制的值(遇到不符合的则结束查找),把找到的值,看做 radix 进制,最后转换为 10 进制。
  4. 其他进制转 10 进制的规则(按权展开求和),比如 2 进制的数字 010,转为 10 进制就是 0 * 2^2 + 1 * 2^1 + 0 * 2^0 = 2,所以 parseInt('010', 2) 输出 2;

特殊说明,16进制输入字符串中的 A 代表 10, F 代表 15;比如 parseInt("1FA.37", 16) 输出 16进制的 "1FA",转 10 进制为:1 * 16^2 + 15 * 16^1 + 10 * 16^0 = 506

抛开 parseInt 不谈,我们单独把 3 进制表示的 10.21,转成 10 进制,它的按权展开求和为 1 * 3^1 + 0 * 3^0 + 2 * 3^-1 + 1 * 3^-2 = 3 + 0 + 2 * 1/3 + 1 * 1/ 3^2 = 34/9

parseInt(27.2, 0);
// => parseInt('27.2', 10);
// => 从左到右,找符合十进制的值,直到遇见不符合 10 进制的值,所以是 '27'
// => 把 '27' 看做 10 进制,转换为 10 进制 => 27

parseInt(0, 1);
// 进制 2 ~ 36 之间 => NaN

parseInt('0013', 2);
// => parseInt('0013', 2);
// => 从左到右,找符合 2 进制的值,直到遇见不符合 2 进制的值,所以是 '001'
// => 把 '001' 看做 2 进制,转换为 10 进制, 按权展开求和
// => 0 * 2^2 + 0 * 2^1 + 1 * 2^0 = 1 

parseInt('14px', 3);
// => parseInt('14px', 3);
// => 从左到右,找符合 3 进制的值,直到遇见不符合 3 进制的值,所以是 '1'
// => 把 '1' 看做 3 进制,转换为 10 进制, 按权展开求和
// => 1 * 3^0 = 1 

parseInt(123, 4);
// => parseInt(123, 4);
// => 从左到右,找符合 4 进制的值,直到遇见不符合 4 进制的值,所以是 '123'
// => 把 '123' 看做 4 进制,转换为 10 进制, 按权展开求和
// => 1 * 4^2 + 2 * 4^1 + 3 * 4^0 = 16 + 8 + 3 = 27

所以上面面试题的最终输出结果是 [27, NaN, 1, 1, 27]

把其他类型转字符串

基本有三种规则:

  • String(val) 显式转 string
  • 隐式转 string
  • toString()方法调用

显式转字符串

  • String()

原始值类型显式转换成字符串:基于引号包起来,BigInt 是去掉 n 对象类型显式转换成字符串: 1. 先调用对象的 Symbol.toPrimitive 这个方法获取原始值,如果不存在这个方法,↓ 2. 再调用对象的 toString 获取原始值,如果获取的值不是原始值,↓ 3. 再调用对象的 valueOf 获取原始值 还没取到原始值则报错,取到原始值用 String 包装后返回。

// 注意区别
Number(10n); // 10
String(10n); // '10' 

var obj = {
    [Symbol.toPrimitive]() {
      return false
    },
    valueOf() {
      console.log('valueof');
      return '10'
    },
    toString() {
      console.log('tostring');
      return 11
    }
}

String(obj); // 'false'

var obj2 = {
    valueOf() {
      console.log('valueof');
      return '10'
    },
    toString() {
      console.log('tostring');
      return 11
    }
}

String(obj2); // '11'

隐式转字符串

原始值类型隐式转换成字符串:基于引号包起来,BigInt 是去掉 n 对象类型隐式转换成字符串: 1. 先调用对象的 Symbol.toPrimitive 这个方法获取原始值,如果不存在这个方法,↓ 2. 再调用对象的 valueOf 获取原始值,如果获取的值不是原始值,↓ 3. 再调用对象的 toString 获取原始值 还没取到原始值则报错,取到原始值直接返回(不用 String 包装)。

 var obj = {
    [Symbol.toPrimitive]() {
      return 2
    },
    valueOf() {
      console.log('valueof');
      return '10'
    },
    toString() {
      console.log('tostring');
      return 11
    }
}

obj == '2' // true
obj + 3 // 5

var obj2 = {
    valueOf() {
      console.log('valueof');
      return '10'
    },
    toString() {
      console.log('tostring');
      return 11
    }
}

obj2 == '11' // false
'a' + obj2 // 'a10'

let time = new Date();

time + "a"
// Date 实例比较例外,不管是隐式转 number 还是 string, 都是会传入 hint 为 string 的类型
// 先调用 time[Symbol.toPrimitive] 方法,发现有,就会调用该方法,传入 'string' 类型
// time[Symbol.toPrimitive]('string'); 返回 "Thu May 27 2021 23:00:45 GMT+0800 (中国标准时间)a"

toString() 转字符串

原始值类型调用 toString 转字符串:基于引号包起来,BigInt 是去掉 n 对象类型调用 toSring 转字符串: 1. 直接调用对象的 toString 获取原始值,如果获取的值不是原始值,↓ 2. 再调用对象的 valueOf 获取原始值 还没取到原始值则报错,取到原始值直接返回(不用 String 包装)。

 var obj = {
    [Symbol.toPrimitive]() {
      return 2
    },
    valueOf() {
      console.log('valueof');
      return '10'
    },
    toString() {
      console.log('tostring');
      return 11
    }
}

obj.toSring(); // 11

总结

你肯定不想看上面又臭又长的总结 + 验证过程,够懂你吧。

// ----------- 隐式转换 -------------
1. 先调用自身 [Symbol.toPrimitive]
2. 再调用 valueOf
3. 最后调用 toString
4. 返回得到的原始值

> 注意点:隐式转换的 Date 类型有点奇怪,都会优先转 string 类型,也就是 new Date()[Symbol.toPrimitive]('string')

// ----------- 显式转换 -------------
Number:
1. 先调用自身 [Symbol.toPrimitive],传入 hint 为 number 类型
2. 再调用 valueOf
3. 最后调用 toString
4. 把得到的原始值通过 Number 转成数字类型返回

String:
1. 先调用自身 [Symbol.toPrimitive],传入 hint 为 default/string 类型
2. 再调用 toString
3. 最后调用 valueOf
4. 把得到的原始值通过 String 转成数字类型返回

toString 方法调用:
2. 先调用 toString
3. 再调用 valueOf
4. 返回得到的原始值

数学运算符的转换规则

此类运算中,如果一方为 NaN,则结果为 NaN

  1. 一方为字符串,会将另一方也隐式转为 string 进行字符串拼接
  2. 「除了一方为字符串的字符串拼接」,都会隐式转成 number 进行运算。

+a 或者 -a 中的符号并不是二元运算符「加法」「减法」,而是一个一元运算符,作用是将它后面的操作数转换成数字,和 Number 显式转换完全一样。

1 + 'a' // '1' + 'a' => '1a'

1 + {}  // '1[object Object] 对象隐式转数字,推导过程如下
// ({}).toPrimitive? => ({}).valueOf? => ({}).toString() => '[object Object]'

var obj = {
  toString() {
    return 1;
  }
}

1 + obj // 2
'1' + obj // '11'

var obj2 = {
  valueOf() {
    return 1;
  },
  toString() {
    return 2;
  }
}

1 + obj2 // 2
'1' + obj2 // '11'

var obj3 = {
  [Symbol.toPrimitive]() {
    return 0;
  },
  valueOf() {
    return 1;
  },
  toString() {
    return 2;
  }
}
1 + obj3 // 1
'1' + obj3 // '10'

== 的转换规则

三等号不会进行隐式转换哦

  1. null 和 undefined 互相等,各自等。
  2. NaN 不等于 NaN。
  3. 数据类型相同,不需要转换,引用类型比较的是指针地址。
  4. 一方引用类型,一方字符串的话,会先把对象隐式转换成字符串作比较。
  5. 都隐式转换成 number 类型做比较。
console.log(1 == true) 
// 1 == 1

console.log('1' == true) 
// 都转数字 1 == 1

console.log('1' == 1); 
// 1 == 1

console.log([] == false); 
// 0 == 0

console.log([1, 2] == NaN); 
//  "1,2" == NaN  -> Number("1, 2") == NaN -> NaN == NaN

console.log(['1'] == true); 
// "1" == true -> Number("1") == Number(true) -> 1 == 1

console.log([] == ![]) 
// [] == false -> "" == 0 -> Number("") == 0 -> 0 == 0

console.log({} == !{}) 
// {} == false -> Number("[object Object]") == false -> NaN == 0

console.log({} == {}) 
// false 注意 当两边数据类型相同 不进行隐式转换,如果是复杂数据类型,对比的是指针

console.log([] == []) 
// false 同上

console.log([1, 2] == '1,2'); // 对象和字符串比较 对象要转字符串
// [1, 2] => '1,2' 再做对比 为 true

a == 1 && a == 2 && a == 3

var a = ?;

// 实现如下判断
if (a == 1 && a == 2 && a == 3) {
  console.log('ok');
}

因为把对象转 number,我们会走 Symbol.toPrimitive, valueOf,再走 toString,最好 Number 包装返回值,所以改写对象的三个方法之一即可。

var a = {
  i: 0,
  [Symbol.toPrimitive]() {
    return ++this.i
  }
}

或者直接声明成数组

var arr = [1, 2, 3]

// 当然改 valueOf 方法也可以。
arr.toString = arr.shift;

或者直接劫持变量

var i = 0;

Object.defineProperty(window, 'a', {
  get() {
    return ++i;
  }
});

腾讯的一道面试题

let result = 100 + true + 21.2 + null + undefined + 'Tencnet' + [] + null + 9 + false;

console.log(result);

// 100 + true + 21.2 + null + undefined = 122.2 + 0 + NaN  = NaN
// NaN + 'Tencnet' = 'NaNTencnet';
// 'NaNTencnet' + [] + null + 9 + false = 'NaNTencnet' + '' + null + 9 + false = 'NaNTencnetnull9false'