写在前面
JS的小船继续航行,本期我想分享一个老生常谈的问题——类型转换。
例题:
console.log([]==![]);
最近刷到了这一个题目,我是这么想的:一个东西取反肯定不等于它本身。答案为false,搞定!
但是我在控制台运行:
答案为true,打脸来的不要太快。于是我果断选择问大模型为啥?
我...
原本以为能给我框框一顿输出,让我幡然醒悟。得,还得靠自己,于是我从头开始。
JS中的数据类型
JS中目前一共有7种基本数据类型:
- Number* 数字类型
- Boolean* 布尔类型
- String* 字符串类型
- Null 空值
- Undefined 未定义类型
- BigInt 大整数类型
- Symbol 符号类型
*为包装类:允许其基本值作为对象使用,JS在比较他们时常常会进行隐式类型转换。
当然还有引用数据类型:Object(对象)。
在JavaScript这门编程语言当中,数组(Array),函数(Function),日期(Date) 等等许多都是对象。
类型转换
一、简单的类型转换:原始值转原始值
例:
let str = 'abc';
let num = 123;
let f = false ;
let u = undefined ;
let n = null;
以上几种原始值的转换很常见,但是也可以细分为两种:隐式类型转换和手动类型转换:
1.隐式类型转换
顾名思义,隐式类型转换是我们在使用操作符运算或者比较时,JS为了方便处理,将一种原始值类型默认转变为另一种原始值类型进而统一处理的一种手段。
console.log(str + num);// 输出:'abc123'
console.log(num + f); // 输出: 123
console.log(str + n);// 输出:'abcnull'
console.log(num + n);// 输出: 123
console.log(num == u); // 输出:false
console.log(num == n); //输出:false
这就是JS的隐式类型转换,当使用操作符+ ==时,就会触发JS的隐式类型转换,这样看似乎看不出什么,那么我们继续看看原理。
2.手动类型转换
手动类型转换就是直接调用包装类的构造函数(例如:Boolean()),将需要转换的类型作为参数传递,从而得到结果的一种强制手段。其实JS在隐式类型转换时也是基于这些机制。
- Boolean()
还是上述的例子,我们来看一下Boolean()得到的结果:
//true
console.log(Boolean(str));
console.log(Boolean(num));
console.log(Boolean(-1));
//false
console.log(Boolean(''));
console.log(Boolean(f));
console.log(Boolean(u));
console.log(Boolean(n));
console.log(Boolean(NaN));
console.log(Boolean(0));
console.log(Boolean(false));
console.log(Boolean());
对于字符串来说,非空字符串在转为布尔值类型时结果均为true。
对于数字类型来说,NaN(不可描述的数字)和数字0转为布尔值类型时结果为false,其余数字为true(无论正负)。
而其他基本类型(undefined null),转为布尔值类型为时结果都是false,布尔值转布尔值结果还是本身。
- Number()
console.log(Number('1312')); 1312
console.log(Number(true)); // 1
console.log(Number(0x12)); //18
console.log(Number(false)); // 0
console.log(Number()); // 0
console.log(Number('')); //0
console.log(Number(null)); // 0
console.log(Number('abc')); //NaN 不可描述的数字
console.log(Number(undefined)); //NaN
根据官方文档Annotated ES5,在其他基本数据类型转为Number时,遵循以下规则:
对于数字类型:不变。
对于布尔类型:为true结果是1,false结果是0。
对于字符串类型:JS会先判断是否有数字信息(数字特征:正负整数,十六进制标志0x等),如果有,则结果也为数字。如果识别不出数字特征,则返回NaN,空字符串默认为0。
对于NULL类型:为0。
对于Undefined:为NaN。
- String()
console.log(String());
console.log(String(''));
console.log(String(0)); //'0'
console.log(String(true)); //'true'
console.log(String(false)); //'false'
console.log(String(undefined)); //'undefined'
console.log(String(null)); //'null'
console.log(String(NaN)); //'NaN'
相比于其他两个函数的复杂,String的原始值强制类型转换显得十分粗暴,就在值的两边加上引号变为字符串即可。
搞清楚这个简单的原始值转原始值,接下来我们一起探究更复杂的。
二、复杂的类型转换:对象转原始值
当JS在运算或者比较对象和原始值,又或者是对象和对象之间,会先将对象转为原始值。对象转原始值过程也可以两种方式进行,一个是默认的隐式类型转换,一个是手动类型转换。值得思考的是,二者的同源机制是什么?
我们依旧来看原理:
1.转Boolean
对象转布尔类型值的原理十分简单:任何对象转布尔值结果都是true。
//输出均为:true
console.log(Boolean({}))
console.log(Boolean([]))
console.log(Boolean(Object.create(null)))
console.log(Boolean(Object.create(Array)))
2.转Number
对象转Number类型原始值的过程就复杂多了,我们来看看官方文档 Annotated ES5。
这也是看了我好一会才捋清楚,我来梳理一下大概意思:
- Number(x),判断参数
x是否为原始值,不是的话进行下一步 - 调用默认的ToPrimitive抽象方法,该过程会将Numer类型作为提示(hiti) 传入
- 尝试调用valueOf()方法将对象转为原始值,转换成功,返回原始值;转换失败,继续下一步
- 尝试调用toString()方法将对象转为原始值,转换成功,返回原始值;转换失败,继续下一步
- 如果上述步骤均不能获得原始值,则抛出一个异常TypeError异常
- ToNumber(),如果有原始值返回,则会调用该默认函数,原理和上述原始值类型转Number类型值一致。
当然我们还得知道valueOf和toString()两个方法的原理:
1️⃣valueOf()
valueOf()是每个对象原型上默认的方法,旨在为对象返回其基本类型的值来表示自身,这一行为在其他传统面向对象语言中通常被称为 "拆箱",即将包装类对象拆为基本值。
例如:
上面的num是一个Number对象,封装了数字88,我们使用对象原型上的valueOf()就能直接获取到原始值。
2️⃣toString()
toString()也是每个对象原型上默认的方法,旨在为对象返回一个字符串。
根据不同对象的toString方法的返回值格式,我们可以分为两类:
- 对象类型标识:对于没有自定义toSring()的对象,调用该方法会返回一个格式化的字符串,通常用来表示对象的类型。
let obj = {};
console.log(obj.toString()); // 输出: "[object Object]"
- 自定义标识:开发者可以为某个对象或某个构造函数重写toString()方法,以此生成更具体的内容。
let arr = [1, 2, 3];
console.log(arr.toString()); // 输出: "1,2,3"
let boolObj = new Boolean(true);
console.log(boolObj.toString()); // 输出: "true"
许多内置常用对象的toString()方法都已经被官方重写了,方便开发者使用。
那么接下来我们就来分析Number(obj)这个过程的执行机制:
console.log(Number({}));
这里我们传入了一个空对象。
分析:
- 第一步,判断{},不是一个原始值。
- 第二步,调用valueOf方法,也即会调用Object.prototype.valueOf(),得到结果为:[Object: null prototype] {},该空对象自身的引用。
- 第三步,调用toString()方法,也即调用Object.prototype.toString(),结果为:'[object Object]'
- 第四步,将原始值'[object Object]'转为Number的原始类型,得到NaN,因为该字符串不具有数字特征不能转为对应的数字。
再看一个例子:
console.log(Number([1,2,3]));输出://NaN
因为对于toString调用后的结果'[1,2,3]'来说,只是一个拼接的字符串,并不能转为对应数字,所以结果为NaN。
3.转String
对象转String类型基本值的过程也和Number一样。
- String(x),判断参数
x是否为原始值,不是的话进行下一步 - 调用默认的ToPrimitive抽象方法,该过程会将String类型作为提示(hiti) 传入
- 尝试调用valueOf()方法将对象转为原始值,转换成功,返回原始值;转换失败,继续下一步
- 尝试调用toString()方法将对象转为原始值,转换成功,返回原始值;转换失败,继续下一步
- 如果上述步骤均不能获得原始值,则抛出一个异常TypeError异常
- ToString(),如果有原始值返回,则会调用该默认函数,原理和上述原始值类型转String类型值一致。
例子:
console.log(String({})); //输出:'[object Object]'
这里是因为valueOf()调用后没有拿到原始值,又调用toString()方法拿到了原始值'[object Object]'。
console.log(String([1,2,3])); //输出:'[1,2,3]'
这里同理。
这就是类型转换的所有内容了,分析的时候我们只需要注意类型转换方向就可以了。
三、转换方向的判断
大多数情况,与类型转换有关挂钩的都是运算符,例如“+”、“==”。学会判断转换方向至关重要。
1.算术运算符
当使用+ - * /,在对象或者原始值之间进行运算时,优先将他们转为Number类型的基本值进行运算,如果不能被转为数字,结果可能是NaN,例如:'abc'/123 。
特别地,当操作符的两边但凡有一个String类型的数据,那么都得将他们朝着String类型基本值的方向转换,即都转换为字符串,再去拼接。
2.比较运算符
==是一个常见的不严格判断相等的符号,当使用到==判断时,默认会触发隐式转换,并且会朝着Number类型 转换(这一点十分重要)。
====不会触发隐式类型转换,当我们使用===比较原始值或者对象时,只有值和类型严格相等才会通过判断。
3.逻辑运算符
当使用& | 对原始值或者对象进行逻辑运算时,会触发隐式转化,并且朝着Boolean类型 转换。
四、解决[]==![]问题
ok,上面这个问题现在来看就是很简单的了,相信你看懂了的话也可以秒杀了。
首先,使用==判断会触发隐式类型转换(朝着Number基本类型),[]是一个空的数组对象。
- 第一步:判断[]不是原始值。
- 第二步:开始默认调用Toprimitive()
- 第三步:[].valueOf(),结果为[],还是一个对象。
- 第四步:[].toString(),结果为'',一个空字符串,这里拿到了原始值。
- 第五步:将''转为Number类型的基本值,结果为0。
- 第六步:![] 取反操作针对的是布尔值类型,故[]转为布尔值为true(对象都为true),取反![]为false。
- 第七步:布尔值:false转为Number类型的基本值,结果为0。
故将第五步和最后一步结果==比较后,结果为true。重点是:注意!取反针对的是布尔值,转为布尔值后再转为Number类型基本值才可以进行比较。
总结
本期我们讲了:
- JS的基本数据类型
- 类型转换
- 简单的类型转换:原始值转原始值
- Boolean()
- Number()
- String()
- 复杂的类型转换,对象转原始值
- 转Boolean
- 转Number
- 转String
- 转换方向的判断
- 算术运算符(+)
- 比较运算符(==)
- 逻辑运算符
- 简单的类型转换:原始值转原始值
下期打算分享类型判断的内容,如果你觉得本文对你有帮助,还请点个小赞,这将是我持续创作的动力。
参考:
JavaScript 参考 - JavaScript | MDN (mozilla.org)
Annotated ES5