前言
在JavaScript的世界中,类型转换是每一个开发者都必须面对的挑战。它就像一条暗礁,只有深入了解它的奥秘,才能避免被它击沉。隐式转换,作为类型转换的一部分,经常使我们陷入困扰。不管你是资深开发者还是新入行的初学者,你都可能曾经因为隐式转换而头痛不已,下面我们来对其深入了解一下。
1. == vs ===
在我们学习js的过程中,我们肯定都学习过一些判断语句,比如if,while等等,在这些语句中我们免不了要写判断条件,在js中判断左右两边是否相等有两种写法,下面我们来根据一段代码看看他们两者的区别:
console.log('1' == 1);
console.log('1' === 1);
// 输出:
// true
// false
根据上面的代码我们可以发现在使用这两种不同的比较相等的运算符时,它们是有区别的。
在这里使用==进行判断可以看到它们是相等的,这个是因为v8在背后悄咪咪的进行了将左边隐式转换成了Number类型(毕竟Number类型好比较嘛),然后再进行判断。而'1'转化成Number类型的值是1,所以两边相等。
==官方文档
- 如果两边一个是NaN,返回false
- null == undefined,返回true
- 一边字符串/布尔,一边数字,就把 字符串/布尔 转为数字,再比较
下面我们来看看===怎么就不相等了,在v8中碰到这三个等号兄弟在一起的时候,就不会悄咪咪进行类型转换,而是严格判断类型、值是否都是一样的,如果其中一个不是一样的那么就会返回false,都一样的才会返回true。
==:会发生隐式类型转换,所以只会判断值是否相等
===:不会发生隐式类型转换,意味着会判断值和类型是否相等
2. 原始类型之间转换
我们在上文中聊到js中的==会悄咪咪的进行类型转换,而其中进行类型转换的规则是什么呢?下面我们由浅入深先来聊聊常见的原始类型之间的转换,当我们想将某个变量转化为原始类型的时候,只需要将变量传入js内置的构造函数中即可,下面我们来看代码:
Symbol 和 bigint 类型不会发生类型转换
console.log(Boolean(true));// 传入布尔不会转换,原样输出
console.log(Boolean(undefined));
console.log(Boolean(null));
console.log(Boolean(1), Boolean(-1), Boolean(0), Boolean(NaN));
console.log(Boolean('hello'), Boolean(''));
console.log(Number('1'));
console.log(Number('0'));
console.log(Number(''));
console.log(Number(undefined)); // NaN
console.log(Number(1));// 传入数字不会转换,NaN也是数字类型也会原样输出
console.log(Number('hello'));// NaN
console.log(Number(null));// 0
字符串是啥转啥
console.log(String(1));
console.log(String(NaN));
上面的代码就是我们平常对原始类型进行转换时的方法,大家可以自行输出一下得到什么,根据上面的输出结果我们可以总结出转成不同原始类型的以下几条小规律:
转布尔类型
- undefined 和 null 转布尔输出flase
- Number类型转布尔时,只有 0 和 NaN 输出flase,其他数值输出都是true
- String类型转布尔时,空字符串输出false,否则输出true
转数字类型
- undefined 输出NaN
- null 输出0
- String 字符串中是数字则输出字符串中的数字,如果其中有除了数字以外的字符则输出NaN。如果是空字符串则输出NaN。
转字符串类型
- 传入的是啥外面加个
''就是字符串了
- undefined 和 null 没有构造函数也转换不了
3. 对象转原始值
经过前面原始类型进行转换的学习后,我们知道转换原始类型只能转换成Boolean、String、Number这三种类型,下面我们来看看将对象转换成原始值会是如何进行转换的。
3.1 转布尔
首先我们先来看一个最简单的将对象转换为布尔类型,下面我们看一段代码:
let a = []
if (a) {
console.log('a hello');
}
let b = {}
if (b) {
console.log('b hello');
}
// 输出:
// a hello
// b hello
我们可以看到在if语句中虽然传入的两个对象都是空的,但是这两行代码都能进行输出,这是因为对象转布尔都是true,别问为什么这是官方规定的,咱记住就好。
对象转布尔都是true
3.2 转字符串
讲到对象转字符串大家可能会想,这会不会跟之前原始类型转换成字符串方式一样是加个''然后直接返回字符串呢?这里呢大家猜对了一办,它确实会返回字符串,但是返回的不是对象本身,而是它的类型,下面我们来看一段代码:
let a = {}
// 两种转字符串方法逻辑一样
console.log(String(a)); // String(a) => ToPrimitive(a, String)
console.log(Object.prototype.toString(a));
我们根据控制台输出结果可以看到这两种方法将对象转化为字符串都是输出了一串字符串,并且这个输出结果大家是不是很熟悉,这不就是之前文章类型判断中的
Object.prototype.toString方法的输出结果吗。确实没错,在将对象转换成字符串时,我们可以使用两种方法String(obj) 和 Object.prototype.toString(obj) ,这两种输出结果都是一样的,那么这两种方法在v8中是如何实现的呢,下面我们来为大家解析一下,来看看v8又偷摸干了些什么。
v8在将对象转换成字符串时,虽然说表面我们就调用了一个函数就完成了这个功能,但是成功男人的背后不都有一个女人嘛,而v8就是这个贤内助。在我们进行转换的时候我们先告诉它要把对象转化为字符串类型——
String(obj),然后在把对象转化为字符串的过程中v8会悄咪咪的调用ToPrimitive(obj, String)方法来将对象进行转换,下面我们来看看ToPrimitive的执行过程:
ToPrimitive两个参数:第一个是要转换的变量,第二个是要转换的类型
ToPrimitive执行过程
- 如果 obj 是原始类型,直接返回
- 如果 obj 是引用类型,则调用 toString(),如果得到原始类型则返回
- 如果得不到原始类型,则调用 valueOf(),如果得到原始类型则返回
- 如果还是不能得到原始类型,则报错
在了解完了上面的执行过程之后,我们可以得知在调用String(a)时,先将a和String传入ToPrimitive,然后进行判断,发现a不是原始类型,然后接着调用toString()方法,a在调用toString时会得到一个字符串[object Object]而这个字符串是原始类型,然后将得到的这个字符串返回进行打印。在这个过程中,我们得注意一些引用类型变量调用toString()方法的输出是什么:
引用类型调用toString
- {}.toString 返回由 '[object' 和 class 和 ']'组成的字符串
- [].toString 返回由数组中元素以逗号拼接的字符串
- xx.toString 除了对象和数组其他的引用类型使用该方法都是直接返回 xx 的字符串字面量
3.3 转数字
在我们了解完了对象转字符串的过程后,接下来我们来看一下将对象转数字时,v8是如何进行的,其实与转字符串过程基本一样,只不过在ToPrimitive这个函数内部的执行过程中有一些区别,下面我们来看一段代码:
console.log(Number({}));
// 输出:
// NaN
在这里有些同学可能会猜输出结果是0或者undefined,但是这里输出的却是NaN,这就与对象转Number时过程中的ToPrimitive有关了,下面我们来看看对象转Number时ToPrimitive(传入的两个参数第一个是变量,第二个是要转换的类型)的执行过程:
ToPrimitive执行过程
- 如果 obj 是原始类型,直接返回
- 如果 obj 不是原始类型,则调用 valueOf(),如果得到原始类型则返回
- 如果 obj 是引用类型,则调用 toString(),如果得到原始类型则返回
- 如果还是不能得到原始类型,则报错
在上面的文章中我们只讲了toString这个函数,接下来我们来了解一下valueOf这个函数,下面我们来看看valueOf在浏览器中的效果展示:
我们根据上面这个效果可以的知道valueOf在面对一般的引用类型时都无法将其转化为原始类型,但是特别在面对包装类时,它偏偏能生效,在这里我们只需要记住这一特点即可,下面我们来了解一下上面代码的执行过程。
上面代码在执行时,我们先调用了Number()方法告诉v8我们要将{}转化为Number类型,然后v8会调用ToPrimitive({}, Number)方法并执行,在这个执行过程中我们先判断发现{}不是原始类型,然后调用valueOf()方法根据浏览器的运行结果我们得知会返回一个{},然后我们进入到下一步调用toString()方法对象转化为字符串会返回'[object Object]'这一个字符串此时得到了原始类型我们直接返回即可,这时候将这个字符串传入到函数Number()中时,根据原始类型转换的规则,如果字符串中有除了数字之外的字符会直接转化为NaN,所以此时会输出NaN。
4. 隐式类型转换场景
在前面了解完了对象是如何被v8从背后悄悄转换成原始类型之后,下面我们来了解一下平常v8会进行隐式类型转换的场景:
- 四则运算 + - * / %
- 判断语句 if while == > < >= <= !=
4.1 一元运算符 +
我们都知道在js执行时通常是在进行运算或者判断的时候会进行隐式的类型转换,下面我们来看一个特殊一点的一元运算符,接下来看一段代码看看输出的是什么:
// 将其操作数转换为Number类型
console.log(+'1')
console.log(+[]);
console.log(+[1, 2]);
// 输出:
// 1
// 0
// NaN
这个运算符就相当于我们执行了Number()这个操作将变量用+转化为Number类型,后面两个输出结果可以参考上文中对象转数字的操作。
4.2 二元运算符 +
我们直接来看一段代码:
console.log(1 + '1');
console.log('1' + 1);
// 输出:
// '11'
// '11'
当我们使用+运算符时可以看到Number被它给转化成了字符串然后进行相加,这个呢可以参考一下官方文档,当然我们也可以先用自己的话给大家简单总结一下+当成二元运算符的规律:
lprim是左边的元素,rprim是右边的元素
- 如果 lprim 或者 rprim 是字符串,另一个值直接被 ToString()
- 否则,返回对 ToNumber(lprim) 和 ToNumber(rprim) 应用加法运算的结果
4.3 [] == ![]
我们先来判断一下[] == [],这个想必大家都知道是false,因为引用类型的地址不一样,接下来我们再来看看标题的[] == ![]这个就众说纷纭了有的同学猜测是false,有的说是true,那到底是啥呢我们来看看在v8中上面代码是如何执行的:
[] == ![]
// 由于!会让它后面的先转化为布尔,而引用类型转布尔都是true
[] == !true
// 然后对true取反
[] == false
// 在前文中说到==会将两边转为Number进行比较然后将false转为Number
[] == 0
// 接着我们将[]转为Number类型,v8调用ToPrimitive方法最后返回一个空字符串
'' == 0
// 最后将空字符串转为Number进行比较
0 == 0
// 输出:
// true
结语
希望本文能对大家提供帮助,帮助大家翻过js的这座大山。要特别注意,在开发过程中,尽量使用显式转换,而避免隐式转换,以便于代码的维护和调试