Jak uniknąć błędów „nie można odczytać właściwości undefined”?


118

W moim kodzie mam do czynienia z tablicą, która ma kilka wpisów z wieloma obiektami zagnieżdżonymi w sobie, podczas gdy niektóre nie. Wygląda mniej więcej tak:

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

To sprawia mi problemy, ponieważ czasami muszę iterować po tablicy, a niespójność generuje mi błędy, takie jak:

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);
}

Zdaję sobie sprawę, że mogę powiedzieć if(a.b){ console.log(a.b.c)}, ale jest to niezwykle uciążliwe w przypadkach, gdy jest do 5 lub 6 obiektów zagnieżdżonych jeden w drugim. Czy jest jakiś inny (łatwiejszy) sposób, w jaki mogę to zrobić TYLKO w console.log, jeśli istnieje, ale bez zgłaszania błędu?


3
Błąd jest prawdopodobnie zwykłym wyjątkiem JavaScript, więc wypróbuj try..catchinstrukcję. To powiedziawszy, tablica zawierająca szalenie heterogeniczne elementy wygląda dla mnie na problem projektowy.
millimoose

3
Jeśli twoja struktura nie jest spójna we wszystkich elementach, to co jest złego w sprawdzaniu istnienia? Naprawdę bym użył if ("b" in a && "c" in a.b). Może to być „żmudne”, ale to właśnie dostajesz za niespójność… normalną logikę.
Ian

2
Dlaczego miałbyś mieć dostęp do nieistniejących właściwości, dlaczego nie wiesz, jak wyglądają obiekty?
Bergi

9
Rozumiem, dlaczego ktoś nie chciałby, aby błąd powodował awarię wszystkiego. Nie można zawsze polegać na właściwościach obiektu, które istnieją lub nie istnieją. Jeśli masz coś, co może obsłużyć zdarzenie, w którym obiekt jest zniekształcony, Twój kod jest znacznie wydajniejszy i mniej kruchy.
SSH To

3
Zdziwiłbyś się, ile obiektów / tablic jest zniekształconych w rzeczywistych sytuacjach
OneMoreQuestion

Odpowiedzi:


122

Aktualizacja :

  • Jeśli używasz JavaScript zgodnie z ECMAScript 2020 lub nowszym, zobacz opcjonalne tworzenie łańcuchów .
  • W języku TypeScript dodano obsługę opcjonalnego tworzenia łańcuchów w wersji 3.7 .
// use it like this
obj?.a?.lot?.of?.properties

Rozwiązanie dla JavaScript przed ECMASCript 2020 lub TypeScript starsze niż wersja 3.7 :

Szybkim obejściem jest użycie funkcji pomocniczej try / catch z funkcją strzałki 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');

Działający fragment:

Więcej informacji znajdziesz w tym artykule .


2
Kocham to! jedyne co chciałbym dodać to console.warn wewnątrz haczyka, żebyś wiedział o błędzie, ale trwa dalej.
Rabin Shuki Gur,

Wyłapywanie wszystkich wyjątków bez ponownego rzucania jest złe i generalnie używanie wyjątków jako części oczekiwanego przepływu wykonywania również nie jest świetne - mimo że w tym przypadku jest dość dobrze zawarte.
hugo

50

To, co robisz, rodzi wyjątek (i słusznie).

Zawsze możesz to zrobić

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

Ale nie zrobiłbym tego, zamiast tego pomyśleć o twoim przypadku użycia.

Dlaczego uzyskujesz dostęp do danych, 6 poziomów zagnieżdżonych, których nie znasz? Jaki przypadek użycia to uzasadnia?

Zwykle chciałbyś faktycznie sprawdzić, z jakim obiektem masz do czynienia.

Na marginesie nie powinieneś używać takich instrukcji, if(a.b)ponieważ zwróci to fałsz, jeśli ab jest równe 0 lub nawet jeśli wynosi „0”. Zamiast tego sprawdź, czya.b !== undefined


1
W odniesieniu do twojej pierwszej edycji: jest to uzasadnione; Mam do czynienia z wpisami w bazie danych o strukturze JSON, tak że obiekty będą skalować wiele poziomów pól (np. Entry.users.messages.date itp., Gdzie nie wszystkie sprawy mają wprowadzone dane)
Ari

"zwróci prawdę, jeśli ab wynosi 0" - nie. typeof a.b === "undefined" && a.b!=null- nie trzeba robić drugiej części po pierwszej, a ma to większy sens po prostu zrobićif ("b" in a)
Ian

@ Ian yeah, oczywiście chodziło mi o to na odwrót, to zwróci false, nawet jeśli ab wynosi „0”. Niezły chwyt
Benjamin Gruenbaum,

@BenjaminGruenbaum Brzmi dobrze, nie byłem pewien, czy to masz na myśli. Myślę też, że chcesz typeof a.b !== "undefined" && ab! = Null` - zwróć uwagę na!==
Ian,

3
Jeśli nie chcesz nudzić się ab && abc && console.log (abc), to jest to jedyny sposób na konsekwentne rejestrowanie niewiadomych.
Brian Cray,

14

Jeśli dobrze rozumiem Twoje pytanie, chcesz najbezpieczniej określić, czy obiekt zawiera właściwość.

Najłatwiej jest użyć inoperatora .

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

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

Oczywiście możesz zagnieździć to tak głęboko, jak chcesz

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

Nie jestem pewien, czy to pomoże.


9
Nie możesz bezpiecznie zagnieździć tego tak głęboko, jak chcesz. A co jeśli window.bjest niezdefiniowane? Pojawi się błąd typu:Cannot use 'in' operator to search for 'c' in undefined
Trevor

13

Jeśli używasz lodash , możesz użyć ich funkcji „ma”. Jest podobny do rodzimego „in”, ale zezwala na ścieżki.

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

2
Najlepsze, możemy użyć _.get()domyślnego dla łatwego czytania:_.get(object, 'a.b.c', 'default');
Ifnot

12

Spróbuj tego. Jeśli a.bjest niezdefiniowane, pozostawi ifinstrukcję bez żadnego wyjątku.

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

5

Jest to częsty problem podczas pracy z głębokim lub złożonym obiektem json, więc staram się unikać próbowania / przechwytywania lub osadzania wielu sprawdzeń, które spowodowałyby, że kod byłby nieczytelny, zwykle używam tego małego fragmentu kodu we wszystkich moich procesach, aby wykonać zadanie.

/* 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;
}

5

Jeśli masz lodash , możesz użyć jego .getmetody

_.get(a, 'b.c.d.e')

lub nadaj mu wartość domyślną

_.get(a, 'b.c.d.e', default)

4

Używam undefsafe religijnie. Testuje każdy poziom w twoim obiekcie, aż albo otrzyma żądaną wartość, albo zwróci „undefined”. Ale nigdy błędów.


2
to jest podobne do lodash_.get
Filype

Dobry okrzyk! Nadal przydatne, jeśli nie potrzebujesz innych funkcji lodash.
martinedwards


3

Podoba mi się odpowiedź Cao Shouguanga, ale nie przepadam za przekazywaniem funkcji jako parametru do funkcji getSafe za każdym razem, gdy wykonuję wywołanie. Zmodyfikowałem funkcję getSafe, aby akceptowała proste parametry i czysty 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);
}

To naprawdę nie działa getSafe(myObj.x.c). Wypróbowałem najnowsze wersje Chrome i Firefox.
Rickard Elimää

2

W odpowiedzi str, wartość „undefined” zostanie zwrócona zamiast ustawionej wartości domyślnej, jeśli właściwość jest niezdefiniowana. Czasami może to powodować błędy. Poniższe czynności sprawią, że wartość defaultVal będzie zawsze zwracana, gdy właściwość lub obiekt są niezdefiniowane.

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;
    }
}

Ulepszona wersja mojego kodu Hardy'ego Le Roux nie działa z let myObj = {} getSafe (() => myObj.ab, "nice"), podczas gdy mój działa. Ktoś wyjaśnia, dlaczego?
Cao Shouguang

2

Lodash ma getmetodę, która pozwala na ustawienie wartości domyślnej jako opcjonalnego trzeciego parametru, jak pokazano poniżej:

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

Wyobraź sobie, że chcemy zastosować serię funkcji do xwtedy i tylko wtedy, gdy xjest różna od null:

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

Powiedzmy teraz, że musimy zrobić to samo, aby y:

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

To samo dotyczy z:

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

Jak widać bez odpowiedniej abstrakcji, w końcu będziemy ciągle dublować kod. Taka abstrakcja już istnieje: monada Może .

Może monada posiada zarówno wartość i obliczeniowej kontekst:

  1. Monada zapewnia bezpieczeństwo wartości i stosuje do niej funkcje.
  2. Kontekst obliczeniowy to sprawdzenie zerowe przed zastosowaniem funkcji.

Naiwna implementacja wyglądałaby tak:

⚠️ Ta implementacja ma charakter wyłącznie ilustracyjny! Nie należy tego robić i jest złe na wielu poziomach. Powinno to jednak dać lepsze pojęcie o tym, o czym mówię.

Jak widać, nic nie może się zepsuć:

  1. Do naszej wartości stosujemy szereg funkcji
  2. Jeśli w dowolnym momencie wartość stanie się pusta (lub niezdefiniowana), po prostu nie stosujemy już żadnej funkcji.

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>


Załącznik 1

Nie potrafię wyjaśnić, czym są monady, ponieważ nie jest to celem tego postu, a są w tym ludzie lepsi niż ja. Jednak, jak powiedział Eric Elliot w swoim poście na blogu JavaScript Monads Made Simple :

Niezależnie od Twojego poziomu umiejętności lub zrozumienia teorii kategorii, używanie monad ułatwia pracę z kodem. Niekorzystanie z monad może utrudnić pracę z kodem (np. Piekło wywołań zwrotnych, zagnieżdżone gałęzie warunkowe, więcej gadatliwości).


Załącznik 2

Oto, jak rozwiązałbym twój problem za pomocą monady Może z

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

Odpowiedziałem na to wcześniej i zdarzyło mi się robić dzisiaj podobną kontrolę. Uproszczenie sprawdzania, czy istnieje zagnieżdżona właściwość z kropkami. Możesz zmodyfikować to, aby zwrócić wartość, lub coś domyślnego, aby osiągnąć swój cel.

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;

}

W swoim pytaniu możesz zrobić coś takiego:

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


0

Możesz uniknąć błędu, podając wartość domyślną przed pobraniem właściwości

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);
}

Działa to również świetnie z tablicami:

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)

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.