在真实的世界里,人与人总是有那么一点不信任,“争是非,辨明理”是常事。在编程开发领域中,同样需要对变量的存在、类型、真伪进行校验,几乎每天都是在和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%
的类型判断场景,够用;- 显示类型转换可以减少隐式类型转换的不确定性;