深入令人迷惑的JavaScript类型转换

引言

JS中,类型转换这方面的规则十分复杂,但是又经常用到,一不小心就会掉入陷阱。今天抽空来梳理一下这方面的知识,在开始之前,我们先回顾下JS有多少种数据类型。

es6之前共有6种

  • 原始类型(Primitive Types) :

    • number: 数字类型,包括整数和浮点数。
    • string: 字符串类型。
    • boolean: 布尔类型,truefalse
    • null: 表示空值。
    • undefined: 表示未定义的值。
  • 对象类型(Object Types) :

    • object: 包括普通对象、数组、函数等。

强制类型转换

强制转换主要指使用NumberStringBoolean三个函数,手动将各种类型的值,分布转换成数字、字符串或者布尔值。

Boolean()

原始类型转布尔

它的转换规则相对简单:除了以下六个值的转换结果为false,其他的值全部为true

  • false
  • undefined
  • null
  • -0+0
  • NaN
  • ''(空字符串)

注意:什么都不传,默认是false

js 复制代码
// Primitive -> Boolean
// 构造函数来用
console.log(Boolean()) // 什么都不传,默认是false
console.log(Boolean(false)) // false
console.log(Boolean(undefined)) // false
console.log(Boolean(null)) // false
console.log(Boolean(+0),'+0') // false
console.log(Boolean(-0),'-0') // false
console.log(Boolean(NaN),'NaN') // false 
console.log(Boolean(''),'空字符串') // false

补充说明:

+0-0 是两种特殊的数字值,它们在大多数情况下表现一致,但在某些场景下会有区别。

  • 做除法运算时
js 复制代码
console.log(1 / +0) //Infinity 正无穷大
console.log(1 / -0) //-Infinity 负无穷大
  • 使用 ===== 比较时,+0-0 被认为是相等的,但可以使用 Object.is() 准确检测。
js 复制代码
console.log( +0 === -0) //true
console.log( +0 == -0) //true
console.log( Object.is(+0,-0)) //false

对象类型转布尔

对象转换为布尔值的规则非常简单:所有对象(包括数组、函数、普通对象等)在转换为布尔值时,都会被认为是 true。这是因为 JavaScript 中的对象是"真值"(truthy)

注意:使用 new Boolean(false) 创建的包装对象是一个对象,因此会被转换为 true

js 复制代码
console.log(Boolean(new Boolean(false))); // true

Number()

原始类型转数字

大致规则如下

原始数据类型 转换规则
number 直接返回原值。
string 1.如果字符串是有效的数字,则转换为对应的数字。 2. 空字符串 ""和空格字符" " 转换为 0。 3.其他情况返回 NaN
boolean 1.true 转换为 1。 2.false 转换为 0
null 转换为 0
undefined 转换为 NaN

具体例子:

什么都不传,默认为0

js 复制代码
console.log(Number()) // 什么都不传,默认是0
console.log(Number(123)) // 123
// undefined 在数值上下文中没有转成一个特定数字的含义,而是转成了NaN
console.log(Number(undefined),'undefined') // NaN
console.log(Number(null),'null') // 0
console.log(Number(false),'false') // 0
console.log(Number(true),'true') // 1
console.log(Number(''),'空字符串') // 0
console.log(Number('123'),'字符串123') // 123
console.log(Number('-123'),'字符串-123') // -123
console.log(Number(""),Number(" ")) // 0
console.log(Number("100a"),'字符串100a')// NaN
console.log(Number("-0")) // 0
console.log(Number("+0")) // 0

说明:

字符串转数字

  • 字符串开头和结尾的空白字符会被忽略。
  • 忽略所有前导的 0
  • 如果字符串包含非数字字符(除了开头的空白和正负号),则返回 NaN
js 复制代码
console.log(Number(' 123 ')) // 123
console.log(Number('+123')) // 123
console.log(Number('-123')) // -123
console.log(Number('0x11')) // 17 16进制
console.log(Number('--123')) // NaN
console.log(Number('123abc')) // NaN

NaN 的特殊性

  • NaN 是一个特殊的数字值,表示"不是一个数字"。
  • 任何与 NaN 的运算结果都是 NaN
  • 使用 isNaN()Number.isNaN() 可以检测一个值是否为 NaN

parseInt()使用:

parseInt函数将字符串转为数值,要比Number()函数宽松一点。

  • 从字符串的开头开始解析,直到遇到无法识别为数字的字符为止。
  • 忽略后续的非数字部分。
  • 默认只解析整数(可以通过第二个参数指定进制)。
js 复制代码
parseInt("123"); // 123 
parseInt("123.45"); // 123 (只取整数部分) 
parseInt("123abc"); // 123 (忽略后面的 "abc") 
parseInt("abc123"); // NaN (开头不是数字或有效符号) 
parseInt(" 123 "); // 123 (会自动去除首尾空格)

对象类型转数字

对象转 Number 的过程:

  • 首先尝试通过 valueOf() 获取原始值,直接对该原始值使用Number函数,不再进行后续步骤。。

  • 如果 valueOf() 返回的不是原始值,再尝试通过 toString() 获取原始值,也是使用Number函数。

  • 如果 valueOf()toString() 都返回的是对象,最终会报错。

valueOf() 方法的主要目的是返回对象的"原始值"表示。如果对象本身没有明确的原始值,则默认返回对象自身。

对于 原始值包装对象(如 Number、String、Boolean),valueOf() 返回对应的原始值。而普通对象 或 数组,valueOf() 默认返回对象自身。Date 对象是个例外,valueOf() 返回的是时间戳(以毫秒为单位的时间值)。

toString() 方法的主要目的是将对象转换为字符串表示形式。

  • 普通对象toString() 默认返回 [object Object]
  • 数组toString() 返回元素的字符串化结果,用逗号连接。
    • 包含单个数值的数组不会在末尾添加逗号
    • undefinednull 作为数组元素转换为空字符串""
  • 函数toString() 返回函数的源代码。
  • 日期对象toString() 返回可读的日期时间字符串。
  • 原始值包装对象toString() 返回原始值的字符串表示。
  • 可以通过重写 toString() 方法来自定义对象的字符串化行为。

看晕了吗,没关系,我们通过具体例子来理解:

javascript 复制代码
var obj = {x: 1};
Number(obj) // NaN

// 等同于
if (typeof obj.valueOf() === 'object') {
  Number(obj.toString());
} else {
  Number(obj.valueOf());
}

上面代码中,首先调用obj.valueOf方法, 结果返回对象本身;于是,继续调用obj.toString方法,这时返回字符串[object Object],对这个字符串使用Number函数,得到NaN

之前说过,默认情况下,对象的valueOf方法返回对象本身,所以一般总是会调用toString方法,而toString方法返回对象的类型字符串(比如[object Object])。所以,会有下面的结果。

js 复制代码
Number({}) // NaN

如果toString方法返回的不是原始类型的值,结果就会报错。

js 复制代码
var obj = {
  valueOf: function () {
    console.log('Calling valueOf...');
    return {};
  },
  toString: function () {
    console.log('Calling toString...');
    return {};
  }
};

Number(obj)
// Calling valueOf...
// Calling toString...
// TypeError: Cannot convert object to primitive value

上面代码的valueOftoString方法,返回的都是对象,所以转成数值时会报错,并且可以看到确实是先valueOf,再toString

从上例还可以看到,valueOftoString方法,都是可以自定义的。

javascript 复制代码
Number({
  valueOf: function () {
    return 2;
  }
})
// 2

Number({
  toString: function () {
    return 3;
  }
})
// 3

上面代码对两个个对象使用Number函数。第一个对象返回valueOf方法的值,第二个对象返回toString方法的值。

String()

原始类型转字符

原始类型转换为字符串的规则非常简单且直观。以下是 原始类型转换为字符串 的详细规则:

原始类型 转换规则 示例 转换结果
number 直接转换为对应的数字字符串。 String(42) "42"
string 直接返回原值。 String("hello") "hello"
boolean - true 转换为 "true"。 - false 转换为 "false" String(true) String(false) "true" "false"
null 转换为 "null" String(null) "null"
undefined 转换为 "undefined" String(undefined) "undefined"

注意:什么都不传,默认是""空字符。

javascript 复制代码
console.log(String()) // 什么都不传,默认是""

toString() 方法

除了使用 String() 函数,原始类型(除了 nullundefined)还可以通过调用 toString() 方法转换为字符串。

vbscript 复制代码
let num = 42;
console.log(num.toString()); // "42"

let bool = true;
console.log(bool.toString()); // "true"
  • 原始类型也能通过.调用方法? 实际上,JavaScript 中的其他原始类型(如 NumberStringBoolean )都有对应的包装对象。这些包装对象允许开发者创建相应的对象实例,并使用一些额外的方法和属性。
css 复制代码
var a = 1.234
console.log(typeof a,a.toFixed(2)) //number 1.23
var b = new Number(a)
console.log(typeof b,b.toFixed(2)) //object 1.23
  • 注意nullundefined 没有 toString() 方法,调用会抛出错误,因为undefinednull 都没有包装对象,它们纯粹是原始值。

对象类型转字符

String方法背后的转换规则,与Number方法基本相同,只是互换了valueOf方法和toString方法的执行顺序。

  • 先调用对象自身的toString方法。如果返回原始类型的值,则对该值使用String函数,不再进行以下步骤。
  • 如果toString方法返回的是对象,再调用原对象的valueOf方法。如果valueOf方法返回原始类型的值,则对该值使用String函数,不再进行以下步骤。
  • 如果valueOf方法返回的是对象,就报错。

具体例子:

scss 复制代码
String({a: 1})
// "[object Object]"

// 等同于
String({a: 1}.toString())
// "[object Object]"

上面代码先调用对象的toString方法,发现返回的是字符串[object Object],就不再调用valueOf方法了。

如果toString法和valueOf方法,返回的都是对象,就会报错。

javascript 复制代码
var obj = {
  valueOf: function () {
    console.log('Calling valueOf...');
    return {};
  },
  toString: function () {
    console.log('Calling toString...');
    return {};
  }
};

String(obj)
// Calling toString...
// Calling valueOf...
// TypeError: Cannot convert object to primitive value

可以看出确实是先toString,再valueOf()

下面是通过自定义toString方法,改变返回值的例子。

javascript 复制代码
String({
  toString: function () {
    return 3;
  }
})
// "3"

String({
  valueOf: function () {
    return 2;
  }
})
// "[object Object]"


// "3"

上面代码对两个个对象使用String函数。第一个对象返回toString方法的值(数值3),第二个对象返回的还是toString方法的值([object Object])。

原始类型转对象

之前提到了一下,对原始类型.操作,会自动创建一个对应的包装对象,不需要我们手动操作。

原始值到对象的转换非常简单,原始值通过调用 String()、Number() 或者 Boolean() 构造函数,转换为它们各自的包装对象。

隐式类型转换

遇到以下三种情况时,JavaScript 会自动转换数据类型,即转换是自动完成的,用户不可见。

数学运算符

  1. 减、乘、除

我们在对各种非Number类型运用数学运算符(- * /)时,会先将非Number类型转换为Number类型。

仔细想一下,我们期望做的是数学运算,所以要得到数字类型来进行计算是理所应当的,就应该这么做。

javascript 复制代码
1 - true // 0, 首先把 true 转换为数字 1, 然后执行 1 - 1
1 - null // 1,  首先把 null 转换为数字 0, 然后执行 1 - 0
1 * undefined //  NaN, undefined 转换为数字是 NaN
2 * ['5'] //  10, ['5']首先会变成 '5', 然后再变成数字 5

之前说过,当数组只有一个值的时候,不会在末尾添加逗号,所以valueOf()不行之后,toString()得到了原始值'5'。

  1. 加法的特殊性

为什么加法要区别对待?因为JS里 +还可以用来拼接字符串,所以结果变的十分复杂,我们直接上图。

js 复制代码
console.log('hello' + 1) //hello1
console.log('hello' + undefined) //helloundefined
console.log('hello' + NaN) //helloNaN
console.log('hello' + null) //hellonull
console.log('hello' + 'world') //hello world
console.log('hello' + true) //hellotrue


console.log(1 + null) //1
console.log(1 + undefined) //NaN
console.log(1 + NaN) //NaN
console.log(1 + true) //2

console.log([]+ 1 ) //
function f(){}
console.log(f + 1) //function f(){}1
console.log([]+[]) //空字符
console.log({}+{}) 
console.log({
    valueOf:function(){
        return 1
    },
}+{
    valueOf:function(){
        return 2 
    }
}) //3

console.log({
    toString:function(){
        return {}
    },
}+{
    toString:function(){
        return {}
    }
}) //TypeError: Cannot convert object to primitive value

逻辑语句中的类型转换

当我们使用 if while for 语句时,我们期望表达式是一个Boolean,所以一定伴随着隐式类型转换。而这里面又分为两种情况:

1.单个变量

如果只有单个变量,会先变量转换为Boolean值。

arduino 复制代码
if ('abc') {
  console.log('hello')
}  // "hello"

不过这里有个小技巧,我们之前都说明过:

只有 null undefined '' NaN 0 false 这几个是 false,其他的情况都是 true,比如 {} , []这些对象。

2.使用 == 比较中的6条规则

  • 规则 1:NaN和其他任何类型比较永远返回false(包括和他自己)。
js 复制代码
NaN == NaN // false
  • 规则 2:Boolean 和其他任何类型比较,Boolean 首先被转换为 Number 类型。
js 复制代码
true == 1  // true 
true == '2'  // false, 先把 true 变成 1,而不是把 '2' 变成 true
true == ['1']  // true, 先把 true 变成 1, ['1']toPrimitive成 '1', 再参考规则3
true == ['2']  // false, 同上
undefined == false // false ,首先 false 变成 0,然后参考规则4
null == false // false,同上
  • 规则 3:StringNumber比较,先将String转换为Number类型。
js 复制代码
123 == '123' // true, '123' 会先变成 123
'' == 0 // true, '' 会首先变成 0
  • 规则 4:null == undefined比较结果是true,除此之外,nullundefined和其他任何结果的比较值都为false
js 复制代码
null == undefined // true
null == '' // false
null == 0 // false
null == false // false
undefined == '' // false
undefined == 0 // false
undefined == false // false
  • 规则 5:原始类型引用类型做比较时,引用类型会依照ToPrimitive规则转换为原始类型。

就像金翅小鹏王从四级境压制到道宫境与同为道宫境的叶凡一战。引用类型太强大了,并且一身傲骨,不屑于用境界压制原始人,自降境界与原始类型一较高下。

ToPrimitive规则

ToPrimitive 是 JavaScript 中的一个内部操作,用于将对象转换为原始值(primitive value)。它的行为由一个 提示(hint) 决定,这个提示告诉 ToPrimitive 应该优先尝试哪种类型转换。常见的提示有以下三种:
"string": 表示希望得到一个字符串。
"number": 表示希望得到一个数字。
"default": 表示没有明确的偏好,默认行为因对象类型而异。

大部分情况下跟 "number" 处理相同 ,但 + 号运算符比较特殊:

  • 如果 + 号的另一个操作数是字符串 ,则 default 更倾向于 "string",优先调用 toString()
  • 否则,default "number" 处理方式相同 ,优先调用 valueOf(),就比如==
    如果没法得到一个原始类型,就会抛出 TypeError
ini 复制代码
'[object Object]' == {} 
// true, 对象和字符串比较,对象通过 toString 得到一个基本类型值
'1,2,3' == [1, 2, 3] 
// true, 同上  [1, 2, 3]通过 toString 得到一个基本类型值
  • 规则 6:引用类型引用类型做比较时,比较的是地址。
js 复制代码
console.log([] == []) //false
function foo(){}
const bar = foo
console.log(foo == bar) //true

对非数值类型的值使用一元运算符

一元运算符也会把运算子转成数值。

js 复制代码
console.log(+'abc') //NaN
console.log(-'abc') //NaN
console.log(+true) //1
console.log(+[])//0
console.log(+{})//NaN

最后看几道复杂的题

1 [1,2,3].map(parseInt)

提示:

js 复制代码
console.log([1,2,3].map(item => parseInt(item))) //[ 1, 2, 3 ]

直接将 parseInt 作为 map 的回调函数时,map 会依次将当前元素的值、当前元素的索引和原数组传递给 parseInt。而 parseInt 函数接收两个参数:第一个参数是要解析的字符串,第二个参数是基数(可选,范围是 2 到 36)。

所以等价于:

c 复制代码
console.log([1,2,3].map((item,index,array) => {
  console.log(item,index,array)
  return parseInt(item,index)
}))
 - 答案是[ 1, NaN, NaN ]

2. [] == ![]

ini 复制代码
	- 第一步,![] 会变成 false
	- 第二步,应用 规则2 ,题目变成: [] == 0
	- 第三步,应用 规则5 ,[]的valueOf是[],toString是'',题目变成: '' == 0
        - 第四步,应用 规则3 ,''=>0 题目变成 0 == 0
	- 所以, 答案是 true 

3. [undefined] == false

ini 复制代码
	- 第一步,应用 规则5 ,[undefined]通过toString变成 '',
	  题目变成  '' == false
	- 第二步,应用 规则2 ,题目变成  '' == 0
	- 第三步,应用 规则3 ,题目变成  0 == 0
	- 所以, 答案是 true !
	// 但是 if([undefined]) 又是个true!

写完才发现,我排版的有点乱哈,一些后面讲的知识,在前面就用到了,兄弟们多多包含。

参考文章

# JavaScript深入之头疼的类型转换

# JavaScript 隐式类型转换,一篇就够了!

# 数据类型转换

相关推荐
小兵张健22 分钟前
运用 AI,看这一篇就够了(上)
前端·后端·cursor
不怕麻烦的鹿丸39 分钟前
node.js判断在线图片链接是否是webp,并将其转格式后上传
前端·javascript·node.js
vvilkim1 小时前
控制CSS中的继承:灵活管理样式传递
前端·css
南城巷陌1 小时前
Next.js中not-found.js触发方式详解
前端·next.js
No Silver Bullet1 小时前
React Native进阶(六十一): WebView 替代方案 react-native-webview 应用详解
javascript·react native·react.js
拉不动的猪2 小时前
前端打包优化举例
前端·javascript·vue.js
ok0602 小时前
JavaScript(JS)单线程影响速度
开发语言·javascript·ecmascript
Bigger2 小时前
Tauri(十五)——多窗口之间通信方案
前端·rust·app
倔强青铜三2 小时前
WXT浏览器插件开发中文教程(3)----WXT全部入口项详解
前端·javascript·vue.js
Aphasia3112 小时前
快速上手tailwindcss
前端·css·面试