空虚
我不建议尝试定义或使用计算整个世界中的任何值是否为空的函数。 “空”的真正含义是什么?如果我有let human = { name: 'bob', stomach: 'empty' },是否应该isEmpty(human) 返回true?如果我有let reg = new RegExp('');,是否应该isEmpty(reg) 返回true? isEmpty([ null, null, null, null ]) 怎么样 - 这个列表只包含空,所以列表本身是空的吗?我想在这里提出一些关于 javascript 中“空洞”(一个故意模糊不清的词,以避免预先存在的关联)的注释 - 我想争辩说 javascript 值中的“空洞”永远不应该被笼统地处理。
真/假
为了决定如何确定值的“空性”,我们需要适应 javascript 内置的、固有的关于值是“真”还是“假”的感觉。当然,null 和 undefined 都是“虚假的”。不太自然的是,数字0(除了NaN 之外没有其他数字)也是“假的”。最不自然:'' 是虚假的,但 [] 和 {}(以及 new Set() 和 new Map())是真实的 - 尽管它们看起来都同样空洞!
空与未定义
还有一些关于null 与undefined 的讨论——我们真的需要两者来表达我们的程序的空洞性吗?我个人避免让字母 u、n、d、e、f、i、n、e、d 以这种顺序出现在我的代码中。我总是用null 来表示“空虚”。不过,我们需要再次适应 javascript 对 null 和 undefined 不同之处的固有认识:
- 尝试访问不存在的属性会得到
undefined
- 调用函数时省略参数会导致该参数接收
undefined:
let f = a => a;
console.log(f('hi'));
console.log(f());
- 只有在给定
undefined而不是null时,具有默认值的参数才会接收默认值:
let f = (v='hello') => v;
console.log(f(null));
console.log(f(undefined));
对我来说,null 是一个明确的空洞符号; “本来可以填写的东西故意留空”。
真的undefined 是一个必要的复杂性,它允许一些 js 功能存在,但在我看来 它应该始终留在幕后;没有直接互动。例如,我们可以将undefined 视为 javascript 实现默认函数参数的机制。如果您不向函数提供参数,它将收到 undefined 的值。如果该参数最初设置为undefined,则默认值将应用于函数参数。在这种情况下,undefined 是默认函数参数的关键,但它保留在后台:我们可以实现默认参数功能,而无需参考 undefined:
这是默认参数的错误实现,因为它直接与 undefined 交互:
let fnWithDefaults = arg => {
if (arg === undefined) arg = 'default';
...
};
这是一个很好的实现:
let fnWithDefaults = (arg='default') => { ... };
这是接受默认参数的不好方法:
fnWithDefaults(undefined);
只需这样做:
fnWithDefaults();
非泛型空虚
我认为永远不应该以通用的方式处理空虚。相反,我们应该始终严格地在确定数据是否为空之前获取有关我们数据的更多信息 - 我主要通过检查我正在处理的数据类型来做到这一点:
let isType = (value, Cls) => {
// Intentional use of loose comparison operator detects `null`
// and `undefined`, and nothing else!
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 的通用性并确保它始终是Array 的一种非常快速且富有表现力的方式。不过,有时我们需要允许v 具有多种类型。在这些情况下,我们应该尽早创建 v 不再是通用的代码块:
if (isType(v, String)) {
/* v isn't generic in this block - It's a String! */
} else if (isType(v, Number)) {
/* v isn't generic in this block - It's a Number! */
} else if (isType(v, Array)) {
/* v isn't generic in this block - it's an Array! */
} else {
throw new Error('Expected String, Number, or Array');
}
-
始终使用“白名单”进行验证。如果您需要一个值,例如字符串、数字或数组,请检查这 3 种“白色”可能性,如果都不存在则抛出错误3 满意。我们应该能够看到检查“黑色”的可能性并不是很有用:假设我们写
if (v === null) throw new Error('Null value rejected'); - 这对于确保 null 值不会通过它非常有用,但是如果一个值 确实 通过了,我们仍然对此一无所知。通过此 null 检查的值 v 仍然非常通用 - 除了null 之外的任何东西!黑名单很难消除普遍性。
-
除非值是
null,否则永远不要考虑“空值”。相反,请考虑“一个空的 X”。 本质上,永远不要考虑做类似 if (isEmpty(val)) { /* ... */ } 之类的事情 - 无论 isEmpty 函数是如何实现的(我不想知道......),它没有意义!而且太笼统了!真空度只能在知道val 的类型的情况下计算。真空度检查应如下所示:
-
“一个字符串,没有字符”:
if (isType(val, String) && val.length === 0) ...
-
“一个对象,有 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) ...