Day 19 - 相等判断与型别转换

前言

昨天我们介绍了 undefinednullNaN,也带到了如何将这些特别的值判断出来。

今天我们要来看更多,更多这些会让人感到困惑的判断式,以及它们在实战中误用会发生什麽惨况。

本篇文章会参考 JavaScript-Equality-Table,这是个视觉化又好懂的整理图表,非常推荐在感到困惑的时候来看看。

主要有两个主题:

  • 一般相等 ( == ) 与严格相等 ( === )
  • if 判断式

一般相等 ( == ) 与严格相等 ( === )

这个会是我们通篇讨论的基础,一律使用严格相等( === )来判断

  • 一般相等是「会先经过转型」,左右型别一致後,再看是否相等
  • 严格相等则不会,如果型别不同就直接回传 false

为什麽不要用一般相等?

一般相等的转型满复杂的,而且转型的规则不是那麽直觉,所以这边不特别讲,可以参考网站中的这张表格,一目了然:

按照上图,我们可能会有这种不太直觉的东西:

if (1 == "1") console.log(' 1 == "1" ');
if (true == "true") console.log(' true == "true" ');

看起来,第一个判断式如果成立,那第二个应该也要成立吧?都只是数值的外面框上一个双引号变成字串呀?

但结果只有第一个判断式成立,而第二个却不是。

执行结果

1 == "1"

诸如此类的神奇的转型规则还有很多,可以自行参照上图,但正是因为转型规则太复杂,要使用还得不断思考转型後是否如预期,背一大堆转型的例外。

因此我们才会说一律使用严格相等,果断放弃转型,确保等号两边一定是「基於型别相等的比较」,比较合乎逻辑。

严格相等

Javascript 真的是有很多奇特的点,虽然严格相等已经比一般相等容易理解了,但还是有原则与例外,先来看网站上的图表:

是不是比一般相等乾净多了?

基本上严格相等就是要符合这两点,才会成立:

  • 同型别
  • 同值

primitive v.s. non-primitive

需特别注意这边所谓的「同值」,会根据数值本身是 primitive 或 non-primitive 而又有不同,所以应该这样写:

  • 同型别
  • 同值
    • 如果是 primitive,长相要一模一样
    • 如果是 non-primitive,记忆体位址要一模一样

所以才会看到上图中,绿色的格子并没有填满 []{}[0] 这种 non-primitive 数值,代表以下都是不成立的:

console.log([] === []); // false
console.log({} === {}); // false
console.log([0] === [0]); // false

基本上每次产生一个 object,记忆体位址就是新的一个,除非把 object 放到变数存起来,否则 non-primitive 的比较通常都不会成立:

const person = { name: 'Joey' };
console.log(person === person); // true
console.log(person === { name: 'Joey' }); // false

经过拷贝後呢?因为拷贝其实是把原本 object 内的 key/value 复制到一个新的 object,所以仍然会有一个全新的记忆体位址产生:

const person = { name: 'Joey' };
const copiedPerson = { ...person };

console.log(person === copiedPerson); // false

唯一的例外

就是 NaN,请参考昨天的文章:

console.log(NaN === NaN); // false

结论

判断是否相等时,除了 NaN 要用 isNaN() 来判断,其他全都用严格相等,并且特别注意 non-primitive 的比较。

if 判断式

在搞定了一般相等与严格相等之後,我们接着来看另一种常见的写法,就是更懒一点,连等号都不写了,直接把变数或数值塞在判断式里面:

if (300) conosle.log('会执行');
if ('Hello') conosle.log('会执行');
if ([1,2,3,4]) conosle.log('会执行');

这种判断式里面也会转型,不过比较单纯一点,单纯就是帮你转成 true 或 false

Truthy & Falsy

会被转成 true 的数值,就叫做 truthy value,反之叫做 falsy value。

  • truthy value:不是 falsy 的
  • falsy value:false, 0, -0, 0n, "", null, undefined, NaN

可以注意到,有些我们感觉应该要是空值的东西,其实并不是 falsy:

if ([]) conosle.log('会执行'); // 会执行
if ({}) conosle.log('会执行'); // 会执行
if ("0") conosle.log('会执行'); // 会执行

实战上的陷阱

基於这个原因,如果直接把变数放进 if 判断式转型,有时候会出意外:

const arr = [
    { name: 'Jack', score: 70 },
    { name: 'Allen', score: 65 },
    { name: 'Alice', score: 60 },
    { name: 'Susan', score: 90 }
];
const failedArr = arr.filter(item => item.score < 60);

if (failedArr) {
    const displayFailedArr = failedArr.map(item => item.name).join('、');
    console.log(`低於 60 分名单:${displayFailedArr}`);
}

执行结果

低於 60 分名单:

上面的范例,没有人低於 60 分,所以其实第 7 行的 failedArr 是一个空阵列 []

而第 9 行的 if (failedArr) 就会变成 if ([]),其实没有打算执行 if 里面的程序,但因为 []truthy,所以还是会成立,最後印出来的名单就好像缺了什麽东西一样。

判断阵列内是否有值

如果要判断阵列内是否有值,可以使用 Array.isArray(arr) 加上 arr.length > 0 两个判断:

const arr = [
    { name: 'Jack', score: 70 },
    { name: 'Allen', score: 65 },
    { name: 'Alice', score: 60 },
    { name: 'Susan', score: 90 }
];
const failedArr = arr.filter(item => item.score < 60);

if (Array.isArray(failedArr) && failedArr.length > 0) {
    const displayFailedArr = failedArr.map(item => item.name).join('、');
    console.log(`低於 60 分名单:${displayFailedArr}`);
}

执行结果


如果嫌 if 判断式内太冗长,可以考虑使用 lodash/isEmpty,或者自行把判断式写成共用函式,这样就不会到处都要写这麽长:

const isArrayEmpty = (inputArr) => Array.isArray(inputArr) && inputArr.length === 0;

// ... 这边都跟上个范例一样

if (!isArrayEmpty(failedArr)) {
    // ... 这边都跟上个范例一样
}

0 也是 falsy

const arr = [
    { name: 'Jack', score: 70 },
    { name: 'Allen' },
    { name: 'Alice', score: 60 },
    { name: 'Susan', score: 0 }
];

arr.forEach(item => {
    if (item.score) {
        console.log(`${item.name}:${item.score} 分`);
    }
});

执行结果

Jack:70 分
Alice:60 分

上面的范例 arr 有一些缺陷,有一些人是没有 score 的,面对这种通常会用到 filter 先筛选一遍,把有问题的资料筛掉,或者直接跑 forEach 用 if 来判断。

所以我们试着把 score 放进 if 判断式,期待能够把「有 score 的人都印出来」。

但在众多 number 里面,唯独 0 是 falsy (先不讨论 NaN),所以如果直接把一个 number 放进 if 判断式,遇到 0 的 case 就会发生意外了(如上例 Susan 没有被印出来)。

因此,如果是要判断「是不是数字」,可以用 Number.isInteger() 判断整数,或 Number.isFinite() 判断有限数:

const arr = [
    { name: 'Jack', score: 70 },
    { name: 'Allen' },
    { name: 'Alice', score: 60 },
    { name: 'Susan', score: 0 }
];

arr.forEach(item => {
    if (Number.isFinite(item.score)) {
        console.log(`${item.name}:${item.score} 分`);
    }
});

执行结果

Jack:70 分
Alice:60 分
Susan:0 分

同理,如果嫌 if 判断式内太冗长,也可以拉出去当共用函式:

const isNormalNumber = (inputNum) => Number.isFinite(inputNum);

// ... 这边都跟上个范例一样

arr.forEach(item => {
    if (isNormalNumber(item.score)) {
        // ... 这边都跟上个范例一样
    }
});

排除空值

接着回到我们昨天丢下的问题,有些人会想用 「!」 这个运算子,来一次排除掉这三种空值的概念:

const a = undefined;
const b = null;
const c = NaN;
if (!a && !b && !c) {
    console.log('用一个惊叹号似乎就可以判断了?');
}

如果看完前面的介绍,你应该就会发现,用这种方式除了会把 undefinednullNaN 这三个滤掉,其实也会把所有 falsy 的数值都滤掉,包括了 false0""

而且如果是想要滤掉 []{} 这种,其实是 truthy 的值,反而会达不到效果。

关於这点我其实也找不到一个统一的解,毕竟真的是要看 if 到底要筛选出什麽样的资料,有时候 null 要可以过,有时候 [] 不可以过。

因此我想,认真学会每个类别的判断方式,并且在适当的时机拿出来用,是比较有效的方式,自己也才真的知道在做什麽。

typeof
Number.isFinite()
Number.isNaN()
Array.isArray()

结语

这个章节写完之後,才发觉 Javascript 身为一个弱型别语言,在类别转型方面真的是充满惊恐,对於新手来说或许是友善的,但对於半生不熟的老菜鸟们,想要深入去理解背後的判断原理,真的是需要下一番苦功。

转生之後
没了面孔与相貌
只剩灵魂的连结

参考资料

JavaScript-Equality-Table
Equality_comparisons_and_sameness


<<:  D20/ 怎麽在 compose 与 non-compoe 间传资料 - Compose Side-Effect part 2

>>:  从pyside2 快速移植到pyside6的方法

Day 11 来吧!开始你的第一个广告活动

当你开始想要为你的产品或是服务广告,注册完 Google Ads 後,需要填入你的网站资讯,让我们逐...

【Day 3_ 关於Arm与袋狼的那些事】

制作出Candy Crush的游戏开发商King在今年3月时宣布推出拥有25年丰富历史、陪伴全球许多...

【从实作学习ASP.NET Core】Day13 | 後台 | 编辑与删除

昨天刚打完疫苗,没想到晚上所有症状通通都来了,整个人烂得跟什麽一样,有够惨 今天就把简单的部分贴一贴...

【没钱买ps,PyQt自己写】Day 29 - final project - 2 / 来搞一个自己的 photoshop 吧!後段程序细节篇 (结合 PyQt + OpenCV)

看完这篇文章你会得到的成果图 此篇文章的范例程序码 github https://github.co...

【Day 27】情境模拟:该如何协助设计师,提供可以执行的设计稿 !?

要注意什麽,才能更顺利地提供可执行的设计稿 !? 一、参考热门的设计框架、典范大厂的 Web 框架或...