你不知道的 Node.js Util

Shenfq發表於2021-11-16

從型別判斷說起

在 JavaScript 中,進行變數的型別校驗是一個非常令人頭疼的事,如果只是簡單的使用 typeof 會到各種各樣的問題。

舉幾個簡單的?:

console.log(typeof null) // 'object'
console.log(typeof new Array) // 'object'
console.log(typeof new String) // 'object'

後來,大家發現可以使用 Object.prototype.toString() 方法來進行變數型別的判斷。

const getTypeString = obj => Object.prototype.toString.call(obj)

getTypeString(null) // '[object Null]'
getTypeString('string') //'[object String]'
getTypeString(new String) //'[object String]'

toString() 方法進行代理,可以得到一個型別字串,我們就可以在這個字串上面搞事情。

const getTypeString = obj => {
  return Object.prototype.toString.call(obj)
}
const isType = type => {
  return obj => {
    return getTypeString(obj) === `[object ${type}]`
  }
}

const isArray = isType('Array') // 該方法一般通過 Array.isArray 代替

const isNull = isType('Null')
const isObject = isType('Object')
const isRegExp = isType('RegExp')
const isFunction = isType('Function')
const isAsyncFunction = isType('AsyncFunction')
isNull(null) // true
isObject({}) // true
isRegExp(/\w/) // true
isFunction(() => {}) // true
isAsyncFunction(async () => {}) // true

But,在 Node.js 中,內部其實是有一組用來判斷變數型別的 api 的。而且功能異常豐富,除了基礎型別的判斷,還支援判斷 Promise 物件、Date 物件、各種ArrayBuffer。

const types = require('util/types')

types.isDate(new Date) // true
types.isPromise(new Promise(() => {})) // true
types.isArrayBuffer(new ArrayBuffer(16)) // true

嚴格相等

在 JavaScript 中,物件、陣列等變數在判斷相等的過程中,如果用 === 通常只會判斷這兩個變數是否指向同一記憶體地址。如果想判斷物件的鍵對應的所有值是否相等,需要對兩個物件進行遍歷。在 util 中,也提供了一個方法可以用來判斷兩個物件是否嚴格相等:util.isDeepStrictEqual(val1, val2)

const util = require('util')

const val1 = { name: 'shenfq' }
const val2 = { name: 'shenfq' }

console.log('val1 === val2', val1 === val2) // false
console.log('isDeepStrictEqual', util.isDeepStrictEqual(val1, val2)) // true

該方法同樣可以用來判斷陣列,是否嚴格相等:

const util = require('util')

const arr1 = [1, 3, 5]
const arr2 = [1, 3, 5]

console.log('arr1 === arr2', arr1 === arr2) // false
console.log('isDeepStrictEqual', util.isDeepStrictEqual(arr1, arr2)) // true

Error First & Promise

早期的 Node API 都是 Error First 風格的,也就是所有的非同步函式都會接受一個回撥函式,該回撥的一個引數為 error 物件,如果正常返回 error 物件為 null,後面的引數為成功響應的結果。

// 下面是一個讀取檔案的示例
const fs = require('fs')
fs.readFile('nginx.log', (error, data) => {
  if (error) {
    // 讀取檔案失敗
    console.error(error)
    return
  }
  // 讀取檔案成功,列印結果
  console.log(data)
})

在 Node 8 釋出的時候,新增了一個 promisify 介面,用於將 Error First 風格的 API 轉為 Promise API。

const fs = require('fs')
const util = require('util')

const readFile = util.promisify(fs.readFile)
readFile('./2021-11-11.log', { encoding: 'utf-8' })
  .then(text => console.log(text)) 
    .catch(error => console.error(error))

不過,後來也有很多人覺得這些原生 API 支援 Promise 的方式太過繁瑣,每個 API 都需要單獨的包裝一層 promisify 方法。在 Node 10 釋出的時候,原生模組都新增了一個 .promises 屬性,該屬性下的所有 API 都 Promise 風格的。

const fs = require('fs').promises
fs.readFile('./2021-11-11.log', { encoding: 'utf-8' })
  .then(text => console.log(text)) 
    .catch(error => console.error(error))

注意:Node 14 後,promises API 又新增了一種引入方式,通過修改包名的方式引入。

const fs = require('fs/promises')
fs.readFile('./2021-11-11.log', { encoding: 'utf-8' })
  .then(text => console.log(text)) 
    .catch(error => console.error(error))

除了將 Error First 風格的 API 轉為 Promise API,util 中還提供 callbackify 方法,用於將 async 函式轉換為 Error First 風格的函式。

下面通過 callbackify 將 promise 化的 fs 還原為 Error First 風格的函式。

const fs = require('fs/promises')
const util = require('util')

const readFile = util.callbackify(fs.readFile)
readFile('./2021-11-12.log', { encoding: 'utf-8' }, (error, text) => {
  if (error) {
    console.error(error)
    return
  }
  console.log(text)
})

除錯與輸出

如果有開發過 Node 服務,應該都用過 debug 模組,通過該模組可以在控制檯看到更加明晰的除錯資訊。

const debug = require('debug')
const log = debug('app')

const user = { name: 'shenfq' }

log('當前使用者: %o', user)

其實,通過 util.debug 也能實現類似的效果:

const debug = require('debug')
const log = debug('app')

const user = { name: 'shenfq' }

log('當前使用者: %o', user)

只是在啟動時,需要將 DEBUG 環境變數替換為 NODE_DEBUG

如果你有認真看上面的程式碼,應該會發現,在 log('當前使用者: %o', user) 方法前面的字串中,有一個 %o 佔位符,表示這個地方將會填充一個物件(object)。這與 C 語言或 python 中的,printf 類似。同樣,在 util 模組中,直接提供了格式化的方法:util.format

const { format } = require('util')

console.log(
  format('當前使用者: %o', {
    name: 'shenfq', age: 25
  })
)

除了 %o 佔位符,不同的資料型別應使用不同的佔位符。

佔位符型別
%s字串
%d數字(包括整數和浮點數)
%i整數
%f浮點數
%jJSON
%oObject

JavaScript 中的物件是一個很複雜的東西,除了直接使用 util.format 外加 %o 佔位符的方式格式化物件,util 中還提供了一個叫做 inspect 方法來進行物件格式化。

const { inspect } = require('util')

const user = {
  age: 25,
  name: 'shenfq',
  work: {
    name: 'coding',
    seniority: 5
  }
}

console.log(inspect(user))

這麼看 inspect 好像什麼都沒做,但是 inspect 方法還有第二個引數,用來進行格式化時的一些個性化配置。

  • depth: number:控制顯示層級;
  • sorted: boolean|Function: 是否按照key的編碼值進行排序;
  • compact: boolean:是否進行單行顯示;

當然上面只是一部分配置,更詳細的配置可查閱 node 文件,下面我們寫幾個案例:

所有的屬性都換行顯示:

inspect(user, {
    compact: false
})

只格式化物件第一層的值:

inspect(user, {
  depth: 0,
    compact: false
})

按照key值的編碼倒序輸出:

inspect(user, {
    compact: false,
  sorted: (a, b) => a < b ? 1 : -1
})

相關文章