JavaScript隐式类型转换小结

762

本文首发于我的个人网站blog.skyline.ink,欢迎各位大大访问. 作者水平有限,文章仅供参考,不对的地方希望各位及时指正,共同进步,不胜感激

强弱类型

JavaScript强or弱

对于前端程序员来说,JavaScript可谓灰常强,但此强非彼强。根据维基百科的阐释,在计算机编程中,通俗地将语言分为强类型和弱类型,虽然没有精确的定义,但是强类型有非常严格的规则,包括变量定义时必须指定类型,使用时必须是期望的类型,否则报错或拒绝编译。维基百科上的类型强弱

JavaScript并不需要在定义时指定变量类型,同时使用时,不是期望的值会自动转换,弱类型相对于强类型来说类型检查更不严格,故而引出了今天的问题,自动转换的一些机制

转换规则

隐式转换为布尔值

如表
数据类型 转化成true 转化成false
String 非空字符 ""(空字符)
Number 非零 0与NaN
Object 非Null对象 null
undefined undefined

(注:调用Boolean()方法得到结果相同)


对象隐式转换规则

ToPrimitive

对于多数情况来说,对象隐式转换成字符串或数字,其实调用了一个叫做ToPrimitive(obj,preferredType)的内部方法来干这件事情,看了网上很多资料,都是balabala一大堆,规范里面文字也不少,其实调用这个方法转换的时候,除了date对象走转换数字流程(即preferredType值是number),其他走的都是转字符流程(即preferredType值是string),大概流程如下:

0E0B74FDA0F7561D3AF96126F84DD4B3.png-content

不同对象调用toString()得到的结果
对象 调用toString()
普通对象 "[object Object]"
数组arr arr.join()
函数类 定义函数的代码
日期类 可读日期
正则对象 正则对象字面量的字符
  • 如果数组的某一项的值是null或者undefined,join()方法返回的结果以空字符串连接
  • 基本包装类型的引用类型用其字面量形式的值调用toString()
不同对象调用valueOf()得到的结果
  • 大多数对象,包括普通对象、数组、函数、正则简单返回对象本身
  • 日期对象返回19700101以来的毫秒数值
  • 基本包装类型的引用类型返回其字面量形式的值

其他隐式转换规则

转化成字符串 转化成数字 转化成布尔值
undefined "undefined" NaN false
null "null" 0 false
NaN "NaN" NaN false
[](空数组) "" 0 true
""(空字符串) "" 0 false
  • 布尔值转数字为0/1,转字符串为"true"/"false"
  • 数字转字符串加引号即可🙄
  • 字符串转数字,看去掉引号是否是数字即可🤣,否则为NaN (注:假装这样表述是很严谨的🤣,数字与字符串的相互转换不用表述大家都知道的)

开始转换

常规加号

表达式中有字符串
  • 其他类型隐式转换为字符串
  • 多个加号时,按照从左到右的顺序,两两进行计算
  • 只要表达式中如果有字符串,最终结果一定是字符串
  • 如果有复杂类型,先将复杂类型按照对象隐式转换规则转换成字符串
2 + "3"; // "23"
1 + 2 + "3"; // "33"
true + 2 + "3"; // "33"
1 + "2" + 3; // "123"

"2" + true; //"2true"

"2" + undefined; //"2undefined"
"2" + NaN //"2NaN"
'23' + {'a': 1} //"23[object Object]"

'23' + [1,3,{}, null, undefined, '', '2'] // "231,3,[object Object],,,,2"
[1,3,{}, null, undefined, '', '2'].toString() //"1,3,[object Object],,,,2"
23 + "1,3,[object Object],,,,2" //"231,3,[object Object],,,,2"
表达式中没有字符串
  • 如果没有复杂类型,其他类型隐式转换为数字
  • 如果有复杂类型,先将复杂类型按照对象隐式转换规则转换成原始值再按照如上规则计算
1 + [] //"1"
1 + [1] //"11"
1 + {a:'a'} //"1[object Object]"
null + null //0
true + {a:'a'} //"true[object Object]"
  • 注意undefined 转化成数字是NaN
typeof NaN //"number"
null + undefined //NaN
1 + undefined //NaN

乘,除,取余,常规减

  • 其他类型会隐式转换为数字
1 - '5' //-4
1 - [2, 2] //NaN
1 - {a:1} //NaN
1- undefined //NaN
1 - [] //1
1 - [2, 2] //NaN
1 - null //1

一元加、一元减

  • 一元+运算符将其操作数转换为Number类型。一元减号同理但是反转正负
+ '3'      // 数字3
- '-3'      // 数字3

比较运算符部分 > < >= <= ==

  • 数字vs其他,其他转化为数字
  • 布尔值vs其他,布尔值转数字,数字vs其他
  • 字符串vs字符串,按unicode依次比较(大写字母总是在小写字母之后)
  • 对象vs数字,对象vs字符串,将对象转化为转换成原始值,再进行比较。
  • 如果其中一个操作数是NaN,那么总是返回false(NaN和NaN是不相等的)
  • null 只和undefined是好基友(互相相等)
var x = NaN;
x === NaN; // false

undefined == "undefined" // false
null == "null" // false
null == 0 // false
null == false // false
undefined == 0 // false
undefined == false // false

几个有意思的输出

以下内容纯属拓展,不感兴趣的童鞋可忽略

chrome/safari

[] + {} //"[object Object]"
{} + [] // 0
[] + {} === {} + [] // true
{} + [] === [] + {} // true
{a: 1} // {a: 1}
{a: 1}; // 1
{'a': 1} // {a: 1}
{'a': 1}; // SyntaxError
{} + 0 + {}; // "0[object Object]"
{} + 0 + {} // "[object Object]0[object Object]"

firefox

[] + {} //"[object Object]"
{} + [] // 0
[] + {} === {} + [] // true
{} + [] === [] + {} // false
{a: 1} // 1
{a: 1}; // 1
{'a': 1} // SyntaxError
{'a': 1}; // SyntaxError
{} + 0 + {}; // "0[object Object]"
{} + 0 + {} // "0[object Object]"

需要的其他知识

label语句

MDN:label {a: 1}相关的几个输出里,先忽略分号,{}分别被当做block和object literal,当被当做代码块时,入下所示:

//{'a': 1}
{
    "a": 1; // 语法错误
}

// {a: 1}
{
    a: 
        1;
}     // ^-- Automatic Semicolon Insertion
自动分号插入(ASI)

分号是否书写在前端领域来说,这个问题如同vi、emacs编辑器之争,最好计算机语言之争一般。在知乎上看了JavaScript 语句后应该加分号么?后,大致总结出来就是:

  • 写不写分号看项目风格与个人喜好
  • 书写分号也无法避免ASI带来的问题
  • 采用行首特例加分号的策略,一般只有行首是 [ ( + - / 五个符号之一在其前面加分号,这些地方的分号必须书写

说回ASI,

官方规范中的ASI

官文太抽象,网上大神的翻译大多数更加抽象,根据网上各种资料来看,总结用口水话说就是代码块最后一条语句自动插入,换行时候大多数在语句末尾自动插入,除了:

  • 该语句不是有效结束一个语句的方式。(比如以 . 或 , 或:结尾)
  • 该行是 -- 或 ++(将减量/增量的下一个标记)
  • for、while、do、if 或 else,且之后没有 {
  • 下一行以 [、(、+、*、/、-、,、.或一些其它在单个表达式中两个标记之间的二元操作符。主要遇到[ ( + - /
{}是代码块还是对象?
Abstract Syntax Tree 抽象语法树(AST)

wikipedia: Abstract_syntax_tree

In computer science, an abstract syntax tree (AST), or just syntax tree, is a tree representation of the abstract syntactic structure of source code written in a programming language. Each node of the tree denotes a construct occurring in the source code. The syntax is "abstract" in not representing every detail appearing in the real syntax. For instance, grouping parentheses are implicit in the tree structure, and a syntactic construct like an if-condition-then expression may be denoted by means of a single node with three branches.

其实就是将源代码分析成所对应的树状结构,便于之后的语法分析,代码检查等。现在的很多热门工具如webpack、vue、UglifyJS、Lint等都会用到这个技术,各个浏览器引擎也会使用自家定义的一套语法书生成树规范,生成相应的语法树。

分析

其实由于浏览器厂商众多,每个与解析情况不一致,平常代码中基本不会遇到{}+这种问题,我们也没有精力研究各厂商预解析源码,从Chrome和Firefox来看,总结出来有下面几点:

  • {}的前面有运算符号的时候,{}都会被解析成对象字面量。
  • {}前面没有运算符时候但有;结尾的时候,{}都会被解析成代码块。
  • {}前面什么运算符都没有,{}后面也没有分号(;)结尾
    • Firefox会始终如一的解析为代码块
    • chrome在这种情况下需要被扒一下历史

大概在chrome版本49之前,Chrome控制台上面的输出结果基本和Firefox一致,之后在chrome上有人提出bug,Issue 499864,大概意思就是说我在控制台输入{a: 4, b: 5}你给我报个错干嘛,我就是想要一个对象而已。Chrome确实该近几年大火,没过多久就修复了,修复的方式也特别666,就是凡是语句以{开头,以}结尾,我解析的时候就包裹一层括号在外面。git记录,里面的关键代码如下:

+    if (/^\s*\{/.test(text) && /\}\s*$/.test(text))
+        text = '(' + text + ')';

也就是说{} + 0 + {}其实是({} + 0 + {}), {a: 1}其实是({a: 1}),也就是说在Chrome中,凡是语句以{开头,

  • 以}结尾,语句里第一个{}是对象
  • 不以}结尾,语句第一个{}是代码块
最后用图来看一下AST

以{} + 0 + {}为例来看

  • Chrome

此时,Chrome将第一个{}解析成对象

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/3/22/1624bc2968b4f4c4~tplv-t2oaga2asx-image.image

  • firefox

此时,firefox将第一个{}解析成代码块

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/3/22/1624bc2968b4f4c4~tplv-t2oaga2asx-image.image

看AST的网站

分析之后不难得出如上的结果


参考资料

《JavaScript高级程序设计》