本文通过17道JavaScript经典题目,深入解析类型转换的底层逻辑。内容涵盖显式与隐式转换的区别,String、Boolean、Number三种原始类型的转换规则,以及对象类型通过[[ToPrimitive]]方法转换为原始类型的机制。重点解析一元运算符、比较运算符及相等运算符在类型转换中的行为,帮助开发者理解并避免常见的类型转换陷阱。
前言
前言
类型转换是指将数据从一种数据类型转换为另一种数据类型的过程,例如将字符串转换为数字,或将对象转换为布尔值。在JavaScript中,无论是原始类型还是对象类型,都可以进行类型转换。JavaScript的原始类型包括:number、string、boolean、null、undefined和Symbol。
本文将通过17道题目来深入探讨JavaScript中的类型转换机制。阅读完本文后,你将能够自信地回答这些问题,并理解其背后的原理。文章末尾提供了答案及详细解释。在阅读答案之前,建议先记录下你的答案,最后进行对照,以便发现理解上的偏差。
true + false
12 / "6"
"number" + 15 + 3
15 + 3 + "number"
[1] > null
"foo" + + "bar"
"true" == true
false == "false"
null == ""
!!"false" == !!"true"
["x"] == "x"
[] + null + 1
[1,2,3] == [1,2,3]
{} + [] + {} + [1]
! + [] + [] + ![]
new Date(0) - 0
new Date(0) + 0
上述类型的问题在JavaScript面试中出现的频率极高,因此建议继续阅读以下内容。
隐式与显式类型转换
隐式与显式类型转换
类型转换主要分为隐式类型转换和显式类型转换两类。
当开发人员编写特定代码(如使用Number(value))来在类型之间进行转换时,这被称为显式类型强制转换(或强制类型转换)。
然而,JavaScript是一种弱类型语言,在某些操作下,值可以在两种类型之间自动转换,这称为隐式类型转换。当对不同类型的值使用运算符时,通常会发生隐式类型转换,例如1 == null, 2 / "5", null + new Date()。此外,当值被if语句包裹时也可能发生类型转换,例如if(value) {}会将value转换为boolean类型。
严格相等运算符(===)不会触发类型的隐式转换,因此它可以用于比较值和类型是否都相等。
隐式类型转换是一把双刃剑。虽然使用它可以减少代码量,但有时也会导致难以发现的bug。
三种类型转换
三种类型转换
我们需要了解的第一条规则是:在JavaScript中,只有3种类型的转换。
- to string
- to boolean
- to number
第二条规则是,类型转换的逻辑在原始类型和对象类型上有所不同,但它们最终只会转换成上述3种类型之一。
我们首先分析原始类型的转换过程。
String类型转换
String类型转换
String()方法可用于显式将值转换为字符串。隐式转换通常在包含+运算符且其中一个操作数是string类型时被触发,例如:
String(123) // 显式类型转换
123 + '' // 隐式类型转换
所有原始类型转换为String类型
String(123) // '123'
String(-12.3) // '-12.3'
String(null) // 'null'
String(undefined) // 'undefined'
String(true) // 'true'
Symbol类型转换为String类型较为严格,它只能进行显式转换。
String(Symbol('symbol')) // 'Symbol(symbol)'
'' + Symbol('symbol') // TypeError is thrown
Boolean类型转换
Boolean类型转换
Boolean()方法可用于显式将值转换为boolean类型。
隐式类型转换通常在逻辑判断或存在逻辑运算符(||、&&、!)时被触发。
Boolean(2) // 显示类型转换
if(2) {} // 逻辑判断触发隐式类型转换
!!2 // 逻辑运算符触发隐式类型转换
2 || 'hello' // 逻辑运算符触发隐式类型转换
注意:逻辑运算符(如||和&&)在内部会进行boolean类型转换,但实际上返回的是原始操作数的值,即使它们都不是boolean类型。
// 返回 number 类型 123,而不是 boolean 型 true
// 'hello' 和 '123' 仍然在内部会转换成 boolean 型来计算表达式
let x = 'hello' && 123 // x === 123
boolean类型转换的结果只有true或false两种。
Boolean('') // false
Boolean(0) // false
Boolean(-0) // false
Boolean(NaN) // false
Boolean(null) // false
Boolean(undefined) // false
Boolean(false) // false
任何不在上述假值列表中的值都会转换为true,包括object、function、Array、Date等。Symbol类型是真值,空对象和空数组也是真值。
Boolean({}) // true
Boolean([]) // true
Boolean(Symbol()) // true
!!Symbol() // true
Boolean(function() {}) // true
Number类型转换
Number类型转换
与Boolean()和String()方法类似,Number()方法可用于显式将值转换为number类型。number的隐式类型转换较为复杂,因为它可能在多种情况下被触发。
- 比较操作(>、<、<=、>=)
- 按位操作(|、&、^、~)
- 算术操作(-、+、*、/、%)。注意,当+运算符存在任意一个操作数是string类型时,不会触发number类型的隐式转换。
- 一元+操作
- 非严格相等操作(==或!==)。注意,当==运算符的两个操作数都是string类型时,不会发生number类型的隐式转换。
Number('123') // 显示类型转换
+ '123' // 隐式类型转换
123 != "456" // 隐式类型转换
4 > "5" // 隐式类型转换
5 / null // 隐式类型转换
true | 0 // 隐式类型转换
接下来查看原始类型显式转换为number类型时会发生什么。
Number(null) // 0
Number(undefined) // NaN
Number(true) // 1
Number(false) // 0
Number(" 12 ") // 12
Number("-12.34") // -12.34
Number("\n") // 0
Number(" 12s ") // NaN
Number(123) // 123
当将字符串转换为数字时,引擎首先会删除前导空格、\n、\t字符。如果被修剪后的字符串不是一个有效的数字,则返回NaN。如果字符串为空,则返回0。
Number()方法对null和undefined的处理方式不同:null会转换为0,undefined会转换为NaN。
无论是显式还是隐式转换,都不能将Symbol类型转换为number类型。当尝试这样做时,会抛出错误。
Number(Symbol('my symbol')) // TypeError is thrown
+Symbol('123') // TypeError is thrown
这里有2个特殊的规则需要记住:
1、当将==应用于null或undefined时,不会发生数值转换。null只等于null或undefined,不等于其他任何值。
null == 0 // false, null is not converted to 0
null == null // true
undefined == undefined // true
null == undefined // true
undefined == 0 // false
2、NaN不等于任何值,包括它自己。
NaN === NaN // false
if(value !== value) { console.log('the value is NaN') }
object类型转换
object类型转换
至此,我们已经深入了解了原始类型的转换,接下来我们来看一下object类型的转换。
当涉及对象的操作(如[1] + [2,3])时,引擎首先会尝试将object类型转换为原始类型,然后再将原始类型转换为最终需要的类型,且仍然只有3种类型的转换:number、string、boolean。
最简单的情况是boolean类型的转换,任何非原始类型总是会转换成true,无论对象或数组是否为空。
对象通过内部[[ToPrimitive]]方法转换为原始类型,该方法负责数字和字符串转换。
[[ToPrimitive]]方法接受两个参数:一个输入值和一个需要转换的类型(Number或String)。
number和string的转换都使用了对象的两个方法:valueOf和toString。这两个方法都在Object.prototype上被声明,因此可用于任何派生类,如Date、Array等。
通常[[ToPrimitive]]算法如下:
- 如果输入的值已经是原始类型,直接返回这个值。
- 输入的值调用toString()方法,如果结果是原始类型,则返回。
- 输入的值调用valueOf()方法,如果结果是原始类型,则返回。
- 如果上述3个步骤之后,转换后的值仍然不是原始类型,则抛出TypeError错误。
number类型的转换首先会调用valueOf()方法,如果不是原始值再调用toString()方法。string类型的转换则相反。
大多数JavaScript内置对象类型的valueOf()返回这个对象本身,其结果经常被忽略,因为它不是一个原始类型。因此,大多数情况下,当object需要转换成number或string类型时,最终都调用了toString()方法。
当运算符不同时,[[ToPrimitive]]方法接受的转换类型参数也不相同。当存在==或+运算符时,一般会先触发number类型的转换,再触发string类型转换。
在JavaScript中,你可以通过重写对象的toString和valueOf方法来修改对象到原始类型转换的逻辑。
答案解析
答案解析
接下来我们按照之前的转换逻辑来解释每一道题,看看是否和你的答案一样。
true + false // 1
+运算符会触发number类型转换,对于true和false。
12 / '6' // 2
算术运算符会把字符串'6'转为number类型。
"number" + 15 + 3 // "number153"
+运算符按从左到右的顺序执行,所以优先执行“number” + 15,把15转为string类型,得到“number15”,然后同理执行“number15” + 3。
15 + 3 + "number" // "18number"
15 + 3先执行,运算符两边都是number类型,不用转换,然后执行18 + “number”,最终得到“18number”。
[1] > null // true
==> '1' > 0
==> 1 > 0
==> true
比较运算符>执行number类型隐式转换。
"foo" + + "bar" // "fooNaN"
==> "foo" + (+"bar")
==> "foo" + NaN
==> "fooNaN"
一元+运算符比二元+运算符具有更高的优先级。所以+bar表达式先求值。一元加号执行字符串“bar”的number类型转换。因为字符串不代表一个有效的数字,所以结果是NaN。在第二步中,计算表达式'foo' + NaN。
'true' == true // false
==> NaN == 1
==> false
'false' == false // false
==> NaN == 0
==> false
==运算符执行number类型转换,'true'转换为NaN,boolean类型true转换为1。
null == '' // false
null不等于任何值,除了null和undefined。
!!"false" == !!"true" // true
==> true == true
==> true
!!运算符将字符串'true'和'false'转为boolean类型true,因为不是空字符串,然后两边都是boolean型,不再执行隐式转换操作。
['x'] == 'x' // true
==运算符对数组类型执行number转换,先调用对象的valueOf()方法,结果是数组本身,不是原始类型值,所以执行对象的toString()方法,得到字符串'x'。
[] + null + 1 // 'null1'
==> '' + null + 1
==> 'null' + 1
==> 'null1'
+运算符执行number类型转换,先调用对象的valueOf()方法,结果是数组本身,不是原始类型值,所以执行对象的toString()方法,得到字符串'',接下来执行表达式'' + null + 1。
0 || "0" && {} // {}
==> (0 || '0') && {}
==> (false || true) && true
==> true && true
==> true
逻辑运算符||和&&将值转为boolean型,但是会返回原始值(不是boolean)。
[1,2,3] == [1,2,3] // false
当运算符两边类型相同时,不会执行类型转换,两个数组的内存地址不一样,所以返回false。
{} + [] + {} + [1] // '0[object Object]1'
==> +[] + {} + [1]
==> 0 + {} + [1]
==> 0 + '[object Object]' + '1'
==> '0[object Object]1'
所有的操作数都不是原始类型,所以会按照从左到右的顺序执行number类型的隐式转换,object和array类型的valueOf()方法返回它们本身,所以直接忽略,执行toString()方法。这里的技巧是,第一个{}不被视为object,而是块声明语句,因此它被忽略。计算从+[]表达式开始,该表达式通过toString()方法转换为空字符串,然后转换为0。
! + [] + [] + ![] // 'truefalse'
==> !(+[]) + [] + (![])
==> !0 + [] + false
==> true + [] + false
==> true + '' + false
==> 'truefalse'
一元运算符优先执行,+[]转为number类型0,![]转为boolean型false。
new Date(0) - 0 // 0
==> 0 - 0
==> 0
-运算符执行number类型隐式转换对于Date型的值,Date.valueOf()返回到毫秒的时间戳。
new Date(0) + 0
==> 'Thu Jan 01 1970 02:00:00 GMT+0200 (EET)' + 0
==> 'Thu Jan 01 1970 02:00:00 GMT+0200 (EET)0'
+运算符触发默认转换,因此使用toString()方法,而不是valueOf()。
总结
查看原文
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对本站的支持。