侧边栏

JavaScript中的数据类型转换

发布于 | 分类于 编程语言/JavaScript

JavaScript中的数据类型转换是一个很容易造成困惑的地方。本文是《你不知道的JavaScript(中)》类型转换相关的知识整理,强烈建议去看这本书。

数据类型

变量类型包括

  • 原始类型,包括Boolean、String、Number、Null、Undefined、Symbol、BitInt
  • 引用类型,Object、Array、Function等

在某些时候,需要对数据的类型进行判断,常用的方法有

  • typeof,可以得到undefinedbooleannumberstringobjectfunctionsymbol等类型结果
    • 注意对于数组、null和object而言,typeof都会返回'object'
  • instanceof,用于实例和构造函数的对应,其工作机制是:检测右侧构造函数的prototype属性是否出现在左侧实例对象的原型链上。
  • Object.prototype.toString.call(xxx)将会得到一个包含变量类型的字符串

显式类型转换

首先我们需要了解JS中一些显式的类型转换

ToString

ToString负责非字符串到字符串的强制类型转换

对于基础类型

  • null转换为'null'
  • undefined转换为'undefined'
  • true转换为'true',false转换为'false'
  • []转换为'',{}转换为'{}'
  • 数字遵循通用规则转换,极大值或极小值使用指数形式

对于对象而言,

  • 如果未实现toString,则返回[object Object];如果实现了toString,则调用该方法并使用返回值
js
const a = {
    toString() {
        return 'custom tostring'
    }
}
const b = {}
console.log(`${a}`) // custom tostring
console.log(`${b}`) // [object Object]
  • 数组对象默认自定义了toString,会将所有单元字符串化之后再使用逗号,连接起来。这里的一个特例是数组中null与undefined的元素会被字符串化为空字符串'',而不是上面的普通类型转换规则
    js
    console.log([1,null,2].toString()) // '1,,2'

ToNumber

ToNumber定义了将非数字转换为数字的规则

基本类型的转换

  • true转换为1,false转换为0
  • undefined转换为NaNnull转换为0
  • '''\n'' '等空白字符时会返回0,其余数字常量的字符串会转换为对应数字,转换失败则返回NaN
    • '0'开头的十六进制数字字符串是按照十进制进行转换

对象首先会被ToPrimitive转换成对应的基本值,如果返回的是非数字的基本类型,再应用上面的规则转换为数字

ToBoolean

布尔值只有真假两个值,因此JS中的值转成布尔值的话,也分为两类,

  • 可以被强制转换为false的值,包括undefinednullfalse+0-0NaN和空字符串''
  • 其他被强制转换为true的值
    • 注意对象(包括包装对象)永远为真值

ToPrimitive

可以参考:

在 JavaScript 中,ToPrimitive 是一种类型转换操作,它用于将对象转换为原始值。这个操作在涉及到对象与原始值之间的比较、算术运算以及模板字符串插值等情况下非常重要。ToPrimitive 的工作机制包括调用对象的内置方法来尝试获得一个原始值。

具体来说,ToPrimitive 操作可以分为以下几步:

*. 尝试调用 valueOf 方法

  • 首先,尝试调用对象的 valueOf 方法。如果返回一个原始值(stringnumberbooleansymbolbigint),则返回该值。
  • 如果 valueOf 返回一个对象,则继续执行下一步。

*. 尝试调用 toString 方法

  • 如果 valueOf 方法没有返回原始值,接下来尝试调用对象的 toString 方法。如果返回一个原始值,则返回该值。
  • 如果 toString 也返回一个对象,则会抛出类型错误(TypeError)。

ToPrimitive 操作还可以接受一个可选的 hint 参数,以指定希望的转换类型。hint 可以是 "number""string""default"。根据 hint 的不同,ToPrimitive 操作的优先级会有所不同:

  • hint"number" 时,ToPrimitive 操作优先调用 valueOf 方法。
  • hint"string" 时,ToPrimitive 操作优先调用 toString 方法。

hint会根据当前值参与的运算,自动推断出具体的值,表示当前语境传入的值(或者说是表示这是一个什么类型的运算),下面是一个简单的例子来演示 valueOftoString的操作:

javascript
let obj = {
  valueOf() {
    return 42;
  },
  toString() {
    return "Hello";
  }
};

console.log(+obj); // 目标是转成数字,所以hint为number,调用valueOf,输出 42

console.log(`${obj}`); // 目标是转成字符串,所以hint为string,调用toString,输出 "Hello"

当前语境对于数据的转换有决定性的作用,比如下面的代码

js
console.log(obj + ''); // '42'
console.log(`${obj}`); // 'Hello'

就是因为传入的hint不同,导致得到了不同的结果;我们可以编写一个代码来测试不同的运算中传入的hint值,

js
var a = {
    [Symbol.toPrimitive](hint) {
        console.log(hint)
        // 这里也可以返回原始值,作为toPrimitive操作的结果
    }
}
console.log(+a) // hint为number
console.log(a + '') // hint为default
console.log(`${a}`) // hint为string
console.log(a + 1) // hint为default

var o = {}
o[a] = 'xx' // hint为string

Symbol.toPrimitive是一个特殊的对象方法名,将在toPrimitive的过程中被优先调用

  • 我们创建的字面量对象{}[]不包含Symbol.toPrimitive方法,且valueOf方法返回的是自身,仍旧为引用值
  • 但是Date对象实现了默认的Symbol.toPrimitive方法,且返回的返回的是date.toString的值

==相等判断

判断两个值是否相等时很常见的,JS中提供了=====两个判断相等的运算符:“== 允许在相等比较中进行强制类型转换,而 === 不允许。”

那么在==中如何进行类型转换呢?JS内部有一系列比较复杂的规则。

类型相同

如果两个值的类型都是基础类型,且类型相同时,仅比较他们的值是否相等,但是要注意特例

  • NaN不等于NaN
  • +0等于-0

当二者均为对象类型时时,比较的是内存地址而不是具体值是否相等

js
console.log([] == []) // false, 数组地址不同
console.log([1,2] == [1,2]) // false, 数组地址不同
console.log({} == {}) // false,对象地址不同

类型不同

如果两个值的类型不同,则会发生强制类型转换,会将其中之一或者二者转换成同一类型后再继续比较

x == y 时,ES5 规范 11.9.3.4-5 这样定义:

数字 vs 字符串

规则1:二者有一个为数字,另一个为字符串,则将字符串转换成数字类型

  • 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果
  • 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果
js
// 返回100 == 100的结果,类型相同,比较值,返回true
'100' == 100 // true 

// 首先ToNumber('100x')返回NaN,然后 NaN == 100都为数字类型,比较值,返回false
'100x' == 100 // false

布尔值 vs 其他类型

规则2:二者有一个为布尔值,另一个为其他类型时,会先将布尔值转换成数字,然后再进行==比较

  • 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果
  • 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果
js
// 首先将a转换成数字1,此时计算 1 == '100',应用上面的规则1,将字符串转换成数字,返回 1 == 100的结果
true == '100' // false 

// 基于上面的特点,永远不要使用下面的写法
if(a == true) {}

null vs undefined

null 表示一个对象是“没有值”的值,也就是值为“空”;undefined 表示一个变量声明了没有初始化(赋值);

规则3:null与undefined之间的比较

  • 如果 x 为 null,y 为 undefined,则结果为 true。
  • 如果 x 为 undefined,y 为 null,则结果为 true。
  • 除此之外其他任何值都与这两个值不相等
js
null == undefined // true
undefined = true // true

null == true // false
null == false // false
null == '' // false
null == 0 // false

undefined == true //  false
undefined == false // false
undefined == '' // false
undefined == 0 // false

其他 vs 对象

规则4:对象与非对象之间的对比

  • 如果 Type(x) 是字符串或数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果;
  • 如果 Type(x) 是对象,Type(y) 是字符串或数字,则返回 ToPrimitive(x) == y 的结果。
  • 如果基础值是布尔值,会先应用规则3,然后再进行上面的ToPrimitive操作
// 首先[42]调用ToPrimitive转换为基础值
42 == [42] // true

建议

  • 如果两边的值中有 true 或者 false,千万不要使用 ==。
  • 如果两边的值中有 []、"" 或者 0,尽量不要使用 ==。

还有一些常见的坑

js

// []转换为'',而{}转换为[object Object],因此这里是字符串相加
[] + {}; // "[object Object]" 

// 这里{}会被当做一个空的代码块,因此等价于 +[],会将[]转换为数字,
// 因此调用ToPrimitive返回空字符串, 然后将空字符串转ToNumber换为数字0
{} + []; // 0

伪代码

下面用伪代码演示了==的运算规则

js
function looseEqual(x, y) {
  // 步骤 1
  if (Type(x) === Type(y)) {
    return strictEqual(x, y);
  }

  // 步骤 2
  if (x == null && y == null) {
    return true;
  }

  // 步骤 3
  if (Type(x) === 'Number' && Type(y) === 'String') {
    return looseEqual(x, ToNumber(y));
  }
  if (Type(x) === 'String' && Type(y) === 'Number') {
    return looseEqual(ToNumber(x), y);
  }

  // 步骤 4
  if (Type(x) === 'Boolean') {
    return looseEqual(ToNumber(x), y);
  }
  if (Type(y) === 'Boolean') {
    return looseEqual(x, ToNumber(y));
  }

  // 步骤 5
  if ((Type(x) === 'String' || Type(x) === 'Number' || Type(x) === 'Symbol' || Type(x) === 'BigInt') && Type(y) === 'Object') {
    return looseEqual(x, ToPrimitive(y));
  }
  if ((Type(y) === 'String' || Type(y) === 'Number' || Type(y) === 'Symbol' || Type(y) === 'BigInt') && Type(x) === 'Object') {
    return looseEqual(ToPrimitive(x), y);
  }

  return false;
}

在实际生产中,除了极少数特殊的场景之外,建议都使用===进行严格的相等判断。

+ 加法

参考:MDN文档

加法运算符(+)为两种不同的运算重载:数字加法和字符串连接。

在求值时,它首先将两个操作数强制转换为基本类型。然后,检查两个操作数的类型:

  • 如果有一方是字符串,另一方则会被转换为字符串,并且它们连接起来。
  • 如果双方都是 BigInt,则执行 BigInt 加法。如果一方是 BigInt 而另一方不是,会抛出 TypeError。
  • 否则,双方都会被转换为数字,执行数字加法。
console.log([] + []); // '' + []  -> '' + '' -> ''
console.log({} + []); // '[object Object]'+ '' -> '[object Object]'
console.log([] + {}); // '[object Object]'
console.log({} + {}); // '[object Object][object Object]'

小结

JavaScript的数据类型转换是开发和面试中经常遇见的问题,稍有不慎就容易犯错,本文主要整理类型转换的一些规则,用以备忘。

你要请我喝一杯奶茶?

版权声明:自由转载-非商用-保持署名和原文链接。

本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。