处理 JavaScript 中的非预期数据

更新日期: 2019-12-17阅读: 1.7k标签: 数据

动态类型语言的最大问题就是无法保证数据流总是正确的,因为我们无法“强行控制”一个参数或变量,比方说,让它不为 null。当我们面对这些情况时的标准做法是简单地做一个判断:

function foo (mustExist) {
  if (!mustExist) throw new Error('Parameter cannot be null')
  return ...
}

这样做的问题在于会污染我们的代码,因为要随处做判断,并且实际上也无法保证每一位开发代码的人都像这样判断;我们甚至都不知道这样被传进来的一个参数是 undefined 还是  null ,这在不同团队负责前后端的情况下司空见惯,也是大概率的情况。

如何以更好的方式让“非预期”数据造成的副作用最小化呢?作为一个 后端开发者 ,我想给出一些个人化的意见。


I. 一切的源点

数据有多种来源,最主要的当然就是 用户输入 。但是,也存在其它有缺陷数据的来源,比如数据库、函数返回值中的隐形空数据、外部 api 等。

我们稍后将展开讨论以如何不同的方式对待每一种的情况,要知道毕竟没什么灵丹妙药。大多数这些非预期数据的起源都是人为失误,当语言解析到 null 或 undefined 时,与之配套的逻辑却没准备好处理它们。


II. 用户输入

在这种情况下,我们能做的不多,如果是用户输入的问题,我们通过称为 补水(Hydration) 的方式处理它。换句话说,我们得拿到用户发来的原始输入,比如一个 API 中的负载,并将其转换为我们可以无错应用的某些形式。

在后端,当使用 Express 这样的 web 服务器时,我们可以通过标准的 JSON Schema (https://www.npmjs.com/package/ajv) 或是  Joi 这样的工具对来自前端的用户输入执行所有的操作。

关于我们能用 Express 和 AJV 对一个路由做什么的例子可能是下面这样:

const Ajv = require('ajv')
const Express = require('express')
const bodyParser = require('body-parser')

const app = Express()
const ajv = new Ajv()

app.use(bodyParser.json())

app.get('/foo', (req, res) => {
  const schema = {
    type: 'object',
    properties: {
      name: { type: 'string' },
      password: { type: 'string' },
      email: { type: 'string', format: 'email' }
    },
    additionalProperties: false
    required: ['name', 'password', 'email']
  }

  const valid = ajv.validate(schema, req.body)
    if (!valid) return res.status(422).json(ajv.errors)
    // ...
})

app.listen(3000)

可见我们对一个路由中请求的 body 做了校验,默认情况下 body 是个从 body-parser 包中通过负载接收到的对象,在本例中将其传到一个  JSON-Schema 实例中校验,看看其中的某个属性是否有不同的类型或格式。

重要:注意我们返回了一个 HTTP 422  Unprocessable Entity 状态码,意味着“无法处理的实体”。许多人对待像这样 body 或者 query 错误的请求,使用了表示整体错误的 400  Bad Request 报错;在这种情况中,请求本身并没有错,只是用户发送的数据不符合预期而已。


默认值的可选参数

我们之前做的校验的一个额外收获是,我们开启了一种可能性,那就是 如果一个可选域没有被传值,一个空值也能被传递进我们的应用 。例如,想象一个有  page 和  size 两个参数作为查询字符串的分页路由,但二者都不是必须的;如果它们都没收到的话,必须设定一个默认值。

理想的话,我们的控制器里应该有一个像这样的函数:

function searchSomething (filter, page = 1, size = 10) {
  // ...
}

注意:正如之前我们返回的 422 一样,对于分页查询,重要的是返回恰当的状态码,无论何时对于一个只在返回值中包含了部分数据的请求,都应该返回 HTTP 206  Partial Content ,也就是 “不完整的内容”;当用户到达最后一页且再没有更多数据时,才返回 200;如果用户尝试查询超出了总范围的页数,则返回一个 204  No Content 。

这将会解决我们接受两个 空值 的案例,但这触碰到了在 JavaScript 中通常非常引起争论的一点。 对于可选参数的默认值,只假设了   当且仅当   其为空的情况,而为   null   时就不灵了。 所以如果我们这样操作:

function foo (a = 10) {
  console.log(a)
}

foo(undefined) // 10
foo(20) // 20
foo(null) // null

因此,不能仅靠可选参数。对于这样的情况我们有两种处理方式:

  1. 前端控制器中的 if 语句,虽然看着有点啰嗦:

function searchSomething (filter, page = 1, size = 10) {
  if (!page) page = 1
  if (!size) size = 10
  // ...
}
  1. 直接用 JSON-Schema 处理路由:

可以再次使用 AJV 或 @expresso/validator 来校验数据:

app.get('/foo', (req, res) => {
  const schema = {
    type: 'object',
    properties: {
      page: { type: 'number', default: 1 },
      size: { type: 'number', default: 10 },
    },
    additionalProperties: false
  }

  const valid = ajv.validate(schema, req.params)
    if (!valid) return res.status(422).json(ajv.errors)
    // ...
})


III. 应对 Null 和 Undefined

我个人对在 JavaScript 中用 null 还是  undefined 来表示空值这类争论兴趣不大。如果你对这些概念仍有疑问,下图是个很好的比方:

现在我们知道了每种定义,而 JavaScript 在 2020 将新增了两个实验性的特性(译注:部分引自 MDN)。


空值合并运算符 ??

空值合并运算符 ?? 是一个逻辑运算符。当左侧操作数为 null 或 undefined 时,其返回右侧的操作数。否则返回左侧的操作数。

let myText = '';

let notFalsyText = myText || 'Hello world';
console.log(notFalsyText); // Hello world

let preservingFalsy = myText ?? 'Hi neighborhood';
console.log(preservingFalsy); // ''


可选链操作符 ?.

?. 运算符功能类似于  . 运算符,不同之处在于如果链条上的一个引用 null 或 undefined, . 操作符会引起一个错误,而  ?. 操作符则会按照短路计算的方式返回一个 undefined。

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah'
  }
};

const dogName = adventurer.dog?.name;
console.log(dogName);
// undefined

console.log(adventurer.someNonExistentMethod?.())
// undefined

结合 空值合并运算符 ?? 使用:

let customer = {
  name: "Carl",
  details: { age: 82 }
};
let customerCity = customer?.city ?? "Unknown city";
console.log(customerCity); // Unknown city

这两项新增特性将让事情简单得多,因为我们可以把焦点集中在 null 和  undefined 上从而作出恰当的操作了;用  ?? 而不是布尔值判断  !obj 更易于处理很多错误情况。


IV. 隐性 null 函数

这个暗中作祟的问题更加复杂。一些函数会假设要处理的数据都是正确填充的,但有时并不能如意:

function foo (num) {
  return 23*num
}

若 num 为  null ,则函数返回值会为  0 (译注:如果操作值之一不是数值,则被隐式调用 Number() 进行转换),这不符合我们的期望。在这种情况下,我们能做的只有加上判断。可行的判断形式有两种,第一种可以简单地使用  if :

function foo (num) {
  if (!num) throw new Error('Error')
  return 23*num
}

第二种办法是使用一个叫做 Either 的 Monad(译注:Monad 是一种对函数计算过程的通用抽象机制,关键是统一形式和操作模式,相当于是把值包装在一个 context 中。https://zhuanlan.zhihu.com/p/65449477 )中。对于数据是不是 null 这种模棱两可的问题,这可是个好办法;因为 JavaScript 已经有了一个支持双动作流的原生的函数,即  Promise :

function exists (value) {
  return x != null
    ? Promise.resolve(value)
    : Promise.reject(`Invalid value: ${value}`)
}

async function foo (num) {
  return exists(num).then(v => 23 * v)
}

通过这种方式就可以把来自 exists 中的  catch 方法委派到调用  foo 的函数中:

function init (n) {
  foo(n)
    .then(console.log)
    .catch(console.error)
}

init(12) // 276
init(null) // Invalid value: null


V. 外部 API 和数据库记录

这也是相当常见的情况,特别是当系统是在先前创建和填充的数据库之上开发的时候。例如,一个沿用之前成功产品数据库的新产品、在不同系统间整合用户等等。

这里的大问题不在于不知道数据库,实际上则是我们不知道在数据库层面有什么已经被完成了,我们没法证明数据会不会是 null 或  undefined 。另一个问题是缺乏文档,难以令人满意的数据库文档化还是会带来前面一个问题。

因为返回值数据量可能较大,这样的情况能施展的空间也不大,除了不得不对个别数据作出判断外,在对成组的数据进行正式操作之前用 map 或  filter 进行一遍过滤是个好的做法。


抛出 Errors

对于数据库和外部 API 中的服务器代码使用 断言函数(Assertion Functions) 也是个好的实践,基本上这些函数的做法就是如果数据存在就返回否则报错。这类函数的大多数常见情况,比方说有一个根据一个 id 搜索某种数据的 API:

async function findById (id) {
  if (!id) throw new InvalidIDError(id)

  const result = await entityRepository.findById(id)
  if (!result) throw new EntityNotFoundError(id)
  return result
}

实际应用中,应把 Entity 替换为符合情况的名字,如  UserNotFoundError

该做法之所以好,是因为我们可以用这样一个函数找到的 user,可以被另外的函数用来检索位于其它数据库中的相关数据,比如用户的详细资料;而当我们调用后一个检索函数时,前置函数 findUser 已经 保证 了 user 的真实存在,因为如果出错就会抛出错误并可以据此直接在路由逻辑中找到问题。

async function findUserProfiles (userId) {
  const user = await findUser(userId)

  const profile = await profileRepository.findById(user.profileId)
  if (!profile) throw new ProfileNotFoundError(user.profileId)
  return profile
}

路由逻辑会像这样:

app.get('/users/{id}/profiles', handler)

// --- //

async function handler (req, res) {
  try {
    const userId = req.params.id
    const profile = await userService.findUserProfiles(userId)
    return res.status(200).json(profile)
  } catch (e) {
    if (e instanceof UserNotFoundError
        || e instanceof ProfileNotFoundError)
            return res.status(404).json(e.message)
    if (e instanceof InvalidIDError)
        return res.status(400).json(e.message)
  }
}

只要检查错误实例的名称,就能得知返回了什么类型的错误了。


VI. 总结

  • 在必要的地方单独判断非预期数据

  • 设置可选参数的默认值

  • 用 ajv 等工具对可能不完整的数据进行补水处理

  • 恰当使用实验性的 空值合并运算符 ?? 和 可选链操作符  ?.

  • 用 Promise 包装隐性的空值、统一操作模式

  • 用前置的 map 或 filter 过滤成组数据中的非预期数据

  • 在职责明确的控制器函数中,各自抛出类型明确的错误

用这些方法处理数据就能得到连续而可预测的信息流了。

原文:https://dev.to/khaosdoctor/dealing-with-unexpected-data-in-javascript-2kda  

链接: https://www.fly63.com/article/detial/7468

双向数据绑定与单向数据绑定的各自优势和关系

在react中是单向数据绑定,而在vue和augular中的特色是双向数据绑定。为什么会选择两种不同的机制呢?我猜测是两种不同的机制有不同的适应场景,查了一些资料后,总结一下。

原生JS数据绑定的实现

双向数据绑定是非常重要的特性 —— 将JS模型与HTML视图对应,能减少模板编译时间同时提高用户体验。我们将学习在不使用框架的情况下,使用原生JS实现双向绑定 —— 一种为Object.observe

JavaScript判断数据类型的多种方法【 js判断一个变量的类型】

js判断数据类型的多种方法,主要包括:typeof、instanceof、 constructor、 prototype.toString.call()等,下面就逐一介绍它们的异同。

javascript中的typeof返回的数据类型_以及强制/隐式类型转换

由于js为弱类型语言拥有动态类型,这意味着相同的变量可用作不同的类型。 typeof 运算符返回一个用来表示表达式的数据类型的字符串,目前typeof返回的字符串有以下这些: undefined、boolean、string、number、object、function、“symbol

使用typeof obj===‘object’潜在的问题,并不能确定obj是否是一个对象?

在js中我们直接这样写typeof obj===‘object’有什么问题呢?发现Array, Object,null都被认为是一个对象了。如何解决这种情况,能保证判断obj是否为一个对象

js进制数之间以及和字符之间的转换

js要处理十六进制,十进制,字符之间的转换,发现有很多差不多且书写不正确的方法.一个一个实践才真正清楚如何转换,现在来记录一下它们之间转换的方法。

js判断数字是奇数还是偶数的2种方法实现

奇数和偶数的判断是数学运算中经常碰到的问题,这篇文章主要讲解通过JavaScript来实现奇偶数的判断。2种判断方法:求余% 、&1

js算法_判断数字是否为素数/质数

质数又称素数。指在一个大于1的自然数中,除了1和此整数自身外,没法被其他自然数整除的数。比如100以内共25个,js实现代码如下。

Js数据类型转换_JavaScript 那些不经意间发生的数据类型自动转换

JavaScript自动类型转换真的非常常见,常用的一些便捷的转类型的方式,都是依靠自动转换产生的。比如 转数字 : + x 、 x - 0 , 转字符串 : \\\"\\\" + x 等等。现在总算知道为什么可以这样便捷转换。

Js中实现XML和String相互转化

XML是标准通用标记语言 (SGML) 的子集,非常适合 Web 传输。XML 提供统一的方法来描述和交换独立于应用程序或供应商的结构化数据。 这篇文章主要介绍Js中实现XML和String相互转化

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!