[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"post-\u002Fblog\u002Fhasownproperty-prototype-chain":3},{"id":4,"title":5,"body":6,"category":18,"date":613,"description":614,"draft":615,"extension":616,"meta":617,"navigation":239,"path":618,"readingTime":111,"seo":619,"stem":620,"tags":621,"__hash__":625},"blog\u002Fblog\u002Fhasownproperty-prototype-chain.md","從一個 TypeError 看 hasOwnProperty 與原型鏈",{"type":7,"value":8,"toc":606},"minimark",[9,13,127,130,193,196,200,211,263,272,287,291,298,303,346,351,387,393,414,421,428,443,446,452,461,476,484,487,499,502,511,517,521,524,527,578,581,602],[10,11,12],"p",{},"有一次專案新增一種影像類型，功能做完後，處理查詢結果的地方開始報錯。這段程式會把 API 回傳的欄位名稱，依影像類型對應成畫面要用的名稱：",[14,15,20],"pre",{"className":16,"code":17,"language":18,"meta":19,"style":19},"language-js shiki shiki-themes github-dark","const mapFields = (obj) => {\n  for (const field in obj) {\n    if (obj.hasOwnProperty(field) && mappings[obj.imagetype][field]) {\n      obj[mappings[obj.imagetype][field]] = obj[field]\n    }\n  }\n}\n","js","",[21,22,23,56,76,97,109,115,121],"code",{"__ignoreMap":19},[24,25,28,32,36,39,43,47,50,53],"span",{"class":26,"line":27},"line",1,[24,29,31],{"class":30},"snl16","const",[24,33,35],{"class":34},"svObZ"," mapFields",[24,37,38],{"class":30}," =",[24,40,42],{"class":41},"s95oV"," (",[24,44,46],{"class":45},"s9osk","obj",[24,48,49],{"class":41},") ",[24,51,52],{"class":30},"=>",[24,54,55],{"class":41}," {\n",[24,57,59,62,64,66,70,73],{"class":26,"line":58},2,[24,60,61],{"class":30},"  for",[24,63,42],{"class":41},[24,65,31],{"class":30},[24,67,69],{"class":68},"sDLfK"," field",[24,71,72],{"class":30}," in",[24,74,75],{"class":41}," obj) {\n",[24,77,79,82,85,88,91,94],{"class":26,"line":78},3,[24,80,81],{"class":30},"    if",[24,83,84],{"class":41}," (obj.",[24,86,87],{"class":34},"hasOwnProperty",[24,89,90],{"class":41},"(field) ",[24,92,93],{"class":30},"&&",[24,95,96],{"class":41}," mappings[obj.imagetype][field]) {\n",[24,98,100,103,106],{"class":26,"line":99},4,[24,101,102],{"class":41},"      obj[mappings[obj.imagetype][field]] ",[24,104,105],{"class":30},"=",[24,107,108],{"class":41}," obj[field]\n",[24,110,112],{"class":26,"line":111},5,[24,113,114],{"class":41},"    }\n",[24,116,118],{"class":26,"line":117},6,[24,119,120],{"class":41},"  }\n",[24,122,124],{"class":26,"line":123},7,[24,125,126],{"class":41},"}\n",[10,128,129],{},"修這個問題時，我把判斷改成了這樣：",[14,131,133],{"className":16,"code":132,"language":18,"meta":19,"style":19},"if (\n  Object.prototype.hasOwnProperty.call(obj, field) &&\n  mappings[obj.imagetype] &&\n  mappings[obj.imagetype][field]\n) {\n  \u002F\u002F ...\n}\n",[21,134,135,143,166,173,178,183,189],{"__ignoreMap":19},[24,136,137,140],{"class":26,"line":27},[24,138,139],{"class":30},"if",[24,141,142],{"class":41}," (\n",[24,144,145,148,151,154,157,160,163],{"class":26,"line":58},[24,146,147],{"class":68},"  Object",[24,149,150],{"class":41},".",[24,152,153],{"class":68},"prototype",[24,155,156],{"class":41},".hasOwnProperty.",[24,158,159],{"class":34},"call",[24,161,162],{"class":41},"(obj, field) ",[24,164,165],{"class":30},"&&\n",[24,167,168,171],{"class":26,"line":78},[24,169,170],{"class":41},"  mappings[obj.imagetype] ",[24,172,165],{"class":30},[24,174,175],{"class":26,"line":99},[24,176,177],{"class":41},"  mappings[obj.imagetype][field]\n",[24,179,180],{"class":26,"line":111},[24,181,182],{"class":41},") {\n",[24,184,185],{"class":26,"line":117},[24,186,188],{"class":187},"sAwPA","  \u002F\u002F ...\n",[24,190,191],{"class":26,"line":123},[24,192,126],{"class":41},[10,194,195],{},"這裡改了兩件事，但它們解決的是不同的問題。我一開始把兩者混在一起，後來才理清楚。",[197,198,199],"h2",{"id":199},"先搞懂原型鏈",[10,201,202,203,206,207,210],{},"JavaScript 的物件在找屬性時，自己身上沒有，就會往上一層的原型找，一路找到 ",[21,204,205],{},"Object.prototype","，最後是 ",[21,208,209],{},"null","。",[14,212,214],{"className":16,"code":213,"language":18,"meta":19,"style":19},"const obj = { name: 'Elio' }\n\nobj.name            \u002F\u002F 自己的屬性\nobj.toString()      \u002F\u002F 自己沒有，來自 Object.prototype\n",[21,215,216,235,241,249],{"__ignoreMap":19},[24,217,218,220,223,225,228,232],{"class":26,"line":27},[24,219,31],{"class":30},[24,221,222],{"class":68}," obj",[24,224,38],{"class":30},[24,226,227],{"class":41}," { name: ",[24,229,231],{"class":230},"sU2Wk","'Elio'",[24,233,234],{"class":41}," }\n",[24,236,237],{"class":26,"line":58},[24,238,240],{"emptyLinePlaceholder":239},true,"\n",[24,242,243,246],{"class":26,"line":78},[24,244,245],{"class":41},"obj.name            ",[24,247,248],{"class":187},"\u002F\u002F 自己的屬性\n",[24,250,251,254,257,260],{"class":26,"line":99},[24,252,253],{"class":41},"obj.",[24,255,256],{"class":34},"toString",[24,258,259],{"class":41},"()      ",[24,261,262],{"class":187},"\u002F\u002F 自己沒有，來自 Object.prototype\n",[10,264,265,266,268,269,271],{},"這就是原型鏈。",[21,267,87],{}," 也不是物件自己的方法，而是從 ",[21,270,205],{}," 繼承來的。",[10,273,274,275,279,280,283,284,286],{},"它的作用是判斷屬性是不是物件",[276,277,278],"strong",{},"自己的","，這在 ",[21,281,282],{},"for...in"," 裡很重要，因為 ",[21,285,282],{}," 也會走訪原型鏈上可列舉的屬性。",[197,288,290],{"id":289},"為什麼不直接呼叫-objhasownproperty","為什麼不直接呼叫 obj.hasOwnProperty",[10,292,293,294,297],{},"一般情況下 ",[21,295,296],{},"obj.hasOwnProperty()"," 都能用，但有兩種情況會出問題。",[10,299,300],{},[276,301,302],{},"物件沒有原型：",[14,304,306],{"className":16,"code":305,"language":18,"meta":19,"style":19},"const obj = Object.create(null)\nobj.hasOwnProperty('a') \u002F\u002F TypeError: obj.hasOwnProperty is not a function\n",[21,307,308,330],{"__ignoreMap":19},[24,309,310,312,314,316,319,322,325,327],{"class":26,"line":27},[24,311,31],{"class":30},[24,313,222],{"class":68},[24,315,38],{"class":30},[24,317,318],{"class":41}," Object.",[24,320,321],{"class":34},"create",[24,323,324],{"class":41},"(",[24,326,209],{"class":68},[24,328,329],{"class":41},")\n",[24,331,332,334,336,338,341,343],{"class":26,"line":58},[24,333,253],{"class":41},[24,335,87],{"class":34},[24,337,324],{"class":41},[24,339,340],{"class":230},"'a'",[24,342,49],{"class":41},[24,344,345],{"class":187},"\u002F\u002F TypeError: obj.hasOwnProperty is not a function\n",[10,347,348],{},[276,349,350],{},"物件有同名欄位：",[14,352,354],{"className":16,"code":353,"language":18,"meta":19,"style":19},"const obj = { hasOwnProperty: 'oops' }\nobj.hasOwnProperty('a') \u002F\u002F TypeError\n",[21,355,356,372],{"__ignoreMap":19},[24,357,358,360,362,364,367,370],{"class":26,"line":27},[24,359,31],{"class":30},[24,361,222],{"class":68},[24,363,38],{"class":30},[24,365,366],{"class":41}," { hasOwnProperty: ",[24,368,369],{"class":230},"'oops'",[24,371,234],{"class":41},[24,373,374,376,378,380,382,384],{"class":26,"line":58},[24,375,253],{"class":41},[24,377,87],{"class":34},[24,379,324],{"class":41},[24,381,340],{"class":230},[24,383,49],{"class":41},[24,385,386],{"class":187},"\u002F\u002F TypeError\n",[10,388,389,390,392],{},"資料來自 API 時，你無法保證欄位名稱，所以比較安全的寫法是直接用原型鏈上的方法，再用 ",[21,391,159],{}," 指定檢查對象：",[14,394,396],{"className":16,"code":395,"language":18,"meta":19,"style":19},"Object.prototype.hasOwnProperty.call(obj, field)\n",[21,397,398],{"__ignoreMap":19},[24,399,400,403,405,407,409,411],{"class":26,"line":27},[24,401,402],{"class":68},"Object",[24,404,150],{"class":41},[24,406,153],{"class":68},[24,408,156],{"class":41},[24,410,159],{"class":34},[24,412,413],{"class":41},"(obj, field)\n",[10,415,416,417,420],{},"這樣就不依賴物件自己身上有什麼。ESLint 的 ",[21,418,419],{},"no-prototype-builtins"," 規則檢查的就是這件事。",[10,422,423,424,427],{},"現在也可以直接使用 ",[21,425,426],{},"Object.hasOwn","：",[14,429,431],{"className":16,"code":430,"language":18,"meta":19,"style":19},"Object.hasOwn(obj, field)\n",[21,432,433],{"__ignoreMap":19},[24,434,435,438,441],{"class":26,"line":27},[24,436,437],{"class":41},"Object.",[24,439,440],{"class":34},"hasOwn",[24,442,413],{"class":41},[197,444,445],{"id":445},"真正造成錯誤的原因",[10,447,448,449,451],{},"不過，這次的 TypeError 其實不是 ",[21,450,87],{}," 造成的，而是這一段：",[14,453,455],{"className":16,"code":454,"language":18,"meta":19,"style":19},"mappings[obj.imagetype][field]\n",[21,456,457],{"__ignoreMap":19},[24,458,459],{"class":26,"line":27},[24,460,454],{"class":41},[10,462,463,464,467,468,471,472,475],{},"新增的影像類型沒有對應的設定，",[21,465,466],{},"mappings[obj.imagetype]"," 是 ",[21,469,470],{},"undefined","，再往下讀 ",[21,473,474],{},"[field]"," 就會報錯：",[14,477,482],{"className":478,"code":480,"language":481,"meta":19},[479],"language-text","TypeError: Cannot read properties of undefined\n","text",[21,483,480],{"__ignoreMap":19},[10,485,486],{},"所以修好錯誤的，是多加的這個判斷：",[14,488,490],{"className":16,"code":489,"language":18,"meta":19,"style":19},"mappings[obj.imagetype] &&\n",[21,491,492],{"__ignoreMap":19},[24,493,494,497],{"class":26,"line":27},[24,495,496],{"class":41},"mappings[obj.imagetype] ",[24,498,165],{"class":30},[10,500,501],{},"也可以寫成可選串連：",[14,503,505],{"className":16,"code":504,"language":18,"meta":19,"style":19},"mappings[obj.imagetype]?.[field]\n",[21,506,507],{"__ignoreMap":19},[24,508,509],{"class":26,"line":27},[24,510,504],{"class":41},[10,512,513,516],{},[21,514,515],{},"Object.prototype.hasOwnProperty.call"," 則是順手提升穩健性。兩個改動都是好的，但如果面試被問到「為什麼會壞」，答案是前者。",[197,518,520],{"id":519},"跳過之外要不要提醒","跳過之外，要不要提醒",[10,522,523],{},"可選串連讓程式不會崩潰，但也可能把問題藏起來。新的影像類型如果忘了加設定，欄位會默默沒有對應，畫面只是少了資料。",[10,525,526],{},"如果這是「不應該發生」的情況，在開發環境留下警告會更好：",[14,528,530],{"className":16,"code":529,"language":18,"meta":19,"style":19},"if (!mappings[obj.imagetype]) {\n  console.warn(`缺少影像類型對應：${obj.imagetype}`)\n  return\n}\n",[21,531,532,544,569,574],{"__ignoreMap":19},[24,533,534,536,538,541],{"class":26,"line":27},[24,535,139],{"class":30},[24,537,42],{"class":41},[24,539,540],{"class":30},"!",[24,542,543],{"class":41},"mappings[obj.imagetype]) {\n",[24,545,546,549,552,554,557,559,561,564,567],{"class":26,"line":58},[24,547,548],{"class":41},"  console.",[24,550,551],{"class":34},"warn",[24,553,324],{"class":41},[24,555,556],{"class":230},"`缺少影像類型對應：${",[24,558,46],{"class":41},[24,560,150],{"class":230},[24,562,563],{"class":41},"imagetype",[24,565,566],{"class":230},"}`",[24,568,329],{"class":41},[24,570,571],{"class":26,"line":78},[24,572,573],{"class":30},"  return\n",[24,575,576],{"class":26,"line":99},[24,577,126],{"class":41},[197,579,580],{"id":580},"這次學到的事",[582,583,584,590,596,599],"ul",{},[585,586,587,589],"li",{},[21,588,87],{}," 來自原型鏈，直接呼叫要看物件有沒有這個方法。",[585,591,592,593,595],{},"看到 TypeError 要先看是誰是 ",[21,594,470],{},"，不要只看那一行最顯眼的方法。",[585,597,598],{},"新增類型時，要找出所有依賴類型對照的地方。",[585,600,601],{},"不報錯不等於沒問題，要想清楚失敗時希望看到什麼。",[603,604,605],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}",{"title":19,"searchDepth":58,"depth":58,"links":607},[608,609,610,611,612],{"id":199,"depth":58,"text":199},{"id":289,"depth":58,"text":290},{"id":445,"depth":58,"text":445},{"id":519,"depth":58,"text":520},{"id":580,"depth":58,"text":580},"2025-10-15","新增資料類型後，地圖資料處理突然報錯。追查時順便釐清原型鏈、hasOwnProperty 為什麼要透過 Object.prototype 呼叫，以及真正造成錯誤的原因。",false,"md",{},"\u002Fblog\u002Fhasownproperty-prototype-chain",{"title":5,"description":614},"blog\u002Fhasownproperty-prototype-chain",[622,623,624],"JavaScript","Prototype","Debugging","F1meT6OwyQiSFa3CTi4Q5yYTyr_zRG-5KRroPqXFyH0"]