在真实的世界里,人与人总是有那么一点不信任,“争是非,辨明理”是常事。在编程开发领域中,同样需要对变量的存在、类型、真伪进行校验,几乎每天都是在和if === typeof 打交道。但是你真的弄懂了这些判断语句吗?
一、真假判断
“真亦假时假亦真,无为有处有还无。”——《红楼梦》
if( x )
在if语句中,会触发Boolean上下文,JavaScript会将值强制类型转换为布尔值,再执行判断。 当 x is Trusy时,if语句命中。
在JavaScript中,存在七种假值(又称虚值),分别为undefined null "" false 0 0n NaN。其中0n是ESFuture中新增的一种假值。
undefined是最常见的假值,当判断在一个对象中是否存在某个字段时,使用的就是undefined空值判断:
let people = {name}
if(people.age){ // 等价于 people.age === void 0
// ...
}
建议在代码中使用
void 0来代替undefined,原因是undefined不是JS的保留字,而且void 0比undefined字符少。 事实上,不少JavaScript压缩工具在压缩过程中,将undefined使用void 0代替。
二、相等判断
雄兔脚扑朔,雌兔眼迷离。双兔傍地走,安能辨我是雄雌。——《木兰诗》
在JavaScript中,有两个相等运算符来判断两个操作数是否相等,一个是==相等运算符,另一个是===全等运算符。它们最大区别在于对类型的宽容度。
x === y
===全等运算符对左右两边的孩子(操作数)是严厉的,就像一个严厉的父亲。
===全等运算符首先会检查两边的操作数类型是否一致,然后再检查其值。具体流程如下:
-
两边类型不同,返回false;
-
类型相同,比较其值:
a. 双方都是number类型:
有一方是`NaN`,返回 false; 一方是+0,一方是-0,返回 true; 双方值相同,返回 true; 其他,返回 false;b. 双方都是string类型:
对双方挨个比较字符,若字符顺序数量相同,返回 true; 其他,返回 false;c. 双方均是boolean类型:
值相同,返回 true; 其他,返回 false;d. 双方均是object类型:
若引用地址相同,返回 true; 其他,返回 false;
x == y
==相等运算符对左右两边的孩子(操作数)是宽容的,就像一个慈祥的母亲。
当对两个操作数进行比较时,JavaScript会先对其中一个操作数隐式类型转换,那么当两个操作数进行比较时,都做了些什么?我们需要从ECMA标准中寻找答案。
基本流程如下:
- 如果
type(x) === type(y), 此处比较过程与===相同; - 如果
x is null,y is undefined,则返回true;反之亦然; - 如果
x is string,y is number,则执行ToNumber(x) == y;反之亦然; - 如果
x is boolean,则执行ToNumber(x) == y;反之亦然; - 如果
x is object,则执行ToPrimitive(x) == y;反之亦然; - 都不是以上情况,返回false;
简要概述:类型相同,拼家世拼内涵;类型不同,先平等后比较;
null和undefined是一家,number一家独大,其他都要向我靠;object你别骄傲,降成平民再比较。
在上面过程中我们看到,如果两个操作数的类型相同,则比较过程与===是一致的。如果类型不同,那么就需要隐式类型转换为类型相同的情况后再比较。
上面提到了ToNumber,ToPrimitive的转换过程就是隐式的类型转换。那么它们究竟做了些什么呢?
ToNumber(x):
1. x is undefined, return NaN;
2. x is null, return 0;
3. x is number, return x;
4. x is boolean, return x === true ? 1 : 0;
5. x is string, return [ToNumber Applied to the String Type](http://www.ecma-international.org/ecma-262/5.1/#sec-9.3.1);
6. x is object, return ToNumber(ToPrimitive(x))
ToPrimitive(x):
1. x is object, return x.valueOf() or x.toString();
2. x is non-object, return x;
在ECMA标准中,ToPrimitive(x)可以指定第二个参数PreferredType = "number" | "string",区别在于当x is object时,先调用valueOf()方法还是先调用toString()方法,默认"number"。
至此,我们大致知道了隐式类型转换都做了哪些“幕后工作”了。
经过以上分析,我得出的结论是:能不用==就不要用它,隐式类型转换规则较多,容易产生意想不到的结果。
当然,
==也不是一无是处的,那么何时用==呢? 当接口返回的值可能是数字,可能是字符串,而你又不能确定时,比如obj.chance == 1; 然而,我仍然鼓励你积极和你的后端小伙伴沟通一下,明确下发字段的类型比较稳妥呢!
Object.is(x, y)
在ES6,新增了一种判断两个操作数是否相等的方法,也是最为严格的判等方式——Object.is()。使用它,可以确保两个操作一定是相等的,容不得一点沙子。
Object.is()与===全等运算符的区别在于对待NaN,+0,-0的判定有所不同:
// NaN
NaN === NaN // false
Object.is(NaN, NaN) // true
// +0 -0
+0 === -0 // true
Object.is(+0, -0) // false
三、类型判断
我是谁?我从哪里来?我要到哪里去?——《人生三问》
对变量类型的判断是代码健壮的基础,只有正确判断变量的类型,才可以安心调用变量上部署的方法,如string.charAt() array.map()等。那么有哪些方法可以判断类型呢?
typeof x
typeof是JavaScript内置的一个用于判断类型的一元操作符,它返回一个表示类型的字符串。下表总结了typeof可能的返回值:
| typeof x | Result |
|---|---|
| Undefined | "undefined" |
| Boolean | "boolean" |
| Number | "number" |
| String | "string" |
| Symbol | "symbol" |
| BigInt | "bigint" |
| Function | "function" |
| Null | "object" |
| 其他 | "object" |
从上表可以看出,typeof判断基本类型时非常合适,可以正确返回我们期望的类型字符串。而操作数是对象或者null时,则统一返回"object"字符串,则需要其他的方式来进一步判断类型。
因此,可以得出一个结论:如果你明确变量是基础类型时,请使用typeof操作符来判断类型。其他类型typeof则有些力不从心了。
({}).toString.call(x)
当需要判断更多类型的时候,toString老大哥就勇敢站出来了,需要注意的是toString是Object原型链上的方法。其实,每个内置对象都有一个toString方法,不同对象的toString的行为都是不一样,但它们都是继承自Object。
{}.toString.call(x)返回一个字符串[object Type],其中Type可能取值为 Number String Boolean Function Undefined Null Object Symbol Date Math RegExp ...
ECMA标准这样描述:
- x is undefind, return
[object Undefined]; - x is null,return
[object Null]; - let
O = toObject(x); - return
"[object " + classOf(O) + "]"
({}).toString.call(x)可以很方便的判断JS的内置对象类型,它进一步细化了object分支里的大部分类型,适用于需要更多类型判断场景,以下是type工具函数的示例:
function type(obj) {
let toString = Object.prototype.toString,
typeReg = /\[object\s([A-Z][a-z]*)\]/,
matchArr = toString.call(obj).match(typeReg)
return matchArr ? matchArr[1].toLowerCase() : ''
}
对象的toString 方法可以使用Symbol.toStringTag 这个特殊的对象属性进行自定义输出(详细可参考ES6——Symbol)。举例说明:
let user = {
[Symbol.toStringTag]: 'User'
}
console.log(({}).toString.call(user) // [object User]
如此你可以为你自己的对象或类定义个性化的类型字符串。宿主环境的大部分环境相关对象用的就是这个原理:
console.log(({}).toString.call(window)) // [object Window]
x instanceof XXX
typeof + toString的强强联合,已经包揽了80%的类型判断场景,但仍有20%的自定义对象类型场景无能为力,例如你有一个自定义类Person:
class Person {}
let person = new Person()
typeof person // "object"
{}.toString.call(person) // "[object Object]"
此时,你需要instanceof运算符来检测构造函数的 prototype 属性是否出现在 person 实例对象的原型链上。
有点绕口,至于何为原型链,就不在这里展开了。
x instanceof X,字面意思 x 是否由 X 实例化,返回 true/false。
person instanceof Person // true
// or
Object.getPrototypeOf(person) === Person.prototype
还是instanceof比较直观,😃
四、显式类型转换
隐式类型转换是一把双刃剑。它帮开发者自动转换类型,省去麻烦;有时候又出其不意,莫名其妙。显式类型转换架起基础类型防线。
+x or Number(x)
+一元正号运算符,计算操作数的数值,如果这个操作数不是数值,则尝试将其转换为数值。根据标准描述,+x其实就是执行的toNumber(x)操作:
| +x | Result |
|---|---|
| Boolean | 0 / 1 |
| Number | 值本身 |
| String | 转换为数值 |
| Null | +0 |
| Undefined | NaN |
| Symbol | throw a TypeError |
| Object | ToPrimitive(x),再重复上面步骤 一般是执行valueOf 或 toString方法 |
同理,Number(x)执行的过程也是toNumber(x)的过程。
''+x or String(x)
'' + x中的加号不同于 +x中的加号,此处的加号的作用是字符串的拼接。它会显示的将不是字符串类型的操作数转换为字符串后拼接,即toString操作。
从toString(x)小结中,我们知道,在对象上运行toString方法时,会在原型链上查找到最近的toString方法运行。一般而言,继承自Object的其他对象都会实现自己的toString方法。
"" + 34 // 等价于 "" + (34).toString()
"" + true //等价于 "" + (true).toString()
"" + {} //等价于 "" + ({}).toString()
直接调用String构造函数来显示转换类型,和""+x基本一致。但ES6中新增的基本类型Symbol则不适用直接调用""+x方式来转换为字符串,会异常报错。
// Error
"" + Symbol('JS') // Uncaught TypeError: Cannot convert a Symbol value to a string at <anonymous>
String(Symbol('JS'))
// or
(Symbol('JS')).toString()
看起来,String(x)要比''+x靠谱的多。
!!x
双重非!!运算符显式将任意值强制转换为布尔值。
还记得刚才提到的JS中的七个假值吗?即,x是七个假值时,!!x一定返回的 false,其他情况都返回的 ture。
总结
- JS中存在七个假值(Falsy),
undefined null "" false 0 0n NaN; ===全等运算符比==相等运算符更加严格,且更符合相等性判断预期;typeof与toString的类型判断方法可以覆盖80%的类型判断场景,够用;- 显示类型转换可以减少隐式类型转换的不确定性;