logo
Published on

JavaScript 中是否有标准函数来检查 null、未定义或空白变量?

Authors
  • Name
    Twitter

虚空性

我并不推荐定义或使用一个函数来判断任意值是否为空。什么才是真正的“空”呢?如果我有一个变量 let human = { name: 'bob', stomach: 'empty' },那么 isEmpty(human) 应该返回 true 吗?再比如 let reg = new RegExp('');isEmpty(reg) 应该返回 true 吗?更甚者 isEmpty([ null, null, null, null ]) 这列表中只包含空值,但列表本身算空吗?在本文中,我想讨论一下JavaScript中的“虚空性”这个(故意选择的模糊词汇,以避免先入为主的联想)概念,并强调不应普遍地处理JavaScript值中的“虚空性”。


真值/假值

在决定如何判断值的“虚空性”时,我们需要考虑JavaScript内在的真值(truthy)和假值(falsy)概念。当然,nullundefined都是“假值”。不那么自然的是,数字0(以及唯一的其他数字 NaN)也是“假值”。最不自然的是:'' 是假值,但 []{} (还有 new Set()new Map())却是“真值”——虽然它们看起来都同样虚空!


nullundefined

在讨论 nullundefined 时,我们是否真的需要两者来表达程序中的虚空性?我个人避免在代码中出现 undefined。我总是使用 null 来表示“虚空性”。不过,我们需要了解JavaScript中 nullundefined 的固有区别:

  • 访问不存在的属性会得到 undefined
  • 调用函数时省略参数会导致该参数接收 undefined
let f = a => a;
console.log(f('hi')); // 'hi'
console.log(f()); // undefined
  • 具有默认值的参数仅在传 undefined 时接受默认值,而非 null
let f = (v='hello') => v;
console.log(f(null)); // null
console.log(f(undefined)); // 'hello'

对我而言,null 是“虚空性”的明确标志;“有些东西本可以被填充,但被故意留空”。undefined 是为某些JavaScript特性存在的必要复杂性,但我认为它应始终在后台处理,不应直接与之交互。

实现默认函数参数时,不应直接与 undefined 交互,例如:

let fnWithDefaults = arg => {
  if (arg === undefined) arg = 'default';
  // ...
};

而应采用:

let fnWithDefaults = (arg='default') => { ... };

不应直接这样来接受默认参数:

fnWithDefaults(undefined);

而应使用:

fnWithDefaults();

顺便说一句: 如果你有一个函数带多个参数,你想用某些参数并保留其他参数的默认值怎么办?

例如:

let fnWithDefaults = (a=1, b=2, c=3, d=4) => console.log(a, b, c, d);

如果想为 ad 提供值并接受其他的默认值,错误的做法如下:

fnWithDefaults(10, undefined, undefined, 40);

正确的做法是改用对象参数:

let fnWithDefaults = ({ a=1, b=2, c=3, d=4 }={}) => console.log(a, b, c, d);
fnWithDefaults({ a: 10, d: 40 }); // 现在看起来更简洁!(并且不再涉及 "undefined")

针对性虚空性

我认为不应以通用方式处理虚空性。相反,我们应严谨地获取有关数据的更多信息再确定其是否虚空——我主要通过检查数据类型来做到这一点:

let isType = (value, Cls) => {
  return value != null && Object.getPrototypeOf(value).constructor === Cls;
};

注意这个函数忽略了继承关系——它期望 valueCls 的直接实例,而非 Cls 的子类实例。我避免使用 instanceof 主要有两个原因:

  • ([] instanceof Object) === true (“数组是对象”)
  • ('' instanceof String) === false (“字符串不是字符串”)

需要注意使用 Object.getPrototypeOf 是为了避免一些(模糊的)边界情况,例如 let v = { constructor: String };isType 函数仍然会正确返回 isType(v, String) (false),和isType(v, Object) (true)。

总体来说,我推荐使用这个 isType 函数并结合如下技巧:

  • 尽量减少处理类型未知的值的代码量。 例如,对于 let v = JSON.parse(someRawValue);,我们的 v 变量现在是未知类型。我们应该尽早限制其可能性。最佳方式是通过要求特定类型来做到:例如 if (!isType(v, Array)) throw new Error('Expected Array');——这是快速且明了地消除 v 的泛型性质,并确保其总是一个数组。有时,我们需要允许 v 具有多种类型。在这种情况下,我们应尽早创建 v 不再是泛型的代码块:
if (isType(v, String)) {
  /* 在这个代码块中,v 不是泛型 - 它是字符串! */
} else if (isType(v, Number)) {
  /* 在这个代码块中,v 不是泛型 - 它是数字! */
} else if (isType(v, Array)) {
  /* 在这个代码块中,v 不是泛型 - 它是数组! */
} else {
  throw new Error('Expected String, Number, or Array');
}
  • 始终使用“白名单”进行验证。 如果需要一个值是例如字符串、数字或数组,检查这三种“白"的可能性,如果都不符合,则抛出错误。检验“黑”可能性通常没什么用;例如写 if (v === null) throw new Error('Null value rejected'); 这对于确保 null 值不通过是不错,但如果一个值通过这个空检查,我们仍然对它知之甚少。通过这一空检查的值 v 仍然非常泛型——它只是“除了 null 以外的任何值”!黑名单几乎无助于消除泛型化。
  • 除非一个值是 null,否则不要考虑“一个虚空的值”。而是考虑“一个虚空的 X”。 本质上,不要做类似 if (isEmpty(val)) { /* ... */ } 的事情——无论 isEmpty 函数如何实现(我不想知道...),它都没有意义!而且它过于泛型!虚空性应仅在了解 val 类型的情况下进行计算。虚空检查应如下所示:
    • “一个字符为空的字符串”:if (isType(val, String) && val.length === 0) ...

    • “一个没有属性的对象”:if (isType(val, Object) && Object.entries(val).length === 0) ...

    • “小于或等于零的数字”:if (isType(val, Number) && val <= 0) ...

    • “一个没有项的数组”:if (isType(val, Array) && val.length === 0) ...

    • 唯一例外是当 null 用于表示某些功能时。在这种情况下,说“一个虚空的值” 是有意义的: if (val === null) ...