- Published on
JavaScript 中是否有标准函数来检查 null、未定义或空白变量?
- Authors
- Name
虚空性
我并不推荐定义或使用一个函数来判断任意值是否为空。什么才是真正的“空”呢?如果我有一个变量 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)概念。当然,null
和undefined
都是“假值”。不那么自然的是,数字0
(以及唯一的其他数字 NaN
)也是“假值”。最不自然的是:''
是假值,但 []
和 {}
(还有 new Set()
和 new Map()
)却是“真值”——虽然它们看起来都同样虚空!
null
与 undefined
在讨论 null
和 undefined
时,我们是否真的需要两者来表达程序中的虚空性?我个人避免在代码中出现 undefined
。我总是使用 null
来表示“虚空性”。不过,我们需要了解JavaScript中 null
和 undefined
的固有区别:
- 访问不存在的属性会得到
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);
如果想为 a
和 d
提供值并接受其他的默认值,错误的做法如下:
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;
};
注意这个函数忽略了继承关系——它期望 value
是 Cls
的直接实例,而非 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) ...