最近在准备面试,记录点复习的内容。
类型转换,应该说是强制类型转换,老生常谈的问题,主要就是在各种加减法、弱等、比大小中出现。本来是不想写这个的,直到我看到这个:问 ++[[]][+[]]+[+[]]
输出什么。wtf,还能有这么"优秀"的题?我还是记录一下吧。
js的值类型
js的值类型分为基础(简单)类型和引用(复杂)类型:
- 基础:number、string、boolean、bigint、symbol、undefined、null
- 引用:object、function、array(其实就一个Object,function和array都是对象的衍生)
这里我们说的是值类型,不是变量类型,主要是因为js采用的是弱类型的方式,变量并没有固定的类型,可以随意变更,例如:
js
let a = 1;
a = 'string';
这与强类型语言是完全相反的,强类型的语言在声明变量的时候需要声明变量的类型,如果修改为变量赋一个不同类型的值,会直接报错
C++
#include <stdio.h>
int main() {
int a = 2;
a = "string"; // 报错: incompatible pointer to integer conversion assigning to 'int' from 'char[7]'
}
类型转换
将值的类型从一种类型转换成另一种类型的行为被称作类型转换。正常情况下,是需要使用内置的函数来实现类型转换,例如:
js
let a = 1;
let b = String(a); // 转字符串
let c = Boolean(a); // 转布尔
let d = Number('234'); // 转成数字
但是一些情况下,会触发被动技能,例如这样:
js
let a = 1 + ''; // 会转换为字符串'1'
这种其实还好,还是主动转换,毕竟作为开发者这么写都是知道其结果的。但是还有其他情况,是非主动转换的:
js
let a = 1;
let b = '1';
if (a == b) // true
这种属于隐式的转换,是在开发者不知情的情况下进行的。
js这种形式的类型转换一般称为强制类型转换。也是一直被人诟病的一个特性,以前看过的很多js相关的书籍都觉得这一特性是非常危险、糟糕的设计。以我个人的经验来看,有利有弊,主要还是看使用者怎么使用,要是非要写一个花里胡哨的东西,那就是在挖坑。
强制类型转换
前面说到的强制类型转换可以分为显示转换和隐式转换:
- 显示转换:明确的说明转换的类型
- 隐式转换:没有明确的说明转换的类型,依靠语言内置的机制来转换
显示转换我们就不讨论了,没有太多坑,这里主要讨论隐式转换。
隐式转换
隐式转换属于被动技能,在特定的条件下就会触发,接下来就来盘点这些特定的条件
加号+
加号+
既可以用于数字间的运算,又可以用于字符串凭借。 这是很特殊的操作,即使同样是弱类型语言的PHP,也是把这两个操作分开的。那么在js中,会根据什么样的规则来判断呢?一起看看:
js
let a = 1;
let b = 20;
let c = '1';
let d = '20';
console.log(a + b); // 21
console.log(c + d); // '120'
从上面的代码可以看到,数字和字符串操作的结果完全不一样。但是如果两个值的类型不一致呢?
js
console.log(a + c); // '11'
数字 + 字符串,操作是字符串拼接。反过来呢?
js
console.log(c + a); // '11'
字符串 + 数字,也是字符串拼接。
如果是其他类型的值呢?
js
console.log([1,2,3] + ['4']); // '1,2,34'
console.log({a:1} + {b:2}); // '[object Object][object Object]'
console.log(true + false); // 1
灰常神奇,具体是遵循什么规则呢?
看下规范:
总结一下:针对加法表达式:a + b
,有几个关键步骤:
- 通过GetValue获取
a
和b
的值 - 通过ToPrimitive获取转换值
a1
、b1
- 如果
a1
或b1
是字符串,将它们转为字符串,然后拼接 - 如果不是字符串,将它们转为数字,返回相加的结果
抽象操作ToPrimitive将输入的参数转换为非对象类型,如果传入的是非对象类型,直接返回原始值,如果是对象会通过调用对象的[[DefaultValue]]
并将返回值作为值返回。
[[DefaultValue]]
也是一个方法,这个方法可以接受一个类型参数,根据要求的类型去转换对应的类型的值,在ToPrimitive操作中,这个类型为空,方法会优先尝试处置成数字,就是执行valueOf
,如果没有valueOf
或者是获取到的不是基础类型的值,则执行toString
处理成字符串。
基础类型相加
这里就可以很好的解释前面字符串与数字相加的结果都是字符串的情况:
js
1 + '1' + 5 // '115'
同样,字符串加上boolean类型的也一样
js
'1' + false // '1false'
而在没有字符串的情况下,都会按照数字来处理:
js
1 + true // 2
1 + 3 // 4
true + false // 1
针对基础类型值相加可以总结一个规律:有字符串全转字符串,没有字符串全转数字。
引用类型相加
如果是引用类型,会通过[[DefaultValue]]
来获取其默认值。
看下这个:
js
[1,2,3] + [4] // '1,2,34'
因为数组的valueOf
获取的是数组自身,所以只能转为字符串。上面的两个数组相加则会变成两个字符串拼接。
同样两个对象也是一样的逻辑:
js
{a:1} + {b:2} // '[object Object][object Object]'
同样也有例外的情况:
js
let date = new Date();
1 + date; // 1Fri Apr 19 2024 18:28:31 GMT+0800 (中国标准时间)
Date对象的valueOf
应该是返回当前的时间戳才对,怎么会变GMT的时间格式?这是因为[[DefaultValue]]
在处理时间默认时,如果没有指定类型,会优先执行toString
。
针对引用类型相加可以总结一个规律:先转基础类型,日期转字符串,其他类型优先转数字,非数字则转字符串,转后使用基础类型的规则。
这里有个坑需要说明下:
js
[] + {} // '[object Object]'
{} + [] // 0
这个是因为{}
在前面的时候,编译器会将其识别为一个独立代码块(作用域)而不是一个空对象来处理,所以第二行代码其实是等同于+[]
。
基础类型与引用类型相加
其实也是遵循上面的规则,引用先转成基础类型,再按照基础类型的规则相加
js
1 + []; // '1'
true + {}; // 'true[object Object]'
一元运算符
前面都是将+
作为二元运算符,同时也可以作为一元运算符来执行:
js
+[] // 0
+'' // 0
+2 // 2
+new Date() // 1713526249698
+{} // NaN
这个比较简单,就是将运算符后面的值转成数字,如果不是数字会执行ToNumber(GetValue(expr))
来获取对应的数字(基本上就是不能直接转成数字就转成字符串再转数字)。
ToNumber
的规则如下:
- Boolean类型的,true是1,false是0
- null是0,undefined是NaN
- 对象则是先
ToPrimitive
,如果不是数字,则是转字符串再转数字 - 字符串,这个比较复杂(详细规则见规范),简单来说就是空为0,非空字符串符合数字的规则(纯数字、多进制数字、科学计数法等),出现多余的字符串就是NaN
js
+'123..123'; // NaN
+'.23123'; // 0.23123
+'0x1a'; // 26
+'10e3'; // 10000
其他数学运算符
其他数学运算符如-
、*
、`/```这些,都会被操作的值转为数字,然后进行计算:
js
2 - '1'; // 1
20 * [2]; // 3
20 / {}; // NaN
转数字的规则与+
是一样的。
而其他++
、--
这些必须操作数字不会进行类型转换,不然会直接报错。
弱等和强等
==
和===
都是判断两个值是否相等,这两个主要的区别是==
会在比较时进行类型转换,而===
不会。这也是大部分前端规范中强制要求使用===
的原因。
转换的规则
是否进行类型转换需要视情况而定,需要根据双方的类型是否相同来进行判断。大致的规则如下:
针对表达式x == y
-
如果x和y的类型相同,比较的结果按下面的规则返回
- 如果类型是
undefined
或者null
,返回true
- 如果类型是数字,需要进行多重判断:
- 如果有一方是
NaN
,返回false
- 如果一边是
+0
一边是-0
,返回true
- 如果两边的数字相同,返回
true
- 不属于上面任何情况,返回
false
- 如果有一方是
- 如果类型是字符串,根据字符串的长度和对应位置的字符是否完全相同来返回
- 如果类型是
Boolean
,同时为true
或false
,返回true
否则返回false
- 如果
x
和y
指向同一个对象,返回true
,否则返回false
- 如果类型是
-
如果是
undefined
和null
比较,返回true
js
undefined == null // true
null == undefined // true
- 如果是字符串与数字比较,对字符串执行
ToNumber
转换成数字再进行判断
js
10 == '10.0' // true
10 == '0xa' // true
- 如果有一个值的类型是
Boolean
,会想将这个值转为数字(ToNumber
),然后进行比较
js
1 == true // true
'0' == false // true 先将false转为数字0,然后根据规则3将'0'转为0,然后执行规则1
- 如果一个是字符串或数字,另一个是对象,对对象执行
ToPrimitive
转成基础类型,再进行判断
js
1 == [1]; // true
false == {}; // false
'[object Object]' == {}; // true
- 不符合上面所有情况的,返回
false
总结一下规律:同类型直接比较,非同类型引用类型转基础类型,基础类型优先转数字。
PS:前面说的+0
和-0
主要是为了某些场景(当需要按符号执行不同的操作时)保留了0的符号位。避免出现无符号的0导致出问题。
需要避免的情况
这些情况可能会是面试题中的坑,但是在实际开发中就不要这么写了,发现的直接打断腿
- 使
new Number(1) == 3
成立
js
Number.prototype.valueOf = function() {
return 3;
}
new Number(1) == 3; // true
- 各种与Boolean类型的比对,例如:
js
false == ''; // true
false == []; // true
false == '0'; // true 坑
false == [0]; // true
true == [1]; // true
true == [0]; // false
像这种判断(特别是false的)往往都会出现意料之外的情况,所以遇到有一方是Boolean的比较,最好用===
- 各种极端的情况
js
0 == [];
'' == [];
0 == '';
[] == ![];
"" == [null];
"" == [undefined];
以上情况的结果都是true,但必然不是程序运行时我们希望看到的情况,像0与空数组相等或者是空字符串与空数组相等这种结果会大大增加程序错误的情况,所以最好还是用===
逻辑运算符
js中的逻辑运算符||
(或)和&&
(与)并不算是真实意义上的逻辑运算,逻辑运算最终结果应该是返回一个布尔值,但是js的逻辑运算最终返回的是两个值中的一个,所以我们可以做一些比较简洁的写法:
js
c = a || b; // 其实等于 c = a ? a : b;
a && fn(); // 相当于 if(a) fn()
逻辑运算符也自带了类型转换,两者的步骤基本一致,只是在最后的判断上不一样:
针对于逻辑表达式:left || right
和left && right
,采用以下步骤:
- 对
left
执行ToBoolean
转为布尔值 - 在
||
的表达式中,如果布尔值为true
,返回left
,为false
返回right
- 在
&&
的表单时种,如果布尔值为true
,返回rigth
,为false
返回left
ToBoolean
的规则如下:
- 传入值是Boolean类型,直接返回
- 传入值是
undefined
或null
,返回false
- 传入值为
(+/-)0
、NaN
、空字符串,返回false
- 其他的都返回
true
js
'0' && []; // []
'' && []; // ''
'' || {}; // {}
{} && ''; // ''
逻辑非(!)
对于!x
,会先对x
执行ToBoolean
转换为bx
,如果bx
为true
,返回false
,否则返回true
条件判断
这些含有条件判断的表达式:if(...)
、while(...)
、do..while(...)
、for(;...;)
、?:
,在做条件判断的时候,都会对条件进行ToBoolean
的转换,以此来判断条件是否成立。例如:
js
let a = '';
let b = [];
if (a) { console.log(true); } // 不进入
while(b) {console.log(true); } // true...
比较运算
关系运算符>
、<
、>=
、<=
用于比较两个值,其实核心的算法就是<
,其他的运算符都会被间接转换成<
来执行。
针对于比较表达式:x < y
,在执行时,首先会对x,y执行ToPrimitive
,转换类型为Number,我们把转换后的值定义为:px
、py
。
如果 px
和py
都是字符串,按照两个规则判断:
- 如果
px
是py
的前缀(字符串q可以和另一个字符串p,拼接得到字符串s,则成q为s的前缀)返回true
js
'abc' < 'abcd'; // true
'ttt' < 'tt'; // false
- 如果
py
是px
的前缀,返回false
- 按位比较,取出两个字符串相同位置的字符按字母顺序比较(ASCII码的值),如果位置
k
的字符px[k]
<py[k]
返回true
,否则返回false
。
如果 px
或py
有一个不是字符串,先通过ToNumber
将其转为数字nx
和ny
,然后按照以下规则判断:
nx
或ny
为NaN,则返回false
js
1 < {}; // false
nx
与ny
为同一个数字,返回false
js
1 < 1; // false
+0
和-0
,返回false
js
+0 < -0; // false
-0 < +0; // false
-
无穷数情况:
nx
为+Infinity
(正无穷)返回false
,为-Infinity
(负无穷)返回true
ny
为+Infinity
(正无穷)返回true
,为-Infinity
(负无穷)返回false
js
+Infinity < 0; // false
99999 < +Infinity; // true
-Infinity < 0; // true
99999 < -Infinity; // false
- 如果
nx
的数字值比ny
的数字值小(值包括BigInt类型),返回true
否则返回false
按位取反运算符(~)
~x
对x
执行ToInt32
转换为32为的整数(到 ),这个比较少见,我一般只在这种场景用:
js
if (~[].indexOf(1)) {}
规律
总结下规律:
+
号作为二元运算符,优先字符串,然后是数字,作为一元运算符转数字- 条件判断相关和逻辑运算符的都转布尔类型来判断
- 比较相关的值先转基础类型,全字符串按位比较ASCII码,其他情况转数字比较
总结
回头来看开头提的++[[]][+[]]+[+[]]
,我觉得出这种面试题的都是闲的蛋疼,正常代码就不可能这么写,如果要考类型转换应该换个更复杂的,这个只是眼花缭乱而已。
解析一下,这是一个加法的算式,第一个值是++[[]][+[]]
,第二个是[+[]]
。
先看第一个,[[]][+[]]
看起来挺乱的,其实就是个数组的取值操作而已,像这样:[x][0]
。数组部分是一个二维数组[[]]
,+[]
转为0,所以[[]][+[]]
其实就是[[]][0]
也就是[]
,然后就是++[]
转为数字计算等于++0
为1
第二个值比较简单:[+[]]
其实就是数组[0]
。
所以这个表达式简化为 1 + [0]
,按照总结的加号逻辑,结果应该是'10'
(因为[0]
优先转换成字符串'0'
)
ok,本文总结了下类型转换的内容,顺便总结了下基本的规律。如果有遗漏的地方,请在评论区给个提醒,感谢。