每天都在写的JS判断语句,你真的了解吗?

4,610 阅读9分钟

在真实的世界里,人与人总是有那么一点不信任,“争是非,辨明理”是常事。在编程开发领域中,同样需要对变量的存在、类型、真伪进行校验,几乎每天都是在和if === typeof 打交道。但是你真的弄懂了这些判断语句吗?

一、真假判断

“真亦假时假亦真,无为有处有还无。”——《红楼梦》

if( x )

在if语句中,会触发Boolean上下文,JavaScript会将值强制类型转换为布尔值,再执行判断。 当 x is Trusy时,if语句命中。

在JavaScript中,存在七种假值(又称虚值),分别为undefined null "" false 0 0n NaN。其中0nESFuture中新增的一种假值。

undefined是最常见的假值,当判断在一个对象中是否存在某个字段时,使用的就是undefined空值判断:

let people = {name}
if(people.age){  // 等价于 people.age === void 0
		// ...
}

建议在代码中使用void 0来代替undefined,原因是undefined不是JS的保留字,而且void 0undefined字符少。 事实上,不少JavaScript压缩工具在压缩过程中,将undefined使用void 0代替。

二、相等判断

雄兔脚扑朔,雌兔眼迷离。双兔傍地走,安能辨我是雄雌。——《木兰诗》

在JavaScript中,有两个相等运算符来判断两个操作数是否相等,一个是==相等运算符,另一个是===全等运算符。它们最大区别在于对类型的宽容度。

x === y

===全等运算符对左右两边的孩子(操作数)是严厉的,就像一个严厉的父亲。 ===全等运算符首先会检查两边的操作数类型是否一致,然后再检查其值。具体流程如下:

  1. 两边类型不同,返回false;

  2. 类型相同,比较其值:

    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标准中寻找答案。

基本流程如下:

  1. 如果type(x) === type(y) , 此处比较过程与===相同;
  2. 如果x is nully is undefined,则返回true;反之亦然;
  3. 如果x is stringy is number,则执行ToNumber(x) == y;反之亦然;
  4. 如果x is boolean,则执行ToNumber(x) == y;反之亦然;
  5. 如果x is object,则执行ToPrimitive(x) == y;反之亦然;
  6. 都不是以上情况,返回false;

简要概述:类型相同,拼家世拼内涵;类型不同,先平等后比较;nullundefined是一家,number一家独大,其他都要向我靠;object你别骄傲,降成平民再比较。

在上面过程中我们看到,如果两个操作数的类型相同,则比较过程与===是一致的。如果类型不同,那么就需要隐式类型转换为类型相同的情况后再比较。

上面提到了ToNumberToPrimitive的转换过程就是隐式的类型转换。那么它们究竟做了些什么呢?

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老大哥就勇敢站出来了,需要注意的是toStringObject原型链上的方法。其实,每个内置对象都有一个toString方法,不同对象的toString的行为都是不一样,但它们都是继承自Object{}.toString.call(x)返回一个字符串[object Type],其中Type可能取值为 Number String Boolean Function Undefined Null Object Symbol Date Math RegExp ...

ECMA标准这样描述:

  1. x is undefind, return [object Undefined];
  2. x is null,return [object Null];
  3. let O = toObject(x);
  4. 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。

总结

  1. JS中存在七个假值(Falsy),undefined null "" false 0 0n NaN
  2. ===全等运算符比==相等运算符更加严格,且更符合相等性判断预期;
  3. typeoftoString的类型判断方法可以覆盖80%的类型判断场景,够用;
  4. 显示类型转换可以减少隐式类型转换的不确定性;

ECMA参考标准