引言
写完 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显然相同。
---欢迎各位点赞、收藏、关注,如果觉得有收获或者需要改进的地方,希望评论在下方,不定期更新