引言
写完 JS 类型转换(上)后发现这个知识点是庞大的,还是需要第二篇来进行补充的,所以让我们接着学习JS 的类型转换吧。
原始值转对象(Primitive to Object)
当我们想要将 Primitive
类型转为 Object
时,我们可以通过 new
运算符来显式地进行强转,例如:
javascript
var a = 1.23;
console.log(typeof a); // Number 类型
var b = new Number(a); // Number 对象
console.log(typeof b); // Object
在上述代码中我们将通过new
运算符将a
强转为Number
对象,后续通过typeof
可以查看到b
为Object
类型。
而据我们所知,在Number
对象的prototype
上有toFixed()
这个方法,而Primitive
类型本身上没有任何的方法和属性,所以当出现以下代码时,我们会认为console.log(a.toFixed(1));
会报错。
javascript
var a = 1.23;
var b = new Number(a);
console.log(b.toFixed(1));
console.log(a.toFixed(1));
但是当我们运行后会发现神奇的事情出现了:
并没有发生报错,而是两行代码都能执行,这是为什么呢?这里我们就需要了解 JS 中包装类这一机制。
包装类的概念:
JavaScript中"一切皆对象"
JavaScript的语言特性毫无疑问就是 "一切皆对象" ,而当我们越了解 JS,就会发现这一特性在无限放大,而包装类也是为了实现这一特性而为原始数据类型量身定做的机制。即:所有的数据类型都可以被视为对象。
尽管基本数据类型不是真正的对象,但为了方便操作,JavaScript允许我们像对待对象一样来使用它们。这意味着我们可以直接在原始值上调用方法或访问属性。
包装类的机制
当我们对原始值调用方法或访问属性时,JavaScript会临时创建一个对应的对象实例来执行该操作,执行完后立即销毁这个临时对象。
这样就能解释我们上述代码中为什么没有报错了
javascript
var a = 1.23;
console.log(a.toFixed(1));
// 在对a执行toFixed()操作时,JS 临时创造了一个Number对象,这样a也就具有了Number对象的方法和属性
// 就像执行了:(new Number(a)).toFixed(),只是不用我们显示地转换
而且在执行后a
仍然是number
原始数据类型。
对象转原始值(Object to Primitive)
Object --> Boolean
对象转Boolean只需要注意一个点,即:"所有对象(包括数组和函数)转换为 Boolean 都为 true"。
例如:
javascript
console.log(Boolean(new Boolean(false)))
// 我们可能惯性认为这会输出一个 false
// 但是我们包装了Boolean对象
// 而只要是对象,Boolean 强转就会是 true
console.log(Boolean({}))
console.log(Boolean([]))
执行后就可以得到以下结果:
Object --> String/Number
想要理解对象如何转换为String
和Number
,我们必须跨过拦在转换路程上的"左右护法"---toString
和valueOf
,让我们先单独理解一下两个函数。
toString:
想必我们对toString()
的使用并不陌生,对于所有对象除了null
和 undefined
都包含这个方法,而这个方法在多种情况下自动调用,或者显式地使用 String()
函数或模板字符串来转换对象。
当我们调用对象的toString
方法时,调用的是Object.prototype
上的toString
方法,而其会返还格式为:[object class]
的结果,并且String()
函数也是相同的结果,例如:
javascript
console.log(({a: 2}).toString()); // [object Object]
console.log(Object.prototype.toString.call({a: 2})); // [object Object]
console.log(({a: 2}).toString === Object.prototype.toString); // true
console.log(String({a: 2})); // [object Object]
当然也可以通过call()
来精确返还内容的类型
javascript
console.log(Object.prototype.toString.call({a: 1})) // [object Object]
console.log(Object.prototype.toString.call([1, 2])) // [object Array]
但是许多内置对象重写了 toString()
方法,以提供更有意义的字符串表示形式,但是这些方法仍然来自于Object.prototype
- 数组:返还以逗号间隔的数组元素组成的字符串
- 日期:返还日期和时间的字符串表示
- 函数:返回函数的源代码字符串
例如执行以下代码:
javascript
console.log([1,2,3].toString());
console.log((new Date(2024,12,18)).toString());
console.log((function() {var a = 1;}).toString());
console.log(Array.prototype.__proto__ === Object.prototype) // true
valueOf:
在默认情况下,valueOf()
方法返回对象本身,但对于大多数内置对象,valueOf()
提供了一个更有意义的原始值表示形式。
例如:
javascript
console.log(({a: 1}).valueOf()); // {a: 1}
console.log([1,2,3].valueOf()); // [1,2,3]
console.log(("a").valueOf()); // a
console.log((new Date(2024,12,18)).valueOf()); // 1737129600000
唯一值得注意的是对于Date返还的是该日期对象所代表的时间戳(timestamp),其表示自1970年1月1日00:00:00 UTC(协调世界时)以来的毫秒数。(可以理解为 JS 认为转换为毫秒更利于精确地表示和计算时间)
正片开始:
当你理解了上述的两个函数后,我们才能真正开始学习对象转String和Number,因为在转换操作时,我们主要就是关注这两个方法。
Object --> Number:
当我们在对象字面量里面添加 valueOf 和 toString 方法时,会优先使用本身的方法,而不去寻找原型链上的方法。
例如:
javascript
let specialObj = {
valueOf: function() {
console.log("Calling valueOf...")
return 123;
},
toString: function() {
console.log("Calling toString...")
return "hello";
}
}
console.log(specialObj.valueOf()); // 123
console.log(specialObj.toString()); // NaN
在我们自行创建这两个方法后,那我要是进行转换为Number对象会发生什么事吗?
javascript
console.log(Number(specialObj));
执行后我们得到以下结果:
我们发现并没有执行toString
的操作,这是因为在执行转化Number时,valueOf
比toString
的优先级高,会先调用valueOf
方法,而执行valueOf
已经可以得到一个合理的返还了,所以并不需要再执行toString
了,但是当我们将valueOf
的返还值改为this
呢?
javascript
valueOf: function() {
console.log("Calling valueOf...")
return this;
},
当我们再执行转换Number对象时,我们得到了以下的结果:
我们发现在执行valueOf后并没有得到一个有效的值,所以会再执行toString操作,返还了NaN。
哎🧐👆,在我脑海中产生了一个大胆的想法,如果我们将toString的返还值也改为this后会发生什么事情呢?
javascript
toString: function() {
console.log("Calling toString...")
return this;
}
//为了保险起见,我们使用try catch
try {
Number(specialObj)
} catch (e) {
console.log(e)
}
结果也不出所料,我们得到了一个大大的报错,所以我们可以得出结论,当toString和valueOf方法都返回对象本身时,会抛出异常。
通过上述的操作后,我们就可以得出Object转Number的一个完整流程:
-
- 如果这个对象具有
valueOf
方法,并且可以返回一个原始值,JS 就会将这个原始值转换为数字并返回这个数字
- 如果这个对象具有
-
- 否则,如果对象具有
toString
方法,并且可以返回一个原始值,则 JS 就会将其转换并返回。
- 否则,如果对象具有
-
- 当1和2都行不通的时候,就会报错:
cannot convert object to primitive value
- 当1和2都行不通的时候,就会报错:
Object --> String
同理,其实转String和转Number很相似,只是优先级不同,显然toString对于转String更重要一点,所以转String为以下步骤:
-
- 如果这个对象具有
toString
方法,并且可以返回一个原始值,JS 就会将这个原始值转换为数字并返回这个数字
- 如果这个对象具有
-
- 否则,如果对象具有
valueOf
方法,并且可以返回一个原始值,则 JS 就会将其转换并返回。
- 否则,如果对象具有
-
- 当1和2都行不通的时候,就会报错:
TypeError: Cannot convert object to primitive value
- 当1和2都行不通的时候,就会报错:
传统 VS es6新增的Symbol.toPrimitive
在上文中我们讲述了在es6之前我们传统的转换,但是从ES6,引入了一个新的方法------Symbol.toPrimitive
。其允许我们自定义对象到原始值的转换行为。并且在执行转换时,其优先级是高于传统的toString
和valueOf
的。
在引入 Symbol.toPrimitive
之前,有时会由于 toString()
和 valueOf()
的行为不够明确,导致意外的结果,特别是在不同的上下文中。因此,Symbol.toPrimitive
提供了一种更为直观和可控的方法来定义对象到原始值的转换规则。
自定义 toPrimitive 转换
在toPrimitive
内部方法接收一个提示(hint),这个提示可以是 "string"
、"number"
或 "default"
。而根据提供的提示,它会决定如何选择合适的对象方法来获取原始值。当然我们可以自定义一个toPrimitive
来改变转换的逻辑。这样就可以更精确地控制对象在不同上下文中的行为。
例如:
javascript
let obj = {
[Symbol.toPrimitive](hint) {
if (hint === 'string') {
return '[object Custom]';
} else {
return 42;
}
},
toString() {
return '[object Object]';
},
valueOf() {
return 43;
}
};
console.log(String(obj)); // "[object Custom]" 使用了 Symbol.toPrimitive 并传递了 'string' 提示
console.log(Number(obj)); // 42 使用了 Symbol.toPrimitive 并传递了 'number' 提示
补充:
在我学习这段知识点时,一直有一个疑惑,我们了解有toString
、valueOf
和Symbol.toPrimitive
,但是如果在对象字面量里面包含其他方法,那么其他方法不会对返还值产生影响吗?
而最后得到的结果就是:
在JavaScript中,当需要将一个对象转换为原始类型时,会按照以下步骤进行判断:
- 检查
Symbol.toPrimitive
:如果对象实现了Symbol.toPrimitive
,则直接调用它,并依据提示返回相应的原始值。 - 如果没有
Symbol.toPrimitive
,则按照传统的toPrimitive
内部机制工作:- 对于
string
提示,先尝试toString
,若未成功再试valueOf
。 - 对于
number
或"default"提示,先尝试valueOf
,若未成功再试toString
。
- 对于
- 如果
toString
和valueOf
都不行,就报错:cannot convert object to primitive value
即使我们在对象字面量添加其他方法,但不是Symbol.toPrimitive
,那么这个方法不会直接影响到对象到原始值的转换过程。
隐式类型转换:
在JavaScript中,我们日常编程有几种常见的隐式类型转换情况,想必大家并不陌生,例如:
- 在
if
语句或循环的条件中,一些值会被转换为Boolean类型。 - 在使用比较运算符时,有可能会将部分内容转换为数值比较。
- 在使用
+
运算符时,不单单进行加法运算,还有字符串拼接也会发生隐式转换。
而这里我们重点介绍使用+
运算符和==
运算符的情况。
一元运算符:
"+"运算符在进行一元运算时有个特性,那就是会默认进行一次toNumber转换
我们先来看最简单操作:
javascript
console.log(+ '12'); // 12
console.log(+ [1,2,3]); // NaN
很明显,默认对'12'
和[1,2,3]
进行了toPrimitive
操作,'12' => 12
,而[1,2,3] => 1,2,3
,由于1,2,3
不是一个有效的数字符串,那么返还NaN
,到这里还是很容易理解的。
那么再来看看以下操作:
javascript
console.log(+[]); //a
console.log(+['1']); //b
console.log(+['1,2,3']); //c
console.log(+['1,']); //d
console.log(+{}); //e
是不是开始头皮发麻了,这啥玩意呀?不急,让我们一个个来分析。
+[]
操作:- a、b、c、d我们可以归为一类,都是与数组操作,而当数组进行
toPrimitive
操作时,会先调用valueOf
,观察a、b、c、d,我们发现只有b是最有希望变为数字的,所以b返还的是1
。 - 再来看a、c、d,都需要进行
toString
操作,分别变成a: ""
,c: "1,2,3"
,d: "1,"
,在这其中我们发现,a是空字符串,那么就会进行隐式的Number("")
转换,得到0
,而其他两个进行隐式的Number
转换实在没办法变成有效的数字,那么就返还NaN
。
- a、b、c、d我们可以归为一类,都是与数组操作,而当数组进行
+{}
操作:- 一个空对象?管他什么对象我们没有对其进行诱导那么就先看能不能执行
valueOf
,然后就无功而返了,老老实实执行toString
操作,就会得到返还[object Object]
(可不要忘了前面讲的对象的toString
操作是返还格式为:[object class]
的结果哦),然后发现Number("[object class]")
只能返还NaN
了。 - 给个流程图:
{} ==(valueOf)==> {} ==(toString)==> [Object object] => NaN
- 一个空对象?管他什么对象我们没有对其进行诱导那么就先看能不能执行
所以我们最后得到的是:
javascript
console.log(+[]); //0
console.log(+['1']); //1
console.log(+['1,2,3']); //NaN
console.log(+['1,']); //NaN
console.log(+{}); //NaN
和你一开始思考的答案一样吗🫡?
二元运算符:
哎~又是"+"运算符,而当"+"运算符两端只要有一端不是数字,"+"就是做字符串拼接
但是我们有了一元运算符的前车之鉴,直接给它秒了:
javascript
console.log([] + []);
console.log([] + {});
console.log({} + {});
首先,管他三七二十一,你只要不是Primitive类型,先给我去进行toPrimitive
操作再说(新概念神了),[]
进行toString
变为""
,{}
也进行toString
变为"[object Object]"
,这步完成了,接下来就是最简单的字符串拼接了。所以答案为:
javascript
console.log([] + []); // (空)
console.log([] + {}); // [object Object]
console.log({} + {}); // [object Object][object Object]
当然如果都数字那就是正常的加法了。
==运算符:
在进行==运算符操作的规范实在太多了,为方便各位记忆,就分为以下情况。
-
相同类型比较:
- 如果
x
和y
是同一类型,则直接根据类型的特定规则进行比较:- Undefined 和 Null :如果两者都是
undefined
或null
,返回true
。 - Number :
- 如果
x
或y
是NaN
,返回false
。(不理解请看面试基础之JS类型转换(上)😎) - 如果
x
和y
都是+0
或-0
,无论符号如何,都视为相等,返回true
。 - 否则,如果
x
等于y
,返回true
;否则返回false
。
- 如果
- String :完全相等则返回
true
,否则返回false
。 - Boolean :只有当两者都为
true
或都为false
时,返回true
。 - Object :只有当
x
和y
引用同一个对象实例时,返回true
。
- Undefined 和 Null :如果两者都是
- 如果
-
不同类型的比较:
- Null 和 Undefined :如果一个为
null
另一个为undefined
,返回true
。 - Number/String :
- 如果有一端是
Number
,那么就将另一端进行toNumber
操作。 - 如果有一端是
NaN
,那么就为false
- 如果有一端是
- Boolean与其他类型 :
- 如果是Boolean,那么就默认进行
toNumber
操作。
- 如果是Boolean,那么就默认进行
- Object与其他类型 :
- 如果
x
不是字符串或数字而y
是对象,判断x == ToPrimitive(y)
。 - 如果
x
是对象而y
不是字符串或数字,判断ToPrimitive(x) == y
。
- 如果
- Null 和 Undefined :如果一个为
-
默认情况:
- 如果上述所有条件都不满足,则返回
false
。
- 如果上述所有条件都不满足,则返回
看完了吗?看完了就开始紧张刺激的实战了哦~
两端相同:
先来几个两端类型相同的开开胃
javascript
console.log([] == []);
console.log(NaN == NaN);
console.log(+0 == -0);
显而易见,由于两个数组的地址不同,所以不等;NaN 只要存在在一端就不等;+0和-0在==运算符看来是一样的(但是使用Object.is(+0, -0)
就是不等哦~它更严谨) 所以结果为:
javascript
console.log([] == []); // false
console.log(NaN == NaN); // false
console.log(+0 == -0); // true
两端不同:
接下来就是两端不等的了,看点稍微刁钻一点的:
javascript
console.log(true == '6'); // a
console.log(null == undefined); // b
console.log(2 == ['2']); // c
console.log(false == []); // d
console.log("" == [null]) // e
我们一个个来分析
- a:首先当我们看到有
Boolean
类型时,就直接将其转换为Number
就行了,那么就变成了1 == '6'
也就是1 == 6
当然是不同的 ===>false
- b:只要是
null
和undefined
进行比较,那么就是相等 ===>true
- c:首先将对象进行
toPrimitive
操作,['2']
变为了'2'
,式子也就变成了2 == '2'
,也就是2 == 2
,显然相同 ===>true
- d:先将
false
变为0
,即0 == []
,而[]
进行toPrimitive
操作后变为了""
,再由于是与Number
进行比较,所以对""
进行隐式的Number
转换,得到0
,即0 == 0
,显然相同 ===>true
- e:先将
[null]
进行toPrimitive
操作变为了""
,即"" == ""
,显然相同 ===>true
所以结果为:
javascript
console.log(true == '6'); // false
console.log(null == undefined); // true
console.log(2 == ['2']); // true
console.log(false == []); // true
console.log("" == [null]) // true
最后再来看一个不太一样的例子:
javascript
let x = 4;
let y = {
valueOf: function() {
return 4;
}
}
console.log(x == y);
聪明的小伙伴应该已经把这题秒了,答案是true
,对象字面量要进行valueOf
操作变为4
,即:4 == 4
显然相同。
---欢迎各位点赞、收藏、关注,如果觉得有收获或者需要改进的地方,希望评论在下方,不定期更新