จะหลีกเลี่ยงข้อผิดพลาด 'can't read property of undefined' ได้อย่างไร?


119

ในรหัสของฉันฉันจัดการกับอาร์เรย์ที่มีบางรายการที่มีวัตถุจำนวนมากซ้อนอยู่ภายในซึ่งกันและกันโดยที่บางรายการไม่ทำ มีลักษณะดังนี้:

// where this array is hundreds of entries long, with a mix
// of the two examples given
var test = [{'a':{'b':{'c':"foo"}}}, {'a': "bar"}];

สิ่งนี้ทำให้ฉันมีปัญหาเพราะฉันต้องวนซ้ำผ่านอาร์เรย์ในบางครั้งและความไม่สอดคล้องกันทำให้ฉันเกิดข้อผิดพลาดดังนี้:

for (i=0; i<test.length; i++) {
    // ok on i==0, but 'cannot read property of undefined' on i==1
    console.log(a.b.c);
}

ฉันรู้ว่าฉันพูดif(a.b){ console.log(a.b.c)}ได้ แต่นี่เป็นเรื่องที่น่าเบื่อเป็นพิเศษในกรณีที่มีวัตถุมากถึง 5 หรือ 6 ชิ้นซ้อนกัน มีวิธีอื่น (ง่ายกว่า) ที่ฉันสามารถทำได้เฉพาะ console.log ถ้ามีอยู่ แต่ไม่มีข้อผิดพลาด?


3
ข้อผิดพลาดนี้อาจเป็นข้อยกเว้นของ Javascript ปกติดังนั้นลองใช้try..catchคำสั่ง ที่กล่าวว่าอาร์เรย์ที่มีองค์ประกอบที่แตกต่างกันอย่างมากดูเหมือนจะเป็นปัญหาในการออกแบบสำหรับฉัน
มิลลิโมส

3
หากโครงสร้างของคุณไม่สอดคล้องกันในแต่ละรายการจะมีอะไรผิดพลาดในการตรวจสอบการมีอยู่? if ("b" in a && "c" in a.b)จริงๆผมต้องการใช้ มันอาจจะ "น่าเบื่อ" แต่นั่นคือสิ่งที่คุณจะได้รับจากความไม่สอดคล้องกัน ... ตรรกะปกติ
Ian

2
ทำไมคุณถึงเข้าถึงคุณสมบัติที่ไม่มีอยู่ทำไมคุณไม่รู้ว่าวัตถุมีลักษณะอย่างไร
Bergi

9
ฉันเข้าใจว่าทำไมใครบางคนไม่ต้องการให้ข้อผิดพลาดทำให้ทุกอย่างพัง คุณไม่สามารถพึ่งพาคุณสมบัติของออบเจ็กต์ได้ตลอดเวลาว่ามีอยู่หรือไม่มีอยู่จริง หากคุณมีบางอย่างที่สามารถจัดการกับเหตุการณ์ที่วัตถุมีรูปแบบไม่ถูกต้องแสดงว่าโค้ดของคุณมีประสิทธิภาพมากกว่าและเปราะบางน้อยลง
SSH

3
คุณจะแปลกใจที่จำนวนวัตถุ / อาร์เรย์ผิดรูปแบบในสถานการณ์จริง
OneMoreQuestion

คำตอบ:


122

อัปเดต :

  • หากคุณใช้ JavaScript ตาม ECMAScript 2020 หรือใหม่กว่าโปรดดูการเชื่อมโยงเพิ่มเติมเพิ่มเติม
  • typescript ได้เพิ่มการสนับสนุนสำหรับการผูกมัดตัวเลือกในรุ่น3.7
// use it like this
obj?.a?.lot?.of?.properties

โซลูชันสำหรับ JavaScript ก่อน ECMASCript 2020 หรือ TypeScript ที่เก่ากว่าเวอร์ชัน 3.7 :

วิธีแก้ปัญหาอย่างรวดเร็วคือการใช้ฟังก์ชัน try / catch helper กับฟังก์ชันลูกศร ES6 :

function getSafe(fn, defaultVal) {
    try {
        return fn();
    } catch (e) {
        return defaultVal;
    }
}

// use it like this
getSafe(() => obj.a.lot.of.properties);

// or add an optional default value
getSafe(() => obj.a.lot.of.properties, 'nothing');

ตัวอย่างข้อมูลการทำงาน:

ดูรายละเอียดในบทความนี้


2
ฉันรักมัน! สิ่งเดียวที่ฉันจะเพิ่มคือคอนโซลเตือนภายในการจับเพื่อให้คุณทราบถึงข้อผิดพลาด แต่ยังคงดำเนินต่อไป
Rabbi Shuki Gur

การจับข้อยกเว้นทั้งหมดโดยไม่โยนซ้ำถือเป็นเรื่องไม่ดีและโดยทั่วไปการใช้ข้อยกเว้นเป็นส่วนหนึ่งของขั้นตอนการดำเนินการที่คาดหวังก็ไม่ดีเช่นกันแม้ว่าในกรณีนี้จะมีอยู่ค่อนข้างดี
hugo

50

สิ่งที่คุณกำลังทำอยู่ทำให้เกิดข้อยกเว้น (และถูกต้องเช่นนั้น)

คุณสามารถทำได้เสมอ

try{
   window.a.b.c
}catch(e){
   console.log("YO",e)
}

แต่ฉันจะไม่คิดแทนการใช้งานของคุณ

ทำไมคุณถึงเข้าถึงข้อมูล 6 ระดับซ้อนกันที่คุณไม่คุ้นเคย? กรณีการใช้งานใดที่แสดงให้เห็นถึงสิ่งนี้?

โดยปกติแล้วคุณต้องการตรวจสอบความถูกต้องว่าคุณกำลังจัดการกับวัตถุประเภทใด

นอกจากนี้โปรดทราบว่าคุณไม่ควรใช้คำสั่งเช่นif(a.b)เพราะจะส่งคืนเท็จถ้า ab เป็น 0 หรือแม้ว่าจะเป็น "0" แทนที่จะตรวจสอบว่าa.b !== undefined


1
เกี่ยวกับการแก้ไขครั้งแรกของคุณ: มันเป็นธรรม; ฉันกำลังจัดการกับรายการฐานข้อมูลที่มีโครงสร้าง JSON ดังนั้นวัตถุจะปรับขนาดเขตข้อมูลหลายระดับ (เช่น entries.users.messages.date เป็นต้นซึ่งไม่ใช่ทุกกรณีที่ป้อนข้อมูล)
อาริ

"มันจะกลับมาเป็นจริงถ้า ab เป็น 0" - ไม่ typeof a.b === "undefined" && a.b!=null- ไม่จำเป็นต้องทำส่วนที่สองหลังจากส่วนแรกและมันสมเหตุสมผลกว่าที่จะทำif ("b" in a)
เอียน

@ ฉันเห็นได้ชัดว่าฉันหมายถึงมันในทางกลับกันมันจะส่งกลับเท็จแม้ว่า ab จะเป็น "0" ก็ตาม Nice Catch
Benjamin Gruenbaum

@BenjaminGruenbaum ฟังดูดีไม่แน่ใจว่าคุณหมายถึงอย่างนั้นหรือเปล่า นอกจากนี้ฉันคิดว่าคุณต้องการtypeof a.b !== "undefined" && ab! = null` - โปรดสังเกต!==
Ian

3
หากคุณไม่ต้องการเบื่อหน่าย ab && abc && console.log (abc) นี่เป็นวิธีเดียวที่จะบันทึกสิ่งที่ไม่รู้จักอย่างสม่ำเสมอ
Brian Cray

14

ถ้าฉันเข้าใจคำถามของคุณอย่างถูกต้องคุณต้องการวิธีที่ปลอดภัยที่สุดในการตรวจสอบว่าวัตถุมีคุณสมบัติหรือไม่

วิธีที่ง่ายที่สุดคือการใช้ผู้ประกอบการin

window.a = "aString";
//window should have 'a' property
//lets test if it exists
if ("a" in window){
    //true
 }

if ("b" in window){
     //false
 }

แน่นอนว่าคุณสามารถทำรังได้ลึกเท่าที่คุณต้องการ

if ("a" in window.b.c) { }

ไม่แน่ใจว่าจะช่วยได้หรือไม่


9
คุณไม่สามารถทำรังได้อย่างปลอดภัยเท่าที่คุณต้องการ เกิดอะไรขึ้นถ้าwindow.bไม่ได้กำหนด? คุณจะได้รับข้อผิดพลาดประเภท:Cannot use 'in' operator to search for 'c' in undefined
Trevor

13

หากคุณใช้lodashคุณสามารถใช้ฟังก์ชัน "has" ได้ คล้ายกับ "in" แต่อนุญาตให้ใช้เส้นทางได้

var testObject = {a: {b: {c: 'walrus'}}};
if(_.has(testObject, 'a.b.c')) {
  //Safely access your walrus here
}

2
ดีที่สุดเราสามารถใช้เป็น_.get()ค่าเริ่มต้นเพื่อให้อ่านง่าย:_.get(object, 'a.b.c', 'default');
ไม่ใช่

12

ลองทำตามนี้ หากa.bไม่ได้กำหนดจะออกจากifคำสั่งโดยไม่มีข้อยกเว้นใด ๆ

if (a.b && a.b.c) {
  console.log(a.b.c);
}

5

นี่เป็นปัญหาทั่วไปเมื่อทำงานกับวัตถุ json ที่ลึกหรือซับซ้อนดังนั้นฉันจึงพยายามหลีกเลี่ยงการลอง / จับหรือฝังการตรวจสอบหลาย ๆ ครั้งซึ่งจะทำให้โค้ดไม่สามารถอ่านได้โดยปกติฉันจะใช้โค้ดชิ้นเล็ก ๆ นี้ในกระบวนการทั้งหมดของฉันเพื่อทำงาน

/* ex: getProperty(myObj,'aze.xyz',0) // return myObj.aze.xyz safely
 * accepts array for property names: 
 *     getProperty(myObj,['aze','xyz'],{value: null}) 
 */
function getProperty(obj, props, defaultValue) {
    var res, isvoid = function(x){return typeof x === "undefined" || x === null;}
    if(!isvoid(obj)){
        if(isvoid(props)) props = [];
        if(typeof props  === "string") props = props.trim().split(".");
        if(props.constructor === Array){
            res = props.length>1 ? getProperty(obj[props.shift()],props,defaultValue) : obj[props[0]];
        }
    }
    return typeof res === "undefined" ? defaultValue: res;
}


4

ฉันใช้undefsafe อย่างเคร่งครัด โดยจะทดสอบแต่ละระดับลงไปในออบเจ็กต์ของคุณจนกว่าจะได้รับค่าที่คุณขอหรือส่งกลับ "ไม่ได้กำหนด" แต่ไม่เคยมีข้อผิดพลาด


2
ที่คล้ายกับ lodash_.get
Filype

ร้องดี! ยังคงมีประโยชน์หากคุณไม่ต้องการคุณสมบัติอื่น ๆ ของ lodash
martinedwards

4

ถ้าคุณใช้ Babel คุณแล้วสามารถใช้ไวยากรณ์การผูกมัดตัวเลือกที่มี@ Babel / ปลั๊กอินข้อเสนอ-จำเป็นต้องผูกมัดปลั๊กอินบาเบล สิ่งนี้จะช่วยให้คุณสามารถแทนที่สิ่งนี้:

console.log(a && a.b && a.b.c);

ด้วยสิ่งนี้:

console.log(a?.b?.c);

3

ฉันชอบคำตอบของ Cao Shouguang แต่ฉันไม่ชอบที่จะส่งฟังก์ชันเป็นพารามิเตอร์ไปยังฟังก์ชัน getSafe ทุกครั้งที่โทร ฉันได้แก้ไขฟังก์ชัน getSafe เพื่อยอมรับพารามิเตอร์อย่างง่ายและ ES5 บริสุทธิ์

/**
* Safely get object properties.    
* @param {*} prop The property of the object to retrieve
* @param {*} defaultVal The value returned if the property value does not exist
* @returns If property of object exists it is returned, 
*          else the default value is returned.
* @example
* var myObj = {a : {b : 'c'} };
* var value;
* 
* value = getSafe(myObj.a.b,'No Value'); //returns c 
* value = getSafe(myObj.a.x,'No Value'); //returns 'No Value'
* 
* if (getSafe(myObj.a.x, false)){ 
*   console.log('Found')
* } else {
*  console.log('Not Found') 
* }; //logs 'Not Found'
* 
* if(value = getSafe(myObj.a.b, false)){
*  console.log('New Value is', value); //logs 'New Value is c'
* }
*/
function getSafe(prop, defaultVal) {
  return function(fn, defaultVal) {
    try {
      if (fn() === undefined) {
        return defaultVal;
      } else {
        return fn();
      }
    } catch (e) {
      return defaultVal;
    }
  }(function() {return prop}, defaultVal);
}

getSafe(myObj.x.c)มันไม่ได้จริงๆทำงานกับ ลองใช้ Chrome และ Firefox เวอร์ชันล่าสุด
Rickard Elimää

2

ในคำตอบของ str ค่า "ไม่ได้กำหนด" จะถูกส่งกลับแทนค่าเริ่มต้นที่ตั้งไว้หากไม่ได้กำหนดคุณสมบัติ บางครั้งอาจทำให้เกิดข้อบกพร่อง สิ่งต่อไปนี้จะทำให้แน่ใจว่าค่า defaultVal จะถูกส่งกลับเสมอเมื่อไม่ได้กำหนดคุณสมบัติหรือวัตถุ

const temp = {};
console.log(getSafe(()=>temp.prop, '0'));

function getSafe(fn, defaultVal) {
    try {
        if (fn() === undefined) {
            return defaultVal
        } else {
            return fn();
        }

    } catch (e) {
        return defaultVal;
    }
}

รหัสของฉันเวอร์ชันปรับปรุงของ Hardy Le Roux ใช้ไม่ได้กับให้ myObj = {} getSafe (() => myObj.ab, "nice") ในขณะที่ฉันทำงาน ใครอธิบายว่าทำไม?
Cao Shouguang

2

Lodash มีgetวิธีการที่อนุญาตให้ค่าเริ่มต้นเป็นพารามิเตอร์ที่สามซึ่งเป็นทางเลือกดังที่แสดงด้านล่าง:

const myObject = {
  has: 'some',
  missing: {
    vars: true
  }
}
const path = 'missing.const.value';
const myValue = _.get(myObject, path, 'default');
console.log(myValue) // prints out default, which is specified above
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"></script>


2

ลองนึกภาพว่าเราต้องการใช้ชุดของฟังก์ชันกับxif และเฉพาะในกรณีที่xไม่ใช่ค่าว่าง:

if (x !== null) x = a(x);
if (x !== null) x = b(x);
if (x !== null) x = c(x);

ตอนนี้สมมติว่าเราต้องทำเช่นเดียวกันกับy:

if (y !== null) y = a(y);
if (y !== null) y = b(y);
if (y !== null) y = c(y);

และเช่นเดียวกับz:

if (z !== null) z = a(z);
if (z !== null) z = b(z);
if (z !== null) z = c(z);

อย่างที่คุณเห็นโดยไม่มีนามธรรมที่เหมาะสมเราจะต้องทำซ้ำโค้ดซ้ำแล้วซ้ำเล่า สิ่งที่เป็นนามธรรมดังกล่าวมีอยู่แล้ว: อาจจะเป็นโมนาด

บางที monad ถือทั้งมูลค่าและบริบทการคำนวณ:

  1. monad รักษามูลค่าให้ปลอดภัยและใช้ฟังก์ชันต่างๆ
  2. บริบทการคำนวณคือการตรวจสอบค่าว่างก่อนใช้ฟังก์ชัน

การใช้งานที่ไร้เดียงสาจะมีลักษณะดังนี้:

⚠️การใช้งานนี้มีวัตถุประสงค์เพื่อเป็นภาพประกอบเท่านั้น! นี่ไม่ใช่วิธีที่ควรทำและผิดในหลายระดับ อย่างไรก็ตามสิ่งนี้จะช่วยให้คุณเข้าใจได้ดีขึ้นว่าฉันกำลังพูดถึงอะไร

อย่างที่คุณเห็นว่าไม่มีอะไรสามารถทำลายได้:

  1. เราใช้ชุดฟังก์ชันเพื่อคุณค่าของเรา
  2. หาก ณ จุดใดค่าจะกลายเป็นโมฆะ (หรือไม่ได้กำหนด) เราก็ไม่ใช้ฟังก์ชันใด ๆ อีกต่อไป

const abc = obj =>
  Maybe
    .of(obj)
    .map(o => o.a)
    .map(o => o.b)
    .map(o => o.c)
    .value;

const values = [
  {},
  {a: {}},
  {a: {b: {}}},
  {a: {b: {c: 42}}}
];

console.log(

  values.map(abc)

);
<script>
function Maybe(x) {
  this.value = x; //-> container for our value
}

Maybe.of = x => new Maybe(x);

Maybe.prototype.map = function (fn) {
  if (this.value == null) { //-> computational context
    return this;
  }
  return Maybe.of(fn(this.value));
};
</script>


ภาคผนวก 1

ฉันไม่สามารถอธิบายได้ว่า monads คืออะไรเนื่องจากนี่ไม่ใช่จุดประสงค์ของโพสต์นี้และมีคนที่ดีกว่าฉัน อย่างไรก็ตามดังที่ Eric Elliot กล่าวไว้ในโพสต์บล็อกประวัติ JavaScript Monads Made Simple :

ไม่ว่าคุณจะมีทักษะในระดับใดหรือเข้าใจทฤษฎีหมวดหมู่การใช้ monads ทำให้โค้ดของคุณทำงานได้ง่ายขึ้น การไม่ใช้ประโยชน์จาก monads อาจทำให้โค้ดของคุณทำงานได้ยากขึ้น (เช่น callback hell, สาขาตามเงื่อนไขที่ซ้อนกัน, ความฟุ่มเฟื่อยมากขึ้น)


ภาคผนวก 2

นี่คือวิธีที่ฉันจะแก้ปัญหาของคุณโดยใช้ไฟล์ Maybe monad from

const prop = key => obj => Maybe.fromNull(obj[key]);

const abc = obj =>
  Maybe
    .fromNull(obj)
    .flatMap(prop('a'))
    .flatMap(prop('b'))
    .flatMap(prop('c'))
    .orSome('🌯')
    
const values = [
  {},
  {a: {}},
  {a: {b: {}}},
  {a: {b: {c: 42}}}
];

console.log(

  values.map(abc)

);
<script src="https://www.unpkg.com/monet@0.9.0/dist/monet.js"></script>
<script>const {Maybe} = Monet;</script>


0

ฉันตอบคำถามนี้ก่อนหน้านี้และเกิดขึ้นในการตรวจสอบที่คล้ายกันในวันนี้ การทำให้เข้าใจง่ายเพื่อตรวจสอบว่ามีคุณสมบัติจุดซ้อนอยู่หรือไม่ คุณสามารถแก้ไขค่านี้เพื่อคืนค่าหรือค่าเริ่มต้นบางอย่างเพื่อบรรลุเป้าหมายของคุณ

function containsProperty(instance, propertyName) {
    // make an array of properties to walk through because propertyName can be nested
    // ex "test.test2.test.test"
    let walkArr = propertyName.indexOf('.') > 0 ? propertyName.split('.') : [propertyName];

    // walk the tree - if any property does not exist then return false
    for (let treeDepth = 0, maxDepth = walkArr.length; treeDepth < maxDepth; treeDepth++) {

        // property does not exist
        if (!Object.prototype.hasOwnProperty.call(instance, walkArr[treeDepth])) {
            return false;
        }

        // does it exist - reassign the leaf
        instance = instance[walkArr[treeDepth]];

    }

    // default
    return true;

}

ในคำถามของคุณคุณสามารถทำสิ่งต่างๆเช่น:

let test = [{'a':{'b':{'c':"foo"}}}, {'a': "bar"}];
containsProperty(test[0], 'a.b.c');


0

คุณสามารถหลีกเลี่ยงการรับข้อผิดพลาดได้โดยให้ค่าเริ่มต้นก่อนรับคุณสมบัติ

var test = [{'a':{'b':{'c':"foo"}}}, {'a': "bar"}];

for (i=0; i<test.length; i++) {
    const obj = test[i]
    // No error, just undefined, which is ok
    console.log(((obj.a || {}).b || {}).c);
}

ใช้งานได้ดีกับอาร์เรย์เช่นกัน:

const entries = [{id: 1, name: 'Scarllet'}]
// Giving a default name when is empty
const name = (entries.find(v => v.id === 100) || []).name || 'no-name'
console.log(name)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.