字节前端面试必问之隐式类型转换

什么是类型转换

在 js 中,当运算符在运算时,如果两边数据不统一,CPU 就无法计算,这时我们编译器会自动将运算符两边的数据做一个数据类型转换,转成一样的数据类型再计算这种无需程序员手动转换,而由编译器自动转换的方式就称为隐式转换

例如:1 > "0"这行代码在 js 中并不会报错,编译器在运算符时会先把右边的"0"转成数字 0 然后在比较大小。

== 运算符隐式转换

== 运算符的规则规律性不是那么强,按照下面流程来执行,下面是对 es5 文档的简短总结,下面的规则更容易记。

==用于一般比较,===用于严格比较,==在比较的时候可以转换数据类型,===严格比较,只要类型不匹配就返回 flase

先来看看 == 这兄弟:
强制是将值转换为另一种类型的过程。在这种情况下,==会执行隐式强制。在比较两个值之前,==需要执行一些规则。

假设我们要比较 x == y 的值。

  • 如果 xy 的类型相同,则 JS 会换成===操作符进行比较。

  • 如果 xnull, yundefined,则返回 true

  • 如果 xundefinedynull,则返回 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)

  • 如果 xstringsymbolnumber,而 yobject类型,则返回x == toPrimitive(y)

  • 如果 xobjectystringsymbol 则返回 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 方法)。

这些方法必须返回一个原始值。如果 toStringvalueOf 返回了一个对象,那么返回值会被忽略(和这里没有方法的时候相同)。

默认情况下,普通对象具有 toStringvalueOf 方法:

  • 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 类型:

    1. +(字符串连接符)
  • 转成 number 类型:

    1. ++/-–(自增自减运算符)
    2. + - \* / %(算术运算符)
    3. > < >= <= == != === !=== (关系运算符)
  • 转成 boolean 类型:

    1. !(逻辑非运算符)

字符串连接符与算术运算符隐式转换规则混淆

常见面试题如下

console.log ( 1 + "true" );// ‘1true‘'
console.log ( 1 + true );//2
console.log ( 1 + undefined );// NaN
console.log ( 1 + null );//1

原理分析

此类问题的坑: 如恶区分字符串链接符号 还是 数字链接符?

  1. 字符串链接符: +的两边只要有一个是 string 类型,那么就行,把其他数据类型调用 String()方法转成字符串然后拼接
  2. 数字链接符号: +的两边都是数字类型。会把其他数据类型调用 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

原理分析

  1. 关系运算符:将其他数据类型转成数字
  2. 逻辑非:将其他数据类型使用 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)"
关于我
loading