深层属性,轻松提取

1,735 阅读4分钟
原文链接: zhuanlan.zhihu.com
本文所介绍的工具是 JavaScript 中获取深层嵌套对象的一种解决方案,同时也是 ES6 Proxy 与 TypeScript 结合的落地应用。

面临的问题

假设有这样一个对象,表示的是 用户是否启用了回复通知的设置

const settings = {
    notification: {
    	reply: {
            active: true
        }
        // ...其他设置项
    }
    // ...其他设置项
}

当开发者想要提取 active 的值,最直接的方法是这么做

const isNotificationReplyActive = settings.notification.reply.active

但这是不安全的,因为 JavaScript 中通常使用 `null` 或 `undefined` 分别表示未定义或未声明的值

typeof someVar === 'undefined' // 未声明
let someVar = null             // 已声明,未定义

实际开发过程中可能因为比如节省资源的考虑,当用户未进行过设置时,它的 notification 或者更深的某一级的值是 `null` 或 `undefined`,而非对象。

// 比如当未设置回复通知时,它是这样的
const settings = {
    notification: {
    	// 没有 reply
    }
}

// 这种情况下, settings.notification.reply 的值是 undefined
// JS 中试图获取 undefined 上的 key 时会触发 TypeError
const isNotificationReplyActive = settings.notification.reply.active // TypeError!

于是有的开发者采取了这样的措施

const isNotificationReplyActive = settings
    && settings.notification
    && settings.notification.reply
    && settings.notification.reply.active
// 或者
try {
    const isNotificationReplyActive = settings.notification.reply.active
} catch (err) {
    // 错误处理
}

经验丰富的开发者都知道,这样做的缺点很多,在此就不展开了。


于是一些工具函数诞生了,比如 lodash 的 `_.get`

import _ from 'lodash'
const isNotificationReplyActive = _.get(settings, 'notification.reply.active')

虽然它保证了开发者在提取属性的过程中不会因为遇到 `undefined` 或 `null` 之类的值而抛出 TypeError ,但缺点也很明显——

  1. 属性的路径被写成了字符串,开发者无法获得 IDE/编辑器 的自动补全与智能纠错。
  2. 不能使用便捷的解构语法——
const { notification: { reply: { active } } } = settings


简直是一夜回到解放前。

解决方法 —— safe-touch

现在让我们来回顾一下本文开头的那张图,它即是本文的主角、上述所有问题的解决方案——safe-touch。

safe-touchwww.npmjs.com图标

来看一下如何使用它:

// 引入
import safeTouch from 'safe-touch'

const settings = { /* ... */ }
// 包裹要提取的对象
const touched = safeTouch(settings)

// 把它当作函数调用,可以获得原始值
touched() === settings // true

// 亦可以直接获取 settings 上存在的属性,同样通过调用取得属性值
// 在现代化的 IDE/编辑器 中,这一过程可以给出智能提示与自动补全
touched.notification.reply.active() // 若依本文开头给出的例子,值为 true

// 可以安全地获取并不存在的属性,返回 undefined ,不会抛出 TypeError
touched.something.does.not.exist[Math.random()]() // undefined

// 支持解构
const { notification: { reply: { active, notExistingKey } } } = touched
active() // true
notExistingKey() // undefined

在 VS Code 等现代化工具中可以获得智能提示:

支持自动补全
支持解构

性能

我做了一套简单的性能测试,分别在 NodeJS v8.9.1 和 v10.7.0 进行了测试,结果在这个 gist

测试用的代码在这里

我简单总结一下测试结果——

  1. 几乎仅在取的属性的嵌套层级多于五层且所有层级的属性都存在的情况下,即能成功取到深层属性的情况下,safe-touch 的性能是低于 try...catch 的。说明生成很多 Proxy 实例的开销大于创建一个 try...catch 块但未触发错误的开销。
  2. 如果提取属性的过程中遇到错误,即取不存在的属性下的属性, 则形势大反,safe-touch 的性能基本是 try...catch 的几十倍。
  3. 社区中(评论里提到的)一些其他类似的工具,不乏通过 try...catch 实现的,因此综合开发体验来说, safe-touch 可能是更好的选择。

通过测试也发现在 NodeJS v10 下用 Proxy 的性能远好于 v8 下的,相比大约是 5 倍。

我的测试脚本是运行十万次的耗时统计,在实际开发的应用中采取上述任何一种方式可能并不会有用户体验上的差别。但不管哪种方式,都比不上原生的 chain 方式,因此我很期待 ?. 操作符的正式问世,即使它的作用只是转化成等价的 chain 形式的表达式,也会是非常棒的。

babel 已经提供了 ?. 操作符 @babel/plugin-proposal-optional-chaining ,感兴趣的可以玩一下。

另外还有一个反直觉的现象,在测试结果的最后部分,deepChainedRetrieve 比 deepRetrieve 快,即 a && a.b && ... && a.b.c.d.e.f.g.h.i 的方式要比直接 a.b.c.d.e.f.g.h.i 更快(翻倍)!这太神奇,也许是我的测试脚本存在bug,或者引擎有黑科技,改天重新研究下,大概会是我下一篇的文章主题。

源代码在这里,欢迎 Star/issue:

EnixCoda/safe-touchgithub.com图标