有一次專案新增一種影像類型,功能做完後,處理查詢結果的地方開始報錯。這段程式會把 API 回傳的欄位名稱,依影像類型對應成畫面要用的名稱:
const mapFields = (obj) => {
for (const field in obj) {
if (obj.hasOwnProperty(field) && mappings[obj.imagetype][field]) {
obj[mappings[obj.imagetype][field]] = obj[field]
}
}
}
修這個問題時,我把判斷改成了這樣:
if (
Object.prototype.hasOwnProperty.call(obj, field) &&
mappings[obj.imagetype] &&
mappings[obj.imagetype][field]
) {
// ...
}
這裡改了兩件事,但它們解決的是不同的問題。我一開始把兩者混在一起,後來才理清楚。
先搞懂原型鏈
JavaScript 的物件在找屬性時,自己身上沒有,就會往上一層的原型找,一路找到 Object.prototype,最後是 null。
const obj = { name: 'Elio' }
obj.name // 自己的屬性
obj.toString() // 自己沒有,來自 Object.prototype
這就是原型鏈。hasOwnProperty 也不是物件自己的方法,而是從 Object.prototype 繼承來的。
它的作用是判斷屬性是不是物件自己的,這在 for...in 裡很重要,因為 for...in 也會走訪原型鏈上可列舉的屬性。
為什麼不直接呼叫 obj.hasOwnProperty
一般情況下 obj.hasOwnProperty() 都能用,但有兩種情況會出問題。
物件沒有原型:
const obj = Object.create(null)
obj.hasOwnProperty('a') // TypeError: obj.hasOwnProperty is not a function
物件有同名欄位:
const obj = { hasOwnProperty: 'oops' }
obj.hasOwnProperty('a') // TypeError
資料來自 API 時,你無法保證欄位名稱,所以比較安全的寫法是直接用原型鏈上的方法,再用 call 指定檢查對象:
Object.prototype.hasOwnProperty.call(obj, field)
這樣就不依賴物件自己身上有什麼。ESLint 的 no-prototype-builtins 規則檢查的就是這件事。
現在也可以直接使用 Object.hasOwn:
Object.hasOwn(obj, field)
真正造成錯誤的原因
不過,這次的 TypeError 其實不是 hasOwnProperty 造成的,而是這一段:
mappings[obj.imagetype][field]
新增的影像類型沒有對應的設定,mappings[obj.imagetype] 是 undefined,再往下讀 [field] 就會報錯:
TypeError: Cannot read properties of undefined
所以修好錯誤的,是多加的這個判斷:
mappings[obj.imagetype] &&
也可以寫成可選串連:
mappings[obj.imagetype]?.[field]
Object.prototype.hasOwnProperty.call 則是順手提升穩健性。兩個改動都是好的,但如果面試被問到「為什麼會壞」,答案是前者。
跳過之外,要不要提醒
可選串連讓程式不會崩潰,但也可能把問題藏起來。新的影像類型如果忘了加設定,欄位會默默沒有對應,畫面只是少了資料。
如果這是「不應該發生」的情況,在開發環境留下警告會更好:
if (!mappings[obj.imagetype]) {
console.warn(`缺少影像類型對應:${obj.imagetype}`)
return
}
這次學到的事
hasOwnProperty來自原型鏈,直接呼叫要看物件有沒有這個方法。- 看到 TypeError 要先看是誰是
undefined,不要只看那一行最顯眼的方法。 - 新增類型時,要找出所有依賴類型對照的地方。
- 不報錯不等於沒問題,要想清楚失敗時希望看到什麼。