前言
JavaScript作为一门弱类型语言,其变量可以任意赋值从而转换类型,这既是优点也有缺点。如果开发者明白自己的赋值或操作会引发类型转换,那么也就无所谓了,但很多情况下开发者是不清楚自己的操作可能引发隐式类型转换,这个就有点危险,而且还不好问题定位时麻烦多多。 比如下面这张来源于Here的图,相信很多人都会感谢Brendan Eich(布兰登 · 艾克),哈哈。
笑过之后我们开始揭开JavaScript隐式转换的一角,先了解一些预备知识,然后对这些题目解疑,最后进行一些拓展
预备知识
==弱相等运算符
规范文档Abstract Equality Comparison的翻译过来就是下面的:
- 如果
x非正常值 (比如x本身会抛出错误),则中断执行- 如果
y非正常值 (同上),则中断执行- 如果
x的数据类型和y的数据类型相同,则返回以严格运算符执行判断的结果,即x===y的结果- 如果
x是null,y是undefined,返回true- 如果
x是undefined,y是null,返回true- 如果
x的数据类型是Number,y的数据类型是String,则将y转成Number,然后返回x==toNumber(y)的结果- 如果
x的数据类型是String,y的数据类型是Number,则将x转成Number,然后返回toNumber(x)==y的结果- 如果
x的数据类型是Boolean,则将x转成Number,然后返回toNumber(x)==y的结果- 如果
y的数据类型是Boolean,则将y转成Number,然后返回x==toNumber(y)的结果- 如果
x的数据类型是String、Number或者Symbol,y的数据类型是Object,则将y转成原始类型,然后返回x==toPrimitive(y)的结果- 如果
x的数据类型是Object,y的数据类型是String、Number或者Symbol,则将x转成原始类型,然后返回toPrimitive(x)==y的结果- 返回
false
ToNumber
规范文档ToNumber翻译过来就是:
参数类型 结果 完成标志 ( 例如 return、break、throw等)如果参数是一个异常中断,就返回这个参数,否则就返回该参数转换成 Number之后的数值Undefined返回 NaNNull返回 +0Boolean如果参数是 true,返回1;如果参数是false,返回+0Number返回参数 (不做转换) String见 StringToNumberSymbol抛出一个 TypeError异常Object采用下述的步骤:
- 利用
ToPrimitive(argument,hint Number)的方式转成原始类型- 将上述步骤的原始类型转成数值,即
ToNumber(primValue),并返回该数值 |
StringToNumber
规范文档ToNumber Applied to the String Type里面东西有点多,感兴趣可自行跳转查阅,这里简单的说一下:
- 如果字符串中只包含数字(包括前面带加号或负号的情况),则将其转换为十进制数值,即
"1"会变成1,"123"会变成123,而"011"会变成11(注意:前导的零被忽略了);- 如果字符串中包含有效的浮点格式,如
"1.1",则将其转换为对应的浮点数值(同样,也会忽略前导零);- 如果字符串中包含有效的十六进制格式,例如
"0xf",则将其转换为相同大小的十进制整数值;- 如果字符串是空的(不包含任何字符),则将其转换为
0;- 如果字符串中包含除上述格式之外的字符,则将其转换为
NaN。
ToPrimitive
规范文档ToPrimitive。东西比较多,简单概述就是。ToPrimitive(input[, preferredType])函数接受两个参数,第一个input为被转换的数据,第二个preferredType为希望转换成的类型(默认为default,接受的值为Number或String)。如果input不是Oject,即基础类型 (Null, Undefinded, String, Boolean, Number,Symbol, 以及将来纳入规范的BigInt) 时,直接返回输入input。如果input为Object时,在第二个参数为空的情况下,并且input为Date的实例时,此时preferredType会被设置为String,其他为空的情况preferredType默认为Number。
如果preferredType为Number,ToPrimitive执行过程如下:
- 如果
obj为原始值,直接返回;- 否则调用
obj.valueOf(),如果执行结果是原始值,返回之;- 否则调用
obj.toString(),如果执行结果是原始值,返回之;- 否则抛异常。
如果preferredType 为 String,将上面的第 2 步和第 3 步调换,即:
- 如果
obj为原始值,直接返回;- 否则调用
obj.toString(),如果执行结果是原始值,返回之;- 否则调用
obj.valueOf(),如果执行结果是原始值,返回之;- 否则抛异常。
ToBoolean
规范文档ToBoolean,概述一下就是:
undefined、null直接返回falseSymbol、Object永远返回true- 本身就是
Boolean型直接返回原本值Number型的除+0、-0和NaN返回false以外,其余都返回trueString型中空字符串 (即长度为0) 返回false,其他情况返回true。
解疑
1. typeof NaN //"number"
NaN,即Not a Number,不是一个数字,它本身是Number类型的,所以利用typeof判断类型是自然返回"number"。但是NaN又是一个特殊的Number类型,它永远为假,同时自身也不等于自身。所以有时候在判断一个变量是不是NaN时,可以通过是否等于自身来判断。
function isNaN(value) {return !value == value;}isNaN(NaN);
2. 9999999999999999 //10000000000000000
JavaScript针对数值,只有Number类型,采用64bits双浮点精度,如下图所示。
- 第
1位:符号位,0表示正数,1表示负数 - 第
2位到第12位(共11位):指数部分,0~2047 - 第
13位到第64位(共52位):小数部分(即有效数字)
因为有效位数只有53位,所以JavaScript能精确表示的最大整数就是Math.pow(2, 53),十进制就是9007199254740992,在JavaScript也设置了Number.MAX_SAFE_INTEGER(最大安全整数)和Number.MIN_SAFE_INTEGER(最小安全整数)。当JavaScript在存储超过9007199254740992的数值时,可能会存在精度丢失的情况(超过52位的会被自动去掉)。例如:
以下为摘录阮一峰的文章:
- 符号位决定了一个数的正负,指数部分决定了数值的大小,小数部分决定了数值的精度。
- 指数部分一共有
11个二进制位,因此大小范围就是0到2047。IEEE 754规定,如果指数部分的值在0到2047之间(不含两个端点),那么有效数字的第一位默认总是1,不保存在64位浮点数之中。也就是说,有效数字这时总是1.xx...xx的形式,其中xx..xx的部分保存在64位浮点数之中,最长可能为52位。因此,JavaScript提供的有效数字最长为53个二进制位。(-1)^ 符号位 1.xx…xx 2^ 指数部分- 上面公式是正常情况下(指数部分在
0到2047之间),一个数在JavaScript内部实际的表示形式。- 精度最多只能到
53个二进制位,这意味着,绝对值小于等于2的53次方的整数,即-253到253,都可以精确表示。
3. 0.1+0.2 == 0.3 //false
同样的这也是因为JavaScript数值精度丢失的原因导致等于判断为false。JavaScript本身也考虑到了这一点,所以设置一个可接受的误差范围,即Number.EPSILON,当两个值的差值小于等于这个可接受误差范围时,就可以认为这两个数值时相等的。
function isEqual(num1, num2) {return Math.abs(num1-num2) <= Number.EPSILON;}isEqual(0.1 + 0.2, 0.3);
4. Math.min() 和 Math.max()
查看规范文档可知,当Math.min()方法无参数时返回Infinity,而Math.max()无参数时返回-Infinity。
JavaScript能够表示的最大数值和最小数值分别为Number.MAX_VALUE和Number.MIN_VALUE中,在大多数浏览器中,它们分别是1.7976931348623157e+308和5e-324。可以看到,它们都是正数,是绝对值中的最大和最小数值。
还有比他们更大或者更小的值,当计算结果得到一个超过数值范围的值,那么就会转成Ifinity(正无穷)和-Infinity(负无穷)
JavaScript也提供了 2 个属性保存这两个无穷值,分别是Number.NEGATIVE_INFINITY(负无穷)和Number.POSITIVE_INFINITY(正无穷)
5. []+[]、[]+{}
下面的解释引自《JavaScript 高级程序设计第三版》:
在使用一元操作符(如 +、-、++、—)时,
JavaScript存在隐式类型转换。
- 在应用于一个包含有效数字字符的字符串时,先将其转换为数字值
- 在应用于一个不包含有效数字字符的字符串时,将变量的值设置为
NaN- 在应用于布尔值
false和true是,分别转换为0和1- 在应用于对象时,先调用对象的
valueOf()方法,取的一个可供操作的值。如果结果为NaN,就在调用toString()方式转成字符串,在执行前面的操作。
上述表述就是ToPrimitive的另一种翻译。未被重定义的情况下valueOf() 方法返回指定对象的基础类型值;toString() 方法返回一个表示该对象的字符串。
在这里[]和{}都是引用类型,是对象,所以先调用valueOf方法,后面调用toString方法。
第一个[]+[],Array对象重写了Object的valueOf方法和toString方法,valueOf返回数组本身,toString返回与没有参数 (默认为逗号拼接) 的 join() 方法返回的字符串相同。所以这里先返回了数组本身,无法进行+操作,在调用toString方法,变成了空字符串,两个空字符串相加,所以最后输出空字符串。
第二个[]+{},[]最终转成空字符串,+ 运算实际上变成了字符串拼接方法,于是{}调用Object的原生toString方法,转成了“[object Object]”,最终拼接为了“[object Object]”。
6. {} + []
这个和[]+[]、[]+{}点相似,如果按照第五项中的解释去理解,是得不到结果的。为什么呢?这要说到JavaScript引擎本身解释代码的问题了,在JavaScript解释{}时,有两种情况,一种是语句块,一种是对象定义。
当直接在控制台输入{}+[]时,此时解释器将{}解释为语句块,即{};+[],所以输出就变成了+[]的结果,这里+符号会强制转换,执行toNumber()操作,空数组返回数字0。
如果在外面加上括号,即({}+[]),那么{}就会被解释为对象,最后返回“[object Object]”。
7. true+true+true === 3
有运算操作符时,Boolean类型false转为0,true转为1,所以左侧结果为3,值相等,类型也相等,故返回true
8. true - true
和上面同样的理由,转变成数值运算1-1,所以返回0
9. true == 1
相等操作符,两侧会进行toNumber操作,进行值判断,不进行类型判断。所以1 == 1,返回true
10. true === 1
全等操作符,既判断值,也判断类型,即不做类型转换,这里值虽然相同,一个为Boolean类型,一个为Number类型,所以返回false
11. (! + [] + [] + ![]).length
运算符具有优先级
这里可以看到逻辑非!操作符优先级比+高,所以第一个逻辑非!先执行,相当于!(+[]),+[]进行toNumber操作返回0,然后逻辑非进行toBoolean操作返回true,然后![]执行,于是[]先进行toBoolean的操作,返回true,然后逻辑非操作变成false,然后执行从左至右+运算,即true+[]+false,变成了字符串拼接,于是返回“truefalse”,这个字符串的长度也就是9了
12. 9 + "1"
无论是9+"1",还是"9"+1,结果都是91。+运算符可以是数字相加运算,也可以是字符拼接运算。但是规范文档规定了,如果+运算符两侧存在字符串时,就调用toString()方法,进行字符串拼接操作,所以这里结果都是91。
13. 9 - "1"
-运算符和+运算符不同,因为-运算符就是数字运算减的操作,所以先转成Number类型,所以无论是‘9’ -1还是9 -‘1’,结果都是8
14. [] == 0
根据== 弱相等运算符中的规则,空数组最后会执行toNumber转成数字0,所以返回true
拓展
相信通过上面的解疑,应该掌握了大部分技巧,现在再来检验一波掌握的如何。
'true' == true0 == null
先思考一波
.
开始思考
.
思考 ing
.
完成思考
就认为大家都思考一波了
1. 'true' == true
嘿嘿,肯定有看错然后答错的。
不先说答案,按照流程走一波:
- 在这个相等运算中,左侧
'true'的数据类型是String,右侧true的数据类型是Boolean - 首先满足== 弱相等运算符第
9条,所以布尔值true转成数值1,返回'true'==1的值 - 其次
'true'==1又满足第7条,所以字符串true根据上面讲的规则,转换成NaN,故返回NaN==1 - 然后
NaN都不等于任何值,包括它本身,即NaN==NaN返回false - 最后
'true'==true返回false
2. 0 == null
在这个相等运算中,左侧0的数据类型是Number,右侧null的数据类型是Null(内部Type运算的结果,与typeof运算符无关),所以根据上面的规则,前面11条都不满足,直到第12步才返回false。
另附上一份图,自行按照流程走即可得到答案。
