字节前端面试必问之隐式类型转换
什么是类型转换
在 js 中,当运算符在运算时,如果两边数据不统一,CPU 就无法计算,这时我们编译器会自动将运算符两边的数据做一个数据类型转换,转成一样的数据类型再计算这种无需程序员手动转换,而由编译器自动转换的方式就称为隐式转换
例如:1 > "0"
这行代码在 js 中并不会报错,编译器在运算符时会先把右边的"0"
转成数字 0
然后在比较大小。
== 运算符隐式转换
==
运算符的规则规律性不是那么强,按照下面流程来执行,下面是对 es5 文档的简短总结,下面的规则更容易记。
==
用于一般比较,===
用于严格比较,==
在比较的时候可以转换数据类型,===
严格比较,只要类型不匹配就返回 flase
。
先来看看 ==
这兄弟:
强制是将值转换为另一种类型的过程。在这种情况下,==
会执行隐式强制。在比较两个值之前,==
需要执行一些规则。
假设我们要比较 x == y
的值。
-
如果
x
和y
的类型相同,则 JS 会换成===
操作符进行比较。 -
如果
x
为null
,y
为undefined
,则返回true
。 -
如果
x
为undefined
且y
为null
,则返回true
。 -
如果
x
的类型是number
,y
的类型是string
,那么返回x == toNumber(y)
。 -
如果
x
的类型是string
,y
的类型是number
,那么返回toNumber(x) == y
。 -
如果
x
为类型是boolean
,则返回toNumber(x)== y
。 -
如果
y
为类型是boolean
,则返回x == toNumber(y)
。 -
如果
x
是string
、symbol
或number
,而y
是object
类型,则返回x == toPrimitive(y)
。 -
如果
x
是object
,y
是string
,symbol
则返回toPrimitive(x) == y
。 -
剩下的 返回
false
速记方法:
- 如果两边都是基本数据类型, 并且有一个数字类型,那另外一个必须变成数字类型, 再进行恒等比较。
- 如果两边有一方是 引用类型,会对引用类型执行
toPrimitive(x)
转化为基本数据类型后进行比较。
基本的 7 种数据类型
undefined
:未定义,null
:空对象,boolean
,number
,string
,bigint
,symbol
2 种引用类型:Object
,Function
为何将数据类型划分为基本数据类型和引用数据类型?
这是因为它们在内存中的存储方式是不一样的。 基本数据类型由于占用空间不会太大,且具有相对固定的空间大小,因为是直接存放在栈(stack)中的。引用数据类型由于占用空间大、占用空间大小不稳定,如果频繁创建会造成性能问题,所以实体数据将存放到堆(heap)数据结构中,而在栈中只记录该数据的堆地址即可,使用的时候再根据堆地址去堆内存中查找。
toPrimitive 方法
上面提到的引用类型 会进行 toPrimitive
转化,这里进一步详解:
toString/valueOf
如果没有 Symbol.toPrimitive
,那么 JavaScript 将尝试寻找 toString 和 valueOf 方法:
obj[Symbol.toPrimitive] = function(hint) {
// 这里是将此对象转换为原始值的代码
// 它必须返回一个原始值
// hint = "string"、"number" 或 "default" 中的一个
}
- 对于
"string"
hint:调用toString
方法,如果它不存在,则调用valueOf
方法(因此,对于字符串转换,优先调用toString
)。 - 对于其他
hint
:调用valueOf
方法,如果它不存在,则调用toString
方法(因此,对于数学运算,优先调用valueOf
方法)。
这些方法必须返回一个原始值。如果 toString
或 valueOf
返回了一个对象,那么返回值会被忽略(和这里没有方法的时候相同)。
默认情况下,普通对象具有 toString
和 valueOf
方法:
toString
方法返回一个字符串"[object Object]"
。valueOf
方法返回对象自身。
如下面例子所示:
let user = {
name: "John",
money: 1000,
// 对于 hint="string"
toString() {
return `{name: "${this.name}"}`;
},
// 对于 hint="number" 或 "default"
valueOf() {
return this.money;
}
};
alert(user); // toString -> {name: "John"}
alert(+user); // valueOf -> 1000
alert(user + 500); // valueOf -> 1500
写出 a 的值,考察 == 中的隐式转换
这道面试最早出现在京东前端面试中
var a = {
i: 1,
toString: function () {
return a.i++;
}
}
if (a == 1 && a == 2 && a == 3) {
console.log("OK");
}
题解中: toString
换成 valueOf
也是一样的。
因为 ==
右侧是数字类型,因此会对a
对象调用 valueOf
方法,对象没有定义此方法,此时会调用toString
方法。因此每次对i
的值累计。
如果结果为:
var a = {
i: 1,
toString: function () {
return this.i++;
},
}
if (a == 1 && a == 2 && a == 3) {
console.log("OK");
}
这种方式依然是对的。 关于 this 作用域问题,在前面文章中有详细讲解。
[字节面试题] 写出==的对比
// 题目如下
Object.prototype.toString.call(Symbol)
// 返回值 "[object Function]" Symbol是一个函数类型
if([] == 0), [1,2] == "1,2", if([]), [] == 0 //面试会问,具体是怎么对比过程。
// if([] == 0) 比较过程如下:
// 1. 先对[]调用valueOf方法,返回[],并非基础数据类型.
// 2. 然后调用toString(),会执行[].join(',')得到 ''
// 3. 然后执行 Number('') 进行隐式类型转换为 0
// 4. 最后执行=== 操作。
// 因此结果为true
[1,2] == "1,2"
//结果为true,过程同上
if([])
// 会执行Boolean操作, 结果为false的有 下面7种
// 0, -0, NaN, null, undefined, "", false
[] == 0
// 结果为true,同样的原理
// 公司:头条 分类:JavaScript
// 请写出以下代码的打印结果 (这个恒等的问题得好好的看一下了)
// if([] == false){console.log(1)};
// 比较过程
// 1. 因为 false 先转化为 0,
// 2. [].toPrimitive, 结果为 ''
// 3. '' 执行 Number('') 结果为 0
// 4. 因此会输出1
if({} == false) {console.log(2)};
// 不会输出
// 1. false 变为0
// 2. {}.toPrimitive 是 "[object Object]"
// 3. Number("[object Object]") 结果为 NaN。
// NaN == 0 结果为false,命中的是 最后一条其他
if([]){console.log(3)};
//能输出 3,
// 有限的 6 个为 false 虚值[false,0,null,undefined,'',NaN]
if([1] == [1]){console.log(4)};
// 归属到其他的里面 都会 false
多加几道题进行练习
"0" == false; // true
false == 0; // true
false == ""; // true
false == []; // true
"" == 0; // true
"" == []; // true
0 == []; // true
NaN == NaN //false
+0 == -0 //true
如果需要加强可以接着思考
[] == ![] // true 因为先进性了Boolean操作
[] == 0 // true
[2] == 2 // true
['0'] == false // true
'0' == false // true
[] == false // true
[null] == 0 // true // [null].toString() 结果为 ''
null == 0 // false
[null] == false // true
null == false // false // 这个地方比较难,因为null是一个object类型,本质上是 NaN == 0 在比较。有些大厂喜欢这么问题!!
[undefined] == false // true
undefined == false // false // undefined 的类型就是 undefined类型。属于其他的false
隐式转换规则
-
转成 string 类型:
+
(字符串连接符)
-
转成
number
类型:++/-–
(自增自减运算符)+ - \* / %
(算术运算符)> < >= <= == != === !===
(关系运算符)
-
转成 boolean 类型:
!
(逻辑非运算符)
字符串连接符与算术运算符隐式转换规则混淆
常见面试题如下
console.log ( 1 + "true" );// ‘1true‘'
console.log ( 1 + true );//2
console.log ( 1 + undefined );// NaN
console.log ( 1 + null );//1
原理分析
此类问题的坑: 如恶区分字符串链接符号 还是 数字链接符?
- 字符串链接符:
+
的两边只要有一个是 string 类型,那么就行,把其他数据类型调用 String()方法转成字符串然后拼接 - 数字链接符号:
+
的两边都是数字类型。会把其他数据类型调用 Number()方法转成数字然后做加法计算
console.log ( 1 + "true" );//1true
//+是字符串连接符: String(1) + 'true' = '1true'
console.log ( 1 + true );//2
//+是算术运算符 : 1 + Number(true) = 1 + 1 = 2
console.log ( 1 + undefined );// NaN
// +是算术运算符 : 1 + Number(undefined) = 1 + NaN = NaN
console.log ( 1 + null );//1
// +是算术运算符 : 1 + Number(null) = 1 + 0 = 1
关系运算符
会把其他数据类型转换成 number 之后再比较关系 常见面试题如下:
console.log ( "2" > 10 );
//false
console.log ( "2" > "10" );
//true
console.log ( "abc" > "b" );
//false
console.log ( "abc" > "aad" );
//true
console.log ( NaN == NaN );
//false
console.log ( undefined == null );
//true
原理分析
console.log ( "2" > 10 );
//false
当关系运算符两边有一边是字符串的时候,会将其他数据类型使用 Number()转换,然后比较关系
Number('2') > 10 = 2 > 10 = false
console.log ( "2" > "10" );
//true
当关系运算符两边都是字符串的时候,此时同时转成 number 然后比较关系
重点:此时并不是按照 Number()的形式转成数字,而是按照字符串对应的 unicode 编码来转成数字
使用这个方法可以查看字符的 unicode 编码: 字符串.charCodeAt(字符下标,默认为 0)
'2'.charCodeAt() > '10'.charCodeAt() = 50 > 49 = true
常见面试题
//大坑
console.log ( [] == 0 );
//true
console.log ( ! [] == 0 );
//true
//神坑
console.log ( [] == ! [] );
//true
console.log ( [] == [] );
//false
//史诗级坑
console.log({} == !{});
//false
console.log({} == {});
//false
原理分析
- 关系运算符:将其他数据类型转成数字
- 逻辑非:将其他数据类型使用
Boolean()
转成布尔类型
- 以下八种情况转换为布尔类型会得到 false
- 0 、-0、NaN、undefined、null、''(空字符串)、false、document.all()
- 除以上八种情况之外所有数据都会得到 true
小结
字符串和数字
- "+" 操作符,如果有一个为字符串,那么都转化到字符串然后执行字符串拼接
- "-" 操作符,转换为数字,相减
(-a, a * 1 a/1)
都能进行隐式强制类型转换
布尔值到数字
1 + true = 2
1 + false = 1
转换为布尔值
for
中第二个, while
, if
, 三元表达式
, || (逻辑或)
&&
(逻辑与)
左边的操作数
符号(Symbol)
- 不能被转换为数字
- 能被转换为布尔值(都是 true)
- 可以被转换成字符串 "Symbol(cool)"