那些 JavaScript 自带的奇妙 Bug

米娜桑,哦哈哟~
本章讲解关于 JavaScript 奇妙的 Bug,与其说是Bug,不如说是语言本身隐藏的奥秘。接下来就看看可能会影响到我们编程的那些Bug吧。

typeof null === "object"


官方自带的Bug,typeof 操作符会返回对应操作数类型的字符串 表示,唯独 null,返回object文档解释说:

在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于?null?代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null?也因此返回?"object"

指针是一个变量,其储存的值是一个地址,一般空指针储存的地址为 0x00;对象的引用也是一个变量,储存的值也是一个地址,比如:0x000001、0x000002。
而JavaScript会根据实际数据储存值得到标签类型,那么 typeof null === ‘object‘

NaN !== NaN


如果 var a = NaN , 那么 a !== a
这是 NaN 本身的一个特点,也只有NaN,比较之中不等于它自己。
所以判定是否为NaN有三种方式

isNaN(a) //为NaN或者强制转换为数字后是NaN时,则返回true
Number.isNaN(a) //仅当为NaN时为true
a !== a //成立的唯一情况是 a 的值为 NaN

[ ]!==[ ] , [ ]==[ ]


对于前半段 []!==[]

一个变量在内存中都需要一个空间来存储,而内存会根据其类型分配到栈内存(stack)或堆内存(heap)。
最新的 ECMAScript标准 定义了 8?种数据类型:

  • null:空指针对象
  • Undefined:未定义
  • Boolean:布尔值
  • Number:整数、浮点数、特殊值(InfinityNaN
  • String:字符串
  • Symbol:一种实例是唯一且不可改变的数据类型
  • BigInt:一种用于表示任意精度格式的整数的数据类型
  • object:对象
    除了前面7种原始数据类型在声明变量时是储存在栈内存,而对象类型则是在栈内存中存储一个引用地址,该地址指向堆内存的值。
let num = 1,
    arr1 = [],
    arr2 = [],
    arr3 = arr2
    
    console.log(arr1 === arr2) //false
    console.log(arr2 === arr3) //true

那些 JavaScript 自带的奇妙 Bug
内存分配如图,虽然它们在堆内存中分配在不同位置的储存的值是等价的。但进行 arr1 === arr2 判断时会对比它们的引用地址,故不为真。
arr2 === arr3 其实本质就是 arr2 === arr2,因此,该改变arr3,其实就是改变arr2,这就引申出浅拷贝深拷贝的话题

对于后半段 []==![]

以相等操作符 == 比较两个值是否相等,在比较前将两个被比较的值转换为相同类型,即隐式转换。转换规则如下,详情请查看 相等性判断 文档
那些 JavaScript 自带的奇妙 Bug

根据上述规则,转换过程如下:

[] == ![]
[] == false //优先执行逻辑非操作,返回false
''== 0 //[]使用toString转换;false转换为数字
0 === 0 //''转化为数字,进行全等判定,比较完毕 。

所以,大多数情况下,不建议使用 == 判定,使用 === 结果更容易预测。由于没有进行隐式转换,=== 评估更快(虽然影响微小)

0.1 + 0.2 !== 0.3


这不仅是JavaScript的Bug,也是计算机语言的“通病”。
从最底层的电路来讲,一般电路通过给电子器件施加电压,根据其电压高低状态(高电平、低电平),也就是所谓的电子器件开关,来实现二值数字逻辑,即0和1。而正是这种方便快捷的状态,决定了计算机采用二进制进行运算。

而小数的二进制大多为无限循环的,如果用每个电子器件开关的状态对应记录这些无限循环的二进制数字,这显然是不可能的。
最终根据IEEE 754标准,0舍1入,使用64位固定长度来表示(ECMAScript?语言规范有所提及),所以有如下结果:

(0.1).toString(2)
//"0.0001100110011001100110011001100110011001100110011001101"

(0.2).toString(2)
//"0.001100110011001100110011001100110011001100110011001101"

(0.3).toString(2)
//"0.010011001100110011001100110011001100110011001100110011"

而0.1、0.2进行二进制加运算得到的结果应为

//0.0100110011001100110011001100110011001100110011001100111
//对应十进制为 0.30000000000000004
//因此 0.1 + 0.2 !== 0.3

a === 1 && a === 2 , a == 1 && a == 2


看似荒谬的比较,为什么会存在 a 能满足上述条件呢?这得分开讨论。

对于前半段 a === 1 && a===2

如果 a 是原始数据类型,那上述的全等判定就不能成立,所以 a 就需要通过函数进行变形,那么 a 是一个返回1或着2的函数对象。
这个时候我们可以利用 getter 将对象属性绑定到查询该属性时将被调用的函数。而其对象正是 window 对象。
不难得出

let i = 1  
Object.defineProperty(window, 'a', {  
    get() {  
        return i++ 
    }  
})
console.log(a === 1 && a === 2) //true
//当访问 window.a 时候则会实行 a 函数

对于后半段 a === 1 && a===2

在提及 []==![] 讨论过,当比较的两者类型不一致的时候,将进行隐式转换。对应的值会执行其内置的 ToPrimitive() 函数操作:
1、进行 valueOf(),如果得到的为原始数据类型(如Date类型会得到对应的数字),则返回对应原始值,否则进行第2步。
2、进行 toString() ,返回对应原始值,如果失败,抛出 TypeError

let a = {  
    i: 0,  
    valueOf() {  
        return this.i += 1  
    }  
}  
console.log(a == 1 && a == 2) //true

相关推荐