溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何處理 JavaScript 中的非預(yù)期數(shù)據(jù)

發(fā)布時間:2021-09-30 17:58:10 來源:億速云 閱讀:138 作者:柒染 欄目:開發(fā)技術(shù)

這期內(nèi)容當(dāng)中小編將會給大家?guī)碛嘘P(guān)如何處理 JavaScript 中的非預(yù)期數(shù)據(jù),文章內(nèi)容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。

  動態(tài)類型語言的最大問題就是無法保證數(shù)據(jù)流總是正確的,因為我們無法“強行控制”一個參數(shù)或變量,比方說,讓它不為 null。當(dāng)我們面對這些情況時的標(biāo)準(zhǔn)做法是簡單地做一個判斷:

  function foo (mustExist) {

  if (!mustExist) throw new Error('Parameter cannot be null')

  return ...

  }

  這樣做的問題在于會污染我們的代碼,因為要隨處做判斷,并且實際上也無法保證每一位開發(fā)代碼的人都像這樣判斷;我們甚至都不知道這樣被傳進來的一個參數(shù)是 undefined 還是 null ,這在不同團隊負(fù)責(zé)前后端的情況下司空見慣,也是大概率的情況。

  如何以更好的方式讓“非預(yù)期”數(shù)據(jù)造成的副作用最小化呢?作為一個 后端開發(fā)者 ,我想給出一些個人化的意見。

  一. 一切的源點

  數(shù)據(jù)有多種來源,最主要的當(dāng)然就是 用戶輸入 。但是,也存在其它有缺陷數(shù)據(jù)的來源,比如數(shù)據(jù)庫、函數(shù)返回值中的隱形空數(shù)據(jù)、外部 API 等。

  我們稍后將展開討論以如何不同的方式對待每一種的情況,要知道畢竟沒什么靈丹妙藥。大多數(shù)這些非預(yù)期數(shù)據(jù)的起源都是人為失誤,當(dāng)語言解析到 null 或 undefined 時,與之配套的邏輯卻沒準(zhǔn)備好處理它們。

  二. 用戶輸入

  在這種情況下,我們能做的不多,如果是用戶輸入的問題,我們通過稱為 補水(Hydration) 的方式處理它。換句話說,我們得拿到用戶發(fā)來的原始輸入,比如一個 API 中的負(fù)載,并將其轉(zhuǎn)換為我們可以無錯應(yīng)用的某些形式。

  在后端,當(dāng)使用 Express 這樣的 web 服務(wù)器時,我們可以通過標(biāo)準(zhǔn)的 JSON Schema (https://www.npmjs.com/package/ajv) 或是 Joi 這樣的工具對來自前端的用戶輸入執(zhí)行所有的操作。

  關(guān)于我們能用 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 做了校驗,默認(rèn)情況下 body 是個從 body-parser 包中通過負(fù)載接收到的對象,在本例中將其傳到一個 JSON-Schema 實例中校驗,看看其中的某個屬性是否有不同的類型或格式。

  重要:注意我們返回了一個 HTTP 422 Unprocessable Entity 狀態(tài)碼,意味著“無法處理的實體”。許多人對待像這樣 body 或者 query 錯誤的請求,使用了表示整體錯誤的 400 Bad Request報錯;在這種情況中,請求本身并沒有錯,只是用戶發(fā)送的數(shù)據(jù)不符合預(yù)期而已。

  默認(rèn)值的可選參數(shù)

  我們之前做的校驗的一個額外收獲是,我們開啟了一種可能性,那就是 如果一個可選域沒有被傳值,一個空值也能被傳遞進我們的應(yīng)用 。例如,想象一個有 page 和 size 兩個參數(shù)作為查詢字符串的分頁路由,但二者都不是必須的;如果它們都沒收到的話,必須設(shè)定一個默認(rèn)值。

  理想的話,我們的控制器里應(yīng)該有一個像這樣的函數(shù):

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

  // ...

  }

  注意:正如之前我們返回的 422 一樣,對于分頁查詢,重要的是返回恰當(dāng)?shù)臓顟B(tài)碼,無論何時對于一個只在返回值中包含了部分?jǐn)?shù)據(jù)的請求,都應(yīng)該返回 HTTP 206 Partial Content ,也就是 “不完整的內(nèi)容”;當(dāng)用戶到達最后一頁且再沒有更多數(shù)據(jù)時,才返回 200;如果用戶嘗試查詢超出了總范圍的頁數(shù),則返回一個 204 No Content 。

  這將會解決我們接受兩個 空值 的案例,但這觸碰到了在 JavaScript 中通常非常引起爭論的一點。 對于可選參數(shù)的默認(rèn)值,只假設(shè)了 當(dāng)且僅當(dāng) 其為空的情況,而為 null 時就不靈了。 所以如果我們這樣操作:

  function foo (a = 10) {

  console.log(a)

  }

  foo(undefined) // 10

  foo(20) // 20

  foo(null) // null

  因此,不能僅靠可選參數(shù)。對于這樣的情況我們有兩種處理方式:

  前端控制器中的 if 語句,雖然看著有點啰嗦:

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

  if (!page) page = 1

  if (!size) size = 10

  // ...

  }

  直接用 JSON-Schema 處理路由:

  可以再次使用 AJV 或 @expresso/validator 來校驗數(shù)據(jù):

  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)

  // ...

  })

  三. 應(yīng)對 Null 和 Undefined

  我個人對在 JavaScript 中用 null 還是 undefined 來表示空值這類爭論興趣不大。

  現(xiàn)在我們知道了每種定義,而 JavaScript 在 2020 將新增了兩個實驗性的特性(譯注:部分引自 MDN)。

  空值合并運算符 ??

  空值合并運算符 ?? 是一個邏輯運算符。當(dāng)左側(cè)操作數(shù)為 null 或 undefined 時,其返回右側(cè)的操作數(shù)。否則返回左側(cè)的操作數(shù)。

  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

  結(jié)合 空值合并運算符 ?? 使用:

  let customer = {

  name: "Carl",

  details: { age: 82 }

  };

  let customerCity = customer?.city ?? "Unknown city";

  console.log(customerCity); // Unknown city

  這兩項新增特性將讓事情簡單得多,因為我們可以把焦點集中在 null 和 undefined 上從而作出恰當(dāng)?shù)牟僮髁?用 ?? 而不是布爾值判斷 !obj 更易于處理很多錯誤情況。

  四. 隱性 null 函數(shù)

  這個暗中作祟的問題更加復(fù)雜。一些函數(shù)會假設(shè)要處理的數(shù)據(jù)都是正確填充的,但有時并不能如意:

  function foo (num) {

  return 23*num

  }

  若 num 為 null ,則函數(shù)返回值會為 0 (譯注:如果操作值之一不是數(shù)值,則被隱式調(diào)用 Number() 進行轉(zhuǎn)換),這不符合我們的期望。在這種情況下,我們能做的只有加上判斷??尚械呐袛嘈问接袃煞N,第一種可以簡單地使用 if :

  function foo (num) {

  if (!num) throw new Error('Error')

  return 23*num

  }

  第二種辦法是使用一個叫做 Either 的 Monad(譯注:Monad 是一種對函數(shù)計算過程的通用抽象機制,關(guān)鍵是統(tǒng)一形式和操作模式,相當(dāng)于是把值包裝在一個 context 中。https://zhuanlan.zhihu.com/p/65449477 )中。對于數(shù)據(jù)是不是 null 這種模棱兩可的問題,這可是個好辦法;因為 JavaScript 已經(jīng)有了一個支持雙動作流的原生的函數(shù),即 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 方法委派到調(diào)用 foo 的函數(shù)中:

  function init (n) {

  foo(n)

  .then(console.log)

  .catch(console.error)

  }

  init(12) // 276

  init(null) // Invalid value: null

  五. 外部 API 和數(shù)據(jù)庫記錄

  這也是相當(dāng)常見的情況,特別是當(dāng)系統(tǒng)是在先前創(chuàng)建和填充的數(shù)據(jù)庫之上開發(fā)的時候。例如,一個沿用之前成功產(chǎn)品數(shù)據(jù)庫的新產(chǎn)品、在不同系統(tǒng)間整合用戶等等。

  這里的大問題不在于不知道數(shù)據(jù)庫,實際上則是我們不知道在數(shù)據(jù)庫層面有什么已經(jīng)被完成了,我們沒法證明數(shù)據(jù)會不會是 null 或 undefined 。另一個問題是缺乏文檔,難以令人滿意的數(shù)據(jù)庫文檔化還是會帶來前面一個問題。

  因為返回值數(shù)據(jù)量可能較大,這樣的情況能施展的空間也不大,除了不得不對個別數(shù)據(jù)作出判斷外,在對成組的數(shù)據(jù)進行正式操作之前用 map 或 filter 進行一遍過濾是個好的做法。

  拋出 Errors

  對于數(shù)據(jù)庫和外部 API 中的服務(wù)器代碼使用 斷言函數(shù)(Assertion Functions) 也是個好的實踐,基本上這些函數(shù)的做法就是如果數(shù)據(jù)存在就返回否則報錯。這類函數(shù)的大多數(shù)常見情況,比方說有一個根據(jù)一個 id 搜索某種數(shù)據(jù)的 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

  }

  實際應(yīng)用中,應(yīng)把 Entity 替換為符合情況的名字,如 UserNotFoundError。

  該做法之所以好,是因為我們可以用這樣一個函數(shù)找到的 user,可以被另外的函數(shù)用來檢索位于其它數(shù)據(jù)庫中的相關(guān)數(shù)據(jù),比如用戶的詳細(xì)資料;而當(dāng)我們調(diào)用后一個檢索函數(shù)時,前置函數(shù) findUser 已經(jīng) 保證 了 user 的真實存在,因為如果出錯就會拋出錯誤并可以據(jù)此直接在路由邏輯中找到問題。

  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)

  }

  }

  只要檢查錯誤實例的名稱,就能得知返回了什么類型的錯誤了。


  在必要的地方單獨判斷非預(yù)期數(shù)據(jù);設(shè)置可選參數(shù)的默認(rèn)值;用 ajv 等工具對可能不完整的數(shù)據(jù)進行補水處理;恰當(dāng)使用實驗性的 空值合并運算符 ?? 和 可選鏈操作符 ?.;用 Promise 包裝隱性的空值、統(tǒng)一操作模式;用前置的 map 或 filter 過濾成組數(shù)據(jù)中的非預(yù)期數(shù)據(jù);在職責(zé)明確的控制器函數(shù)中,各自拋出類型明確的錯誤;用這些方法處理數(shù)據(jù)就能得到連續(xù)而可預(yù)測的信息流了。

上述就是小編為大家分享的如何處理 JavaScript 中的非預(yù)期數(shù)據(jù)了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關(guān)知識,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI