一个6年的前端老兵对js隐式转换的思考

739 阅读7分钟

鸡汤

我觉得这个世界上没有什么毫无道理的横空出世,如果没有大量的积累,大量的思考,是不会把事情做好的,人可以不上学,但一定要学习,这世上有太多的能人,你以为的极限,弄不好,只是别人的起点。所以只有不停的进取,才能做到不丢人。 —— 韩寒

序言

我们都知道js中的数据类型强制转换包括显示隐式,那在日常的开发过程中,尤其是在条件判断的时候,经常会因为数据类型的显示跟隐式转换而导致判断条件不生效,从而引起一些列奇奇怪怪的bug。可能工作过五六年的老兵,对这块也未必达到熟练于心。所以,无论你是职场新人or是职场老兵,在你我都是平凡人智商的情况下,想要挖掘自己的技术深度跟广度,成为行业专家,笔者认为是需要经过反复的刻意练习不断的复盘才能达到的。

先来看两道道常见的面试题

第一道

null == 0 // false

null > 0 // false
 
null < 0 // false

null >= 0 // true
 
null <= 0 // true

[undefined] == false // true

undefined == false // false

[] == ![] // true

[] == [] // false

{} == {} // false

{} == !{} // false

[] == 0 // true

[2] == 2 // true

['0'] == false // true

'0' == false // true

[] == false // true

[null] == 0 // true

上述80%的比较看上去不难吧?是不是有几个有点晕?没关系,那我们再来一道。

第二道

 var a = ?;
 if (a == 1 && a == 2 && a == 3) {
   console.log('小样儿!');
 }

问:当a等于什么的时候,if条件成立,并打印?

这道题,如果大家对隐式转换有一定的认识跟积累的话,可能很快就有思路知道怎么去解答,相反,如果你没有积累的话,面试官问到你,大脑瞬间一定会有很多小星星。这道题的答案我们在最后在揭晓。

再来回顾一下js的数据类型

关于第一道题中的[] == []、{} == {}[] == ![]的结果,估计很多小伙伴都会产生疑问?所以,在揭穿这几个判断之前我们有必要再回顾一下js的基本数据类型以及存储。

js的基本数据类型:

  • string
  • boolean
  • number
  • null
  • undefined
  • symbol

还有一种较为复杂的数据类型:object

如果按照数据的存储方式来说的话,我们又可以把数据类型分为:值类型引用类型。值类型是存储在栈内存中的,而引用类型的地址是存储在栈内存中的,比如对象名,函数名等,然后通过指针指向存储在堆内存中的数据。

一张图

js中的堆与栈.png

// 基本数据类型 - 栈内存
let a1 = 123456;
// 基本数据类型 - 栈内存
let a2 = 'abcdef';
// 基本数据类型 - 栈内存
let a3 = true;
// 基本数据类型 - 栈内存
let a4 = null;
// 基本数据类型 - 栈内存
let a5 = undefined;

// 数组的指针存放在栈内存中,指针指向的数组存放在堆内存中
let c = [1, 2, 3];
// 对象的指针存放在栈内存中,指针指向的对象存放在堆内存中
let b = { a: 1 };

回顾完js类型的存储后我们再来看这道题

[] == []{} == {}

在js进行类型转换时如果两边都是引用类型,则直接比较内存中的地址(也就是指针指向的地址)

console.log([]==[]) // false,指针指向的地址不同

console.log({} == {}) // false,指针指向的地址不同

js隐式类型转换的规则

ToString是将其他类型转换为字符串类型的操作

ToString

carbon (2).png

解释一下上述转换:

  • null:转为"null"
  • undefined:转为"undefined"
  • 布尔类型:true和false分别被转为"true"和"false"
  • 数字类型:转为数字的字符串形式,如10转为"10", 1e21转为"1e+21"
  • 数组:转为字符串是将所有元素按照","连接起来,相当于调用数组的Array.prototype.join()方法,如[1, 2, 3]转为"1,2,3",空数组[]转为空字符串,数组中的null或undefined,会被当做空字符串处理
  • 普通对象:转为字符串相当于直接使用Object.prototype.toString(),返回"[object Object]"

ToNumber

ToNumber是将其他类型转换为数字类型的操作

carbon (3).png

解释一下上述转换:

  • null: 转为0
  • undefined:转为NaN
  • 字符串:如果是纯数字形式,则转为对应的数字,空字符转为0, 否则一律按转换失败处理,转为NaN
  • 布尔型:true和false被转为1和0
  • 数组:数组首先会被转为原始类型,也就是ToPrimitive,然后在根据转换后的原始类型按照上面的规则处理
  • 对象:对象首先会被转为原始类型,也就是ToPrimitive,然后在根据转换后的原始类型按照上面的规则处理

ToBoolean

ToBoolean是将其他类型转换为布尔类型的操作

carbon (4).png

解释一下上述转换:

  • null: 转为false
  • undefined:转为false
  • '':转为false
  • NaN:转为false
  • 0:转为false
  • []: 转为true
  • {}:转为true
  • Infinity:转为true

总结:

js中的假值只有false、null、undefined、空字符、0和NaN,其它值转为布尔型都为true

ToPrimitive

ToPrimitive指对象类型类型(如:对象、数组)转换为原始类型的操作。

两个非常重要的转换原则:

  1. 当对象类型需要被转为原始类型时,它会先查找对象的valueOf方法,如果valueOf方法返回原始类型的值,则ToPrimitive的结果就是这个值
  2. 如果valueOf不存在或者valueOf方法返回的不是原始类型的值,就会尝试调用对象的toString方法,也就是会遵循对象的ToString规则,然后使用toString的返回值作为ToPrimitive的结果。

carbon (5).png

解释一下上述转换:

  • Number([]), 空数组会先调用valueOf,但返回的是数组本身,不是原始类型,所以会继续调用toString,得到空字符串,相当于Number(''),所以转换后的结果为"0"
  • Number(['10'])相当于Number('10'),得到结果10
  • obj1valueOf方法返回原始类型100,所以ToPrimitive的结果为100
  • obj2没有valueOf,但存在toString,并且返回一个原始类型,所以Number(obj2)结果为102
  • obj3toString方法返回的不是一个原始类型,无法ToPrimitive,所以会抛出错误。

一张图总结

image.png

宽松比较==隐式转换的区别

通常我们在开发的过程中建议使用全等来做数据类型的比较,防止宽松相等在隐式转换所带来的各种各样的问题。

布尔类型和其他类型的相等比较

  • 只要布尔类型参与比较,该布尔类型的值首先会被转换为数字类型
  • 根据布尔类型ToNumber规则,true转为1,false转为0
false == 0 // true
true == 1 // true
true == 2 // false
const a = 10;
// a回被隐式转换为布尔类型true
if (a) {
    console.log('执行回调');
}

数字类型和字符串类型的相等比较

  • 数字类型字符串类型做相等比较时,字符串类型会被转换为数字类型
 0 == '' // true
 1 == '1' // true

对象类型和原始类型的相等比较

  • 对象类型原始类型做相等比较时,对象类型会依照ToPrimitive规则转换为原始类型
 [2] == 2 // true
  • 数组[2]是对象类型,所以会进行ToPrimitive操作,也就是先调用valueOf再调用toString,根据数组ToString操作规则,会得到结果"2", 而字符串"2"再和数字2比较时,会先转为数字类型,所以最后得到的结果为true

第一道题中特殊的一道比较分析:

[] == ![] // true
  1. !优先级比==高,先转右边,[]是对象类型,转成布尔值为true,!true就是false,即[] == false
  2. 右侧转成数字为0,即[] == 0
  3. 左侧是一个对象,valueOf()转出来不是字符也不是字符串,调用toString(),得到空字符串,即'' == 0
  4. 空字符串转成数字,即0 == 0 为true

我们再回头看第二道面试题分析:

 var a = {
    // 定义一个属性来做累加
    i: 1,
    valueOf () {
      return this.i++
    }
 };
 if (a == 1 && a == 2 && a == 3) {
   console.log('小样儿!');
 }

当然,如果我们没有定义valueOf方法的时候,也可以定义toString方法来达到上述题目的要求

 var a = {
    // 定义一个属性来做累加
    i: 1,
    toString () {
      return this.i++
    }
 };
 if (a == 1 && a == 2 && a == 3) {
   console.log('小样儿!');
 }

null、undefined、NaN跟原始类型作相等比较

  • null、undefined、NaN跟原始类型作相等比较时,不会发生隐式转换,比较的结果直接为false

回顾一下第一道面试题的前几道相等比较:

null == 0 // false

null > 0 // false
 
null < 0 // false

NaN == NaN // false

undefined == null // true

null == null // true

null == undefined // true

总结

  • 一个中心:左右两边转换成number为中心
  • 两个基本点:转换条件:1.类型不同时才转换 2.两边都是引用类型时直接比较地址
  • 一国两制:null、NaN、undefined使用一套制作,其它的使用另一套制度

images.jpeg