JavaScript神奇之旅,不要在跟我说精通js了

1,022 阅读6分钟
原文链接: zhuanlan.zhihu.com

今天在空间timeline上看见一张图片,描述了JavaScript作为一门“神奇”的语言有着许许多多所谓的“特性“,下面让我们来一一探索一下。(参考书:犀牛书)

1. 抽风的数字类型

1.1. NaN

>   typeof NaN 
<·  "number" 

NaN是js预定义的全局变量,用以表示0/0的结果。而其被定义为number类型。也可以通过

new Number(NaN)

这样的操作来构建NaN对象。

补充说明:经过评论区指出,NaN的定义是由IEEE754浮点数标准直接给出的,这个和js半毛钱关系都没有。

关于typeof,还有一个有趣的结论应该就是

>   typeof null
<·  "object" 

这是由于null的开头第一位的内部编码和object一样的原因,不然就会是undefined

1.2. 迷之相等

>   9999999999999999
<·  10000000000000000
>   0.5+0.1==0.6
<·  true
>   0.1+0.2==0.3
<·  false

首先,js真的不能进行精确的计算,如果涉及精确的计算,建议自行设计高精度算法。要问为什么,那就是因为js没有专门的整数类型,它的所有number类型(NaN等奇怪的东西)除外,都是浮点数。

js完全采用IEEE754标准的64位方案来表示其数字,从而,根据犀牛书所指出的(也可以根据IEEE754标准推算),其能够准确表示的整数范围只到9007 1992 5474 0992 (这里正好16个数),而16个9,就有着被表示成1*10^16的危险。然而,在js它parseNumber的时候,在后面的幂大于20的时候才会显示成科学计数法。

>   999999999999999990000
<·  1e+21

整数部分尚且可以在二进制下精确表达,但是小数部分,则是真的很容易产生误差。二进制下的小数部分,只能精确表示能被拆分成(1/2)^n的和的部分,剩下的部分,就或多或少有着偷工减料了。

>   0.5+0.1
<·  0.6
>   0.1+0.2
<·  0.30000000000000004

这同时也意味着在处理小数是否相等时,js中需要进行的是差值在多少范围内的比较,而不能用相等直接比较。

1.3. 代劳了的初始化

>   Math.max()
<·  -Infinity
>   Math.min()
<·  Infinity

这个还真的不能算bug,平时题目做多了的人可能就注意到了这个用意,一般而言,最大最小值还真就是这么初始化的。为了方便得进行后续取最大和最小值的比较和判断。

2. 迷之类型转化

后面一整串,其实在犀牛书关于js的类型自动转化之中都有详细解答。所以都列在这里了:

>   []+[]
<·  ""
>   []+{}
<·  "[object Object]"
>   {}+[]
<·  0
>   true+true+true===3
<·  true
>   true-true
<·  0
>   true==1
<·  true
>   true===1
<·  false
>   (!+[]+[]+![]).length
<·  9
>   9+"1"
<·  "91"
>   91-"1"
<·  90
>   []==0
<·  true

但是首先,我们要来讲讲运算符。

+ 加号在js中可以用作number类型的加法运算,也可以用来拼接string。所以参与加法运算的两个参数都需要将数据类型转化为对应的number或者是string。

而js中,只要参与运算的对象不全是number类型,就会发生各种迷之操作。

首先,对象会去调用各自的valueOf(),用valueOf的结果来继续运算(valueOf的结果各种各样,但是会是js的四种基本类型 string、bool、number、object之一),但是不幸的是,一般对象都不会有这个玩意儿,而可以进行valueOf()并且产生number返回值(就是那个距离创世日的毫秒数)的日期对象,在这里会强制调用toString(),所以一般都会直接进入调用各自的toString()然后进行字符串拼接。(如果调用完valueOf()之后,加法两端可以进行数学意义上的加法,那么就会进行啦,不然还是字符串拼接)

js面对加法,没有不择手段的进行数学加法运算,而是不择手段的进行字符串拼接。

那么我们根据以上法则,一个个看过去。

[]+[]

[]数组的valueOf()会直接返回""空字符串,所以结果是空字符串,没有任何毛病。

[]+{}

[]的valueOf()依然会返回空字符串,这没有任何问题。

{}作为object其valueOf()的结果依然是object,因此就会调用toString(),产生返回值"[object Object]"。从而拼接字符串,产生结果。

{}+[]

那么你来说说,这个反过来加怎么就可以得到数字了呢。这个我做了下实验,发现如果是对对象进行赋值的再做相加操作的话,正着和倒着的结果是一样的。但是直接输入以上的式子,结果确实是0。那么这里的{}到底被当成什么了呢?花括号,在这里的意思已经不是对象了,而是我们平时用来框语句块的花括号,代表的是一组空白表达式。所以这里已经不是加法的世界了。相当于是在计算:

+[]

而+作为一元运算符的时候,右边的参数优先转化为数字,而[]会转化为0。

>   true+true+true===3
<·  true
>   true-true
<·  0
>   true==1
<·  true
>   true===1
<·  false

这一串其实,这么看下来应该很好解释,true在做加法运算的时候能够转化成数字(能够成功的valueOf)从而得到数字1。那么1+1+1自然是严格等于3的(毕竟类型转化了,大家都是number)。所以其实这组其实是非常正常的运算结果。

java的相等==是会考虑valueOf的,而===严格相等是不容含糊的(虽然也会出现各种神奇的结果)。

那么下面这个式子就自己品味了吧。

>   []==0
<·  true

而下面这组式子也很好解释:

>   9+"1"
<·  "91"
>   91-"1"
<·  90

加法优先考虑字符串连接,减法自然只存在强行计算。如果字符串转化成number失败,计算结果会是NaN。

那么最后来看看这个式子:

(!+[]+[]+![]).length

到底发生了什么

首先,逻辑非!有着极高的优先级,所以首先计算的是!+[]和![]。

+[]的结果我们刚刚已经知道了是数字0,对其做逻辑非运算会将数字0直接转化为false,然后再得到true

而对[]直接去逻辑非的结果呢?则是对[]直接转化为true再求逻辑非,得到false
那么式子就变成了

(true+[]+false).length

之后如何计算就无需多言了。

只是觉得其中有的内容确实不错,但是有的内容过于钓鱼,所以甘愿咬勾,写下这篇文字。

工作中摸鱼,但愿不会被炒。