前言
在写了上篇文章:JS类型转换:那些你不知道的隐性陷阱和规则 - 掘金 我兴致满满的去刷了类型转换的题目,却发现自己在面对+"1"
、+[]
、[]+{}
这类题目时还是很懵逼
于是我便花了一上午时间阅读了一些大佬的文章终于对JS类型转换机制有了更清晰的认识,接着便写了这篇更深入的JS类型转换机制的文章,并加入toString、valueOf、toPrimitive
的详细解析。
Primitive => Object
原始值通过调用 String()、Number() 或者 Boolean() 构造函数,转换为它们各自的包装对象。
话不多说,先上代码:
var a = 1.234;
console.log(typeof a);
var b = new Number(a);
console.log(typeof b);
console.log(b.toFixed(1))
console.log(a.toFixed(1))
运行一下
number
object
1.2
1.2
可以看到,代码中我们使用 Number
构造函数创建了一个新的 Number
对象 b
,并将变量 a
的值作为参数传递给构造函数。所以当我们调用 Number
对象 b
的 toFixed
方法,并传入参数 1
(该方法用于将数字格式化为指定小数位数的字符串)时,输出了1.2
。可是为什么a.toFixed(1)
也能成功打印1.2,a
难道不是一个原始的数字类型吗???
这就涉及到了我们今天分享的第一个知识点——包装类
- 当对基本类型调用方法时,JavaScript 会自动将其转换为对应的包装类对象。
- 即使
a
是一个原始的数字类型,而不是一个Number
对象,JavaScript 也允许我们直接调用Number
对象的方法。在这个例子中,a.toFixed(1)
实际上是将a
包装成一个临时的Number
对象,然后调用toFixed
方法,最后将结果转换回字符串并输出到控制台。因此,输出结果也是"1.2"
。 "1.234".toFixed(1)
实际上是new Number("1.23").toFixed(1)
。
Object => Primitive
1.对象转布尔值
对象转布尔值只需记住一句话:所有的对象(包括数组和函数)都转换为true
代码示例
console.log(Boolean(new Boolean(false)))//true
console.log(Boolean({}))//true
console.log(Boolean([1,2,3]))//true
2. 对象转字符串和数字(难点)
ToPrimitive
Js引擎内部的抽象操作ToPrimitive(转换为原始值)的方法大体如下:
/**
* @obj 需要转换的对象
* @type 期望转换为的原始数据类型,可选
*/
ToPrimitive(obj,type)
当type
为Number
时,处理步骤如下:
-
若对象有
valueOf()
,则调用,若返回结果为原始值,则进一步转换为数字(看情况转换,非必须)并返回; -
否则,若对象具有
toString()
,则调用,若返回结果为原始值,后续同上; -
都无法获得原始值,那么抛出TypeError 异常;
代码示例
let objectWihoutPrimitiveValueOf = {
valueOf: function() {
console.log('Calling valueOf')
return this;
},
toString: function() {
console.log('Calling toString')
return '789';
}
}
console.log(Number(objectWihoutPrimitiveValueOf))
let problemObj = {
valueOf: function() {
console.log('Calling valueOf')
return this;
},
toString: function() {
console.log('Calling toString')
return this;
}
}
try{
console.log(Number(problemObj))
}catch(e){
console.log('error')
}
输出结果
Calling valueOf
Calling toString
789
Calling valueOf
Calling toString
error
可以看到:
- 这段代码尝试将
objectWihoutPrimitiveValueOf
对象转换为数字。由于valueOf
方法没有返回原始值,JavaScript 会继续调用toString
方法,最终将字符串"789"
转换为数字789
,并输出到控制台。 - 而第二段输出,这部分代码尝试将
problemObj
对象转换为数字。由于valueOf
和toString
方法都没有返回原始值,JavaScript 无法将该对象转换为数字,因此会抛出一个错误。这个错误会被catch
块捕获,并输出error
到控制台。
当type
为String
时,处理步骤如下:
-
若对象有
toString()
,则调用,若返回结果为原始值,则进一步转换为字符串(若本身不是字符串)并返回; -
若对象没有
toString()
或返回的不是一个原始值,那么调用valueOf()
,若结果为原始值,后续同上; -
都无法获得原始值,那么抛出TypeError 异常;
代码示例
let objectWithStringValue = {
toString: function() {
return 'hello';
},
valueOf: function() {
return 1;
}
}
console.log(String(objectWithStringValue))
let objectWithValueOf = {
toString: function() {
console.log('Calling toString')
return this;
},
valueOf: function() {
return 2;
}
}
console.log(String(objectWithValueOf))
输出结果
hello
toString
2
分析一下:
- 当我们想要把对象
objectWithStringValue
转换为字符串时,会优先调用toString()方法,该方法返回字符串"hello"
,所以并不会接着往下寻找,打印hello
- 把对象
objectWithValueOf
转换成字符串时,由于调用toString()方法时返回的是对象本身,所以会继续调用valueOf()方法,返回数字2
,这是一个原始值。因此,String(objectWithValueOf)
最终会将数字2
转换为字符串"2"
,并输出到控制台。
type 是个可选参数,不传入时,默认按照下面规则自动设置
- 若对象为
Date
类型,则 type为String
; - 否则 type 为
Number
;
toString()和valueOf()
可以看到,当我们在控制台输入
Object.prototype
时就可以看到 valueOf()
和 toString()
。而所有对象继承自Object,因此所有对象都继承了这两个方法。
Object.prototype.valueOf(): 对象的valueOf旨在返回对象的原始值,会在需要将对象转换成原始值的地方自动执行。机制如下:
// 1. 基本包装类型直接返回原始值
var num = new Number('123');
console.log(num.valueOf()); // 123
var str = new String('123abc');
console.log(str.valueOf()); // '123abc'
var bool = new Boolean('abc');
console.log(bool.valueOf()); // true
// 2. Date 类型返回一个内部表示:1970年1月1日以来的毫秒数
var date = new Date(2024,12,19);
console.log(date.valueOf())//1737216000000
//3.返回对象本身
var arr = [1,2,3]
console.log(arr.valueOf());//[ 1, 2, 3 ]
Object.prototype.toString(): toString()方法会返回表示该对象的字符串,会在对象预期要被转换成字符串的地方自动执行。机制如下:
// 1. 基本包装类型返回原始值
var num = new Number('123abc');
console.log(num.toString()); // NaN
var str = new String('123abc');
console.log(str.toString()); // '123abc'
var bool = new Boolean('abc');
console.log(bool.toString()); // true
// 2. 默认的 toString()
console.log(({a: 1}).toString())//[object Object]
console.log([1,2].toString())//1,2
console.log((function(){var a = 1;}).toString())//function(){var a = 1;}
// 3. 类自己定义的 toString()
// Date类型转换为可读的日期和时间字符
console.log(String(new Date(2024,12,18)))//Sat Jan 18 2025 00:00:00 GMT+0800 (中国标准时间)
console.log(JSON.stringify({a: 1}))//{"a":1}
值得一提的是,在默认的 toString()
中:
- 数组对象的
prototype
上有一个自定义的toString()
方法,它会将数组的每个元素转换为字符串,并将它们用逗号连接起来。因此,会输出"1,2"
- 而{a:1} 对象没有自定义的
toString()
方法,它会继承Object.prototype.toString()
方法。 因此,会输出"[object Object]"
。 - 而函数对象调用toString()返回这个函数定义的 Javascript 源代码字符串,因此,输出
"function(){var a = 1;}"
另外:
-
JSON.stringify()
方法会将对象的所有可枚举属性转换为 JSON 格式的字符串。
- 因此,会输出
"{"a":1}"
。
一元操作符 +
作为一元运算符时,+
用于将操作数转换为数字类型。即ToNumber()
方法,如果操作数已经是数字类型,则不会发生任何变化。如果操作数是字符串类型,则会尝试将其转换为数字。如果操作数是对象,则会先调用对象的 valueOf()
方法,如果该方法返回的不是原始值,则会继续调用 toString()
方法,然后将结果转换为数字。如果无法转换为数字,则结果为 NaN
。
在铺垫了这么多知识后,我们把罪魁祸首的题目搬出来拷打一遍
console.log(+"1")
console.log(+[])
console.log(+['1,2,3'])
console.log(+['1'])
console.log(+{})
已知我们需调用ToNumber()处理,且当输入的值为对象时,先调用ToPrimitive(obj,Number)
,再对返回值调用Number()
方法
我以+{}
为例,{}
先调用valueOf
方法,返回其对象本身,因为不是原始值,,所以再调用toString
方法,返回[object Object]
,再对其使用Number
方法,打印结果为NaN
。
剩下的题目以此类推,得出结果:
1
0
NaN
1
NaN
怎么样,你做对了吗?
二元运算符 +
+
符号既可以用作一元运算符,也可以用作二元运算符。当它作为二元运算符时,它执行加法操作。
执行步骤如下:
当计算 value1 + value2
时:
-
lprim = ToPrimitive(value1)
-
rprim = ToPrimitive(value2)
-
如果返回值中有一方为字符串,那么返回
ToString(lprim)
和ToString(rprim)
的拼接结果(即+
作字符串连接) -
否则,返回 ToNumber(lprim) + ToNumber(rprim)的运算结果
以一些题目作为例子
console.log(1+'1')//11
console.log(null + 1)//1
console.log([]+{})//[object Object]
console.log({}+{})//[object Object][object Object]
分析:
1 + '1'
因为1
与'1'
都是基本类型,所以直接返回其本身,即lprim=1
, rprim='1'
因为rprim是字符串,
所以执行ToString(1) + '1'
,得到'11'
null + 1
同上,null 与 1 都是基本类型,所以直接返回其本身,即lprim=null
,rprim=1
又因为lprim与rprim都不是字符串,所以返回 ToNumber(null) + ToNumber(1)
的运算结果,得到1
[] + {}
1.- lprim = ToPrimitive([]),因为两个对象中间有个运算符+,相当于ToPrimitive([], Number)
先调用valueOf方法,返回对象本身,因为不是原始值,调用toString方法,返回空字符串""
-
rprim = ToPrimitive({}),同上,返回
[object Object]
-
lprim和rprim都是字符串,执行拼接操作,
""+[object Object]
,得到[object Object]
{}+{}
lprim=rprim = ToPrimitive({}),返回了两个[object Object]
,都是字符串,即进行[object Object]+[object Object]
,得到[object Object][object Object]
关于==
规范
"=="
用于比较两个值是否相等,当要比较的两个值类型不一样的时候,就会发生类型的转换。
使用==
进行比较时的具体步骤如下:
给出一段代码做一些例子:
console.log([]==![])//true
console.log(42 == ['42'])//true
console.log(true == '2')//false
console.log(1 == '2')//false
console.log(null == undefined)//true
分析
1.[]==![]
首先会执行 ![]
操作,转换成 false,由规范中第7条
相当于 [] == false
,且ToNumber(false)=0, 即 [] == 0
,由规范中第9条
=>比较ToPrimitive([])==0
,相当于'' == 0
由规范中第4条
=>ToNumber('')==0
相当于 0 == 0
,结果返回 true
2.42 == ['42'] 看规范第8条:
- 如果Type(x) 时String或Number ,而Type(y) 时 Object,返回比较结果x==ToPrimitive(y)
以这个例子为例,会使用 ToPrimitive
处理 ['42']
,调用valueOf
,返回对象本身,再调用 toString
,返回 '42'
,所以
42 == ['42']
相当于 42 == '42'
相当于42 == 42
,结果为 true
。
3.true == '2'
由规范中第6、7条可知,当一方出现布尔值的时候,就会对这一方的值进行ToNumber处理,也就是说true会被转化成
true == '2'
就相当于 1 == '2'
就相当于 1 == 2
,结果自然是 false
。
4.1 == '2'
做到这是不是知道一眼false了,相当于1==ToNumber('2'),即1==2,false
5.null == undefined
看规范第2、3条,当一方为未定义
,另一方为null
时,返回true
其他情况就不一一列举了,规范中条例的很清楚
如果有错误或不足,欢迎纠正或补充。希望这篇文章能给大家带来帮助。