01月02, 2017

JavaScript中的隐式类型变换

其实写这篇文章的起因是一道比较老的题目,在控制台直接打印++[[]][+[]]+[+[]]会输出10,原因是什么?直接搜索就会找到一大堆解释。

刚开始看到这道题我也比较懵,仔细看看,就会发现还是比较明了的。原本就这么过去了,昨天看到月影大大写了一篇关于为什么[]是false而!![]是true的文章,今天偶然在知乎上看到一个利用JavaScript隐式类型变换来输出各种字母的帖子,还找到了一个利用[]()!+六个字符写出任何JavaScript代码的github项目(这是一个很老的项目,自己太孤陋寡闻了吧)。发现自己还有很多地方理解不透,所以在此写一篇文章来总结、记录、分享一下。

首先至少你要知道JavaScript中原本就有六种数据类型,UndefinedNullBooleanStringNumberObjectES6中引入了一种新的数据类型Symbol,所以一共有七种。

我们先从上面提到的那道题目入手。++[[]][+[]]+[+[]],其实这里面考察了两个知识点,一个是运算符的优先级,一个就是隐式类型变换,当然主要的是隐式类型变换。

这道题目可以拆分为两部分++[[]][+[]][+[]]+[]返回0,所以++[[]][+[]]就相当于++[[]][0],也就是++[],直接运行++[]会报错是因为++a运算符相当于a = a + 1,后面跟着的必须是一个变量,[[]][0]返回的是对[]的引用等价于变量引用,而[]是数组对象本身,所以取前者。整个表达式其实最终就变换为1+[0],所以结果是10

toNumber

我们知道一元加号运算符相当于执行Number函数,也就是规范中的toNumber,我们先来看一下它的执行流程。

alt

前面六中类型转换结果比较简单,就不再赘述,比较复杂的是Object。我们看到在转换Object时,会调用一个ToPrimitive,我们来看看它是什么东西。

ToPrimitive

alt

我们看到除了Object,其它类型都原值返回。我们看看接下来看看对对象又是如何处理的?

alt

里面有一下名称、函数等大家可能不是很清楚。我们一步一步说一下它的流程:

①如果没有指定PreferredType,则使hint值为default

②如果PreferredTypehint String,则hint值为string

③如果PreferredTypehint Number,则hint值为number

④使exoticToPrim等于input@@toPrimitive方法

⑤如果exoticToPrim不是undefined,则

a.使`result`等于执行该方法的返回值
b.如果`result`不是对象则返回
c.否则抛出异常

注:这里的@@toPrimitive是指Symbol.toPrimitive,也就是说,如果我们转换的对象有`Symbol.toPrimitive方法,则会调用该方法。如下所示:

let obj = {};
obj[Symbol.toPrimitive] = function(){
    return 5;
}
Number(obj) // 5

// 接下来重写该方法让其返回一个对象
obj[Symbol.toPrimitive] = function(){
        return {};
}
Number(obj)
// Uncaught TypeError: Cannot convert object to primitive value

⑥如果hintdefault,则使hint值为number

⑦返回OrdinaryToPrimitive(input, hint)

接着是OrdinaryToPrimitive方法的执行流程:

①假设O是一个对象

②假设hint是字符串且值为stringnumber

③如果hint值为string,则使methodNames值为['toString','valueOf']

④否则使methodNames值为['valueOf','toString']

⑤先执行数组中第一个两个方法,如果返回值为非对象,则返回该值,否则执行第二个方法

⑥如果两个方法返回的值都是对象,则抛出TypeError异常

我们再来看之前的那个例子——Number([]),数组的valueOf方法返回的是自身,所以调用toString方法返回空字符串,Number('')则为0。

隐式类型变换操作

接下来,我们看几个会用到隐式类型变换的操作符

'+'运算符

我们知道+JavaScript中有多处用途——加法运算,连接字符串,强制转换为数字等。

+运算符在将两个变量相加时,是如何处理的呢?如下图:

alt

总体上分为以下几步:

①保存+左右两边内容到lvalrval,然后分别对其执行ToPrimitive,并分别记为lprimrprim

②如果lprimrprim中有一个是字符串,则都执行toString方法,然后连接两个字符串

③否则对lprimrprim执行toNumber操作并相加,具体流程这里略去

<运算符

其实该运算符有三种返回值——truefalseundefined

基本步骤如下:

①对xy进行ToPrimitive(x|y, hint Number)转换并保存为pxpy`

②如果pxpy都是字符串,则比较字符串

③否则,流程如下:

alt

规范中提到,如果xy中有一个值返回NaN,则表达式返回undefined,但我在chrome中试了,返回的依然是false

==运算符

流程如下:

alt

总结起来分为以下几种情形:

① 如果xy的类型相同,则执行完全相等操作

② 如果xy分别是nullundefined,则返回true

③ 如果xy当中一个是Number,一个是String,则把字符串转换为数字后再进行比较

④ 如果xy有一个是Boolean,则把该值转换为Number之后再进行比较

⑤如果xy当中一个类型是是NumberStringSymbol中的一种,而另一个的类型是Object,则对该对象进行ToPrimitive操作,然后再进行比较

⑥以上规则都不符合的话,则返回false

===运算符

流程如下:

alt

总结如下:

①如果xy类型不同直接返回false

②如果都是Number,则:

a.`x`、`y`中有一个是`NaN`,则返回`false`
b.`x`、`y`是同一个数值则返回`true`
c.`x`、`y`分别为`+0`和`-0`返回`true`
d.否则返回`false`

③返回SameValueNonNumber(x, y),该函数很简单,就是确定xy是不是相等值或指向同一对象

一元-运算符

相当于先执行toNumber记为oldValue,如果oldValue返回NaN,则返回NaN,否则返回oldValue的负值。

!运算符

执行toBoolean,如下:

alt

总结

总的来说,其实最重要的就是了解不同的操作符,都会优先进行什么隐式类型变换,以及记住常用的toNumberToPrimitivetoBoolean的变换结果是什么。

再回到我开始提到的用非字母字符输出字母的例子,其实就是获取利用隐式变换返回的字符串中某一个字母的位置。比如true中的r

!![]+[] // true
++[[]][+[]] // 1
(!![]+[])[++[[]][+[]]] // r

本文链接:https://www.imliutao.com/post/type-conversion.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。