由一道奇奇怪怪的js加法引发的

当当~当当!

{}+[]和[]+{}这个问题眼熟吗?

说实话第一眼看起来就像是什么老油条搜肠刮肚然后狡黠一笑写出来坑人的,仿佛是茴香豆的茴字有几种写法属于同一类的问题。

然而,本着通过奇奇怪怪问题学一点底层知识的想法,我还是看了下去。其中的原理还是有一定营养,从这一道题可以牵扯出js基本类型、valueOf、toString、通过Object.toString.call判断类型、包装类型、类型转换、原型链等一干知识,堪称大串联了。

我最初是在百度百家号(我也不知道这算什么鬼)看到了这个问题,在查阅资料的过程中有csdn、博客园一票博客网站,也有知乎、stackoverflow等问答社区。越发觉得社区质量高低对于知识的新鲜度和正确性有很大的影响。如何保证自己的知识始终新鲜,始终尽量正确,我认为这需要通过一套系统科学的学习方案,至于这个方案到底应该如何,我也在不停思考,目前所使用的最重要的三个技巧是多方查证+亲手实践+往里深入一点点。

这个问题,使我想到了之前一个遇到了同样的情况但最终没有在收藏夹找出来之前的历史,也从而使我意识到了这种碎片知识记录组织的重要性。

闲话不多说,我们来直接看。

{}+[]

这种情况比较简单,对于{}块,js的解释器是倾向于认为这是一个block而非一个字面量的对象,所以前边的{}直接被忽略掉了,剩下的是+[],这里的加法相当于一个正负的正号,相当于把[]转换为一个数字,这里Number([])===0,最终结果是0。

这里贺师俊的解释是对一个空数组执行正号运算,实际上就是把数组转型为数字。首先调用 [].valueOf() 。返回数组自身,不是primitive value,因此继续调用 [].toString() ,返回空字符串。空字符串转型为数字,返回0,即最后的结果。

我目前并不理解把数组转型为数字为什么要经过这样的流程,这个空字符串转型为数字0我认为才是这个问题的核心。因为对于非空数字如[1,2].valueOf()返回的是[1,2],接着toString()返回”1,2”,最后转型为数字的结果是NaN,从这个分析过程和空数组的对比可以发现实际上最根本的区别是Number(字符串)返回的过程。

至于通过Number进行强制类型转换,我找到了以下规则:

  • 如果是布尔值,true和false分别被转换为1和0。
  • 如果是数字值,返回本身。
  • 如果是null,返回0。
  • 如果是undefined,返回NaN。
  • 如果是字符串,遵循以下规则:
    • 如果字符串中只包含数字,则将其转换为十进制(忽略前导0)
    • 如果字符串中包含有效的浮点格式,将其转换为浮点数值(忽略前导0)
    • 如果是空字符串,将其转换为0
    • 如果字符串中包含非以上格式,则将其转换为NaN
  • 如果是对象,则调用对象的valueOf()方法,然后依据前面的规则转换返回的值。如果转换的结果是NaN,则调用对象的toString()方法,再次依照前面的规则转换返回的字符串值。

现在再回头看看贺老的说法,是不是完全没毛病了?

再加一点补充,Boolean转换规则:

  • String:非空字符串为true
  • Number:除0和NaN都为true
  • Object:除null外任何对象都为true

再补充一点点,==判断时会进行类型转换,而===不会。

对于==而言,undefined和null相等。字符串和数值比较时需把字符串转换成数值。

对于===而言,值得注意的是NaN和谁包括其自身都不相等。

[]+{}

上面提到了valueOf和toString,这里再来介绍下他们。

先看问题本身,实际上这个问题和上个问题还是有一定的区别的。这个问题考察js的加法怎么运行,而上个问题虽然出现了加法符号但是实际作为正号用途,所以我说实际上他们还是有一定区别。

想一想,加法对于数字类型和字符串类型似乎还比较好理解,其他类型比如对象和数组他们的加法似乎很难定义,js的内部在做加法的时候实际上是执行了一个隐式的类型转换的,它会把两边都转换成字符串或者数字,如果转换的结果两边有至少一个是字符串,结果为字符串,否则是数字相加。

这个转换过程靠的是valueOf和toString。

有文章提到内部有一个这样的运算方法ToPrimitive能够把复杂类型转换为基本类型。
当它的参数PreferredType为number时,先进行valueOf再进行toString;如果参数为string时,先进行toString再进行valueOf。如果没有指定这个参数,出了Date外,其他都按照number处理,也就是先valueOf。很简单的原则对不对,但是valueOf和toString都是什么样的规则呢?

网上有的地方对于这两者的返回值出现了一些冲突,于是我自行进行了验证。

valueOf

  • Array,返回本身
  • Boolean,返回本身
  • Date,返回毫秒数
  • Function,返回本身
  • Number,返回本身。注意了,这里如果是直接1.valueOf是会报错的:SyntaxError: No identifiers allowed directly after numeric literal,这种语法不被支持,通过var a=1;a.valueOf()或者Number构造是可以的
  • Object,返回本身
  • String,返回本身

在网上对于valueOf普遍的认为其返回了包装类型的原始值,当然,对于非包装类型的话它们返回本身,特别地是Date。从上述结果来看,这个结论是可信的。至于包装类型,又是另外一个话题了,在这里不多赘述,我曾在对包装类型进行类型判断时产生了很大困惑,最终查阅资料得以解决。以我之所见,掌握包装类型只需要两个重要的点,一是它的生存期,二时经验性得出的一个结论,凡是通过.进行访问的都是会调用包装类型,这点至少在类型判断上有奇效。

toString

  • Array,相当于执行了join(‘,’)操作
  • Boolean,返回”true”或者”false”
  • Date,返回日期的字符串表示,比如”Fri Mar 30 2018 00:33:31 GMT+0800 (CST)”
  • Function,返回函数定义的字符串
  • Number,返回数值的字符串,可以通过传递参数进行进制的转换
  • String,返回其值
  • Object,返回形如”[object ObjectName]”这样的字符串

从toString的直观翻译来看,它就应该是把数据类型转换为字符串的方法,这跟实验结果确实契合。看到这个方法我想大多数人都会想到通过Object.prototype.toString.call()进行类型判断的操作,为什么会有这种方案,并且为什么要使用Object的而非使用自身的toString?

对于问题一,这种解决方案可以返回一个”[object ObjectName]”字符串,而ObjectName就是对象类型的名称。

对于问题二,通过上述例子,也可以猜得出来,这个toString应当是各个对象自己写的了,如果没有自己写,他会继承Object直接返回”[object Object]”。而对于Array这种,虽然自己定义了,但并不能表现自己的类型,所以也不能使用。

再回到问题本身,两边分别变成了””+”[object Object]”。结果不必多说了。

not found!