前言
在写了上篇文章: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
其他情况就不一一列举了,规范中条例的很清楚
如果有错误或不足,欢迎纠正或补充。希望这篇文章能给大家带来帮助。