作者:Eric Elliott
翻译:疯狂的技术宅
原文:https://medium.com/javascript…
未经允许严禁转载
许多 JavaScript 开发人员正在为怎么处理可选值头痛。有什么好办法来最大程度地减少由值(可能为 null
、undefined
或在运行时未初始化)引起的错误?
在某些情况下,一些语言具有内置功能。在某些静态类型的语言中,你可以说 null
和 undefined
是非法值,并且让你的编程语言在编译时抛出 TypeError,但是即使在那些语言中,也无法防止 null 输入在运行时流入程序。
为了更好地处理这类问题,我们需要了解这些值的来源。以下是一些最常见的来源:
- 用户输入
- 数据库 / 网络记录
- 未初始化状态
- 函数什么也不会返回
User Input
用户输入
在处理用户输入时,对这些输入进行验证是第一道也是最好的防线。我经常依靠模式验证器来完成这项工作。例如,检查 react-jsonschema-form。
从流水记录输入
我总是从网络、数据库或用户输入的流水记录中获得的输入。例如我可以用 redux action creators 处理 undefined
值来合并用户记录:
const setUser = ({name = 'Anonymous', avatar = 'anon.png'} = {}) => ({
type: setUser.type,
payload: {
name,
avatar
}
});
setUser.type = 'userReducer/setUser';
有时,你需要根据数据的当前状态显示不同的内容。如果在初始化所有数据之前显示页面,则可能会遇到这种情况。例如当你向用户显示资金余额时,可能会在加载数据之前意外地显示余额为 $0,这会让用户感到不安。你可以创建自定义数据类型,这些数据类型根据当前状态生成不同的输出:
const createBalance = ({
// default state
state = 'uninitialized',
value = createBalance.empty
} = {}) => ({
__proto__: {uninitialized: () => '--',
initialized: () => value,
format () {return this[this.getState()](value);
},
getState: () => state,
set: value => {const test = Number(value);
assert(!Number.isNaN(test), `setBalance Invalid value: ${value}`);
return createBalance({
state: 'initialized',
value
});
}
}
});
createBalance.empty = '0';const setBalance = value => createBalance().set(value);const emptyBalanceForDisplay = createBalance()
.format();
console.log(emptyBalanceForDisplay); // '--'const balanceForDisplay = setBalance('25')
.format(balance);
console.log(balanceForDisplay); // '25'// Uncomment these calls to see the error cases:
// setBalance('foo'); // Error: setBalance Invalid value: foo// Error: createBalance Invalid state: THIS IS NOT VALID
// createBalance({state: 'THIS IS NOT VALID', value: '0'});
上面的代码是一个状态机,不会显示无效状态。当首次创建余额时,它将被设置为 uninitialized
状态。如果你在状态 uninitialized
时尝试显示余额,则始终会得到一个占位符值(“--
”)。
要更改这个值,你必须通过调用 .set
方法或在 createBalance
工厂中定义的 setBalance
来显式的设置一个值。
状态本身是 encapsulated,以保护其免受外界干扰,且可以确保其他函数无法捕获它并将其设置为无效状态。
注意:你可能想知道为什么我要用字符串而不是数字来举例,那是因为用大数字符串来表示货币类型具有十进制精度,可以避免舍入错误,并准确地表示加密货币交易的值,这样可以得到任意有效的十进制精度。
如果你使用 Redux 或 Redux 架构,则可以用 Redux-DSM 声明状态机。
避免创建 null
和 undefined
值
在你自己的函数中,可以避免一开始就创建 null
或 undefined
值。我想到了很多内置于 JavaScript 的方法。见下文。
避免 null
我从来没有在 JavaScript 中显式地创建过 null
值,因为我从来没有真正看到过它的意义。
从 2015 年以来,JavaScript 开始支持默认值,当你不提供相关参数或属性的值时,这些默认值就会被填写。这些默认设置不适用于 null
值。根据我的经验,这通常是一个错误。为了避免这种陷阱,请不要在 JavaScript 中使用 null
。
如果你希望对未初始化的值或空值使用特殊情况,则状态机是更好的选择。
新的 JavaScript 功能
有几个功能可以帮助你处理 nul
或 undefined
值。在撰写本文时,这两个都是第 3 阶段的建议。也许将来你就可以使用它们了。
在撰写本文时,可选链接是第 3 阶段的建议。它是这样的:
const foo = {};
// console.log(foo.bar.baz); // throws error
console.log(foo.bar?.baz) // undefined
空位合并运算符
“空位合并运算符”也是要添加到规范中的第 3 阶段建议,它基本上是“后备值运算符”的一种奇特方法。如果左侧的值为 undefined
或 null
,则其求值为右侧的值。它是这样的:
let baz;
console.log(baz); // undefined
console.log(baz ?? 'default baz');
// default baz// Combine with optional chaining:
console.log(foo.bar?.baz ?? 'default baz');
// default baz
如果未来还没有到来,则需要安装 @babel/plugin-proposal-optional-chaining
和 @babel/plugin-proposal-nullish-coalescing-operator
.。
异步与 Promise
如果某个函数可能没有返回值,那么最好将其包装在 Either 中。在函数式编程中,Either monad 是一种特殊的抽象数据类型,它允许你附加两个不同的代码路径:成功路径或失败路径。JavaScript 有称为 Promise 的内置异步 Either monad-ish 数据类型。你可以用它对未定义的值进行声明式错误分支:
const exists = x => x != null;const ifExists = value => exists(value) ?
Promise.resolve(value) :
Promise.reject(`Invalid value: ${ value}`);ifExists(null).then(log).catch(log); // Invalid value: null
ifExists('hello').then(log).catch(log); // hello
你可以根据需要编写一个同步版本,但我把它留给你做练习。如果你对 functors 和 monads 比较熟悉,那么过程将变得更加容易;如果这听起来令人生畏,也不用担心,只不过是使用 promise。它们是内置的,并且在大多数情况下都可以正常工作。
Maybe 数组
数组实现一个 map
方法,该方法采用一个应用于每个元素数组的函数。如果数组为空,则永远不会调用该函数。换句话说,JavaScript 中的数组可以填补 Haskell 等语言中 Maybe 的角色。
什么是 Maybe?
Maybe 是一种特殊的抽象数据类型,它封装了一个可选值。数据类型有两种形式:
- Just — 包含一个值
- Nothing — 没有值
其核心思想是这样的:
const log = x => console.log(x);
const exists = x => x != null;const Just = value => ({map: f => Just(f(value)),
});const Nothing = () => ({map: () => Nothing(),});const Maybe = value => exists(value) ?
Just(value) :
Nothing();const empty = undefined;
Maybe(empty).map(log); // does not log
Maybe('Maybe Foo').map(log); // logs "Maybe Foo"
上面仅是演示这个概念的例子。你可以围绕 Maybe 建立一个有用函数的完整库去实现其他操作,如 flatMap
和 flat
(在编写多个 Maybe 返回函数时,避免使用 Just(Just(value))
)。但是 JavaScript 已经有了一种数据类型,该数据类型可以直接实现这些功能,它就是数组。
如果你要创建一个可能会也可能不会产生结果的函数(尤其是可能有多个结果),则下面是一个很好的例子。
const log = x => console.log(x);
const exists = x => x != null;const arr = [1,2,3];
const find = (p, list) => [list.find(p)].filter(exists);
find(x => x > 3, arr).map(log); // does not log anything
find(x => x < 3, arr).map(log); // logs 1
我发现不在空列表上调用 map
对于避免 null
和 undefined
值非常有用,但是请记住,如果数组中包含 null
和 undefined
值,它将调用函数处理这些值,因此,如果你的函数可能会产生 null
或 undefined
,则需要将其从返回的数组中过滤掉。这可能会改变集合的长度。
在 Haskell 中,有一个函数 maybe
(类似 map
)将一个函数应用于一个值。但是该值是可选的,并封装在 Maybe 中。我们可以用 JavaScript 的 Array 数据类型做同样的事:
// maybe = b => (a => b) => [a] => b
const maybe = (fallback, f = () => {}) => arr =>
arr.map(f)[0] || fallback;// turn a value (or null/undefined) into a maybeArray
const toMaybeArray = value => [value].filter(exists);// maybe multiply the contents of an array by 2,
// default to 0 if the array is empty
const maybeDouble = maybe(0, x => x * 2);const emptyArray = toMaybeArray(null);
const maybe2 = toMaybeArray(2);// logs: "maybeDouble with fallback: 0"
console.log('maybeDouble with fallback:', maybeDouble(emptyArray));
// logs: "maybeDouble(maybe2): 4"
console.log('maybeDouble(maybe2):', maybeDouble(maybe2));
maybe
会使用一个后备值,然后是一个映射到 may 数组上的函数,然后是一个 may 数组(一个数组包含一个值,或者什么都不包含),然后返回将该函数应用于数组内容的结果,或者返回数组为空时的值。
为了方便起见,我还定义了一个 toMaybeArray
函数,并添加了一个 maybe
函数来进行演示。
如果你想在生产环境代码中执行类似的操作,我已经创建了一个经过单元测试的开源库,可以使它变得更容易,它的名字是 Maybearray。Maybearray 与其他 JavaScript Maybe 库相比的优势在于,它使用原生 JavaScript 数组去表示值,因此你不必对其进行任何特殊处理或进行任何转换处理。当你在调试中遇到 Maybe 数组时,不必问“这是什么奇怪的类型?!”,它只是一个值数组或一个空数组,你已经看到过一百万遍了。
本文首发微信公众号:前端先锋
欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章
欢迎继续阅读本专栏其它高赞文章:
- 深入理解 Shadow DOM v1
- 一步步教你用 WebVR 实现虚拟现实游戏
- 13 个帮你提高开发效率的现代 CSS 框架
- 快速上手 BootstrapVue
- JavaScript 引擎是如何工作的?从调用栈到 Promise 你需要知道的一切
- WebSocket 实战:在 Node 和 React 之间进行实时通信
- 关于 Git 的 20 个面试题
- 深入解析 Node.js 的 console.log
- Node.js 究竟是什么?
- 30 分钟用 Node.js 构建一个 API 服务器
- Javascript 的对象拷贝
- 程序员 30 岁前月薪达不到 30K,该何去何从
- 14 个最好的 JavaScript 数据可视化库
- 8 个给前端的顶级 VS Code 扩展插件
- Node.js 多线程完全指南
- 把 HTML 转成 PDF 的 4 个方案及实现
- 更多文章 …