Come evitare errori "impossibile leggere la proprietà di undefined"?


118

Nel mio codice, ho a che fare con un array che ha alcune voci con molti oggetti annidati l'uno nell'altro, mentre altri no. Ha un aspetto simile al seguente:

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

Questo mi sta dando problemi perché a volte ho bisogno di iterare attraverso l'array e l'incongruenza mi genera errori in questo modo:

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

Sono consapevole di poterlo dire if(a.b){ console.log(a.b.c)}, ma questo è straordinariamente noioso nei casi in cui ci sono fino a 5 o 6 oggetti annidati l'uno nell'altro. Esiste un altro modo (più semplice) per far sì che esegua SOLO console.log se esiste, ma senza generare errori?


3
L'errore è probabilmente una normale eccezione Javascript, quindi prova l' try..catchistruzione. Detto questo, un array che contiene elementi estremamente eterogenei mi sembra un problema di progettazione.
millimoose

3
Se la tua struttura non è coerente tra gli elementi, allora cosa c'è di sbagliato nel verificare l'esistenza? Davvero, lo userei if ("b" in a && "c" in a.b). Può essere "noioso", ma è quello che ottieni per incoerenza ... logica normale.
Ian

2
Perché dovresti accedere a proprietà inesistenti, perché non sai come appaiono gli oggetti?
Bergi

9
Posso capire perché qualcuno non vorrebbe che un errore mandasse tutto in crash. Non puoi sempre fare affidamento sulle proprietà di un oggetto per esistere o non esistere. Se disponi di qualcosa in grado di gestire l'evento che l'oggetto non è valido, il tuo codice è molto più efficiente e meno fragile.
SSH questo

3

Risposte:


121

Aggiornamento :

  • Se utilizzi JavaScript secondo ECMAScript 2020 o versioni successive, vedi concatenamento facoltativo .
  • TypeScript ha aggiunto il supporto per il concatenamento opzionale nella versione 3.7 .
// use it like this
obj?.a?.lot?.of?.properties

Soluzione per JavaScript prima di ECMASCript 2020 o TypeScript precedente alla versione 3.7 :

Una soluzione rapida consiste nell'usare una funzione di aiuto try / catch con la funzione freccia 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');

Snippet di lavoro:

Vedi questo articolo per i dettagli.


2
Lo adoro! l'unica cosa che aggiungerei è un console.warn all'interno del fermo, in modo che tu sappia dell'errore ma continua.
Rabbi Shuki Gur

Catturare tutte le eccezioni senza rilanciare è un male , e in genere anche usare le eccezioni come parte del flusso di esecuzione previsto non è eccezionale, anche se in questo caso è abbastanza ben contenuto.
Hugo

50

Quello che stai facendo solleva un'eccezione (e giustamente).

Puoi sempre farlo

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

Ma non penserei invece al tuo caso d'uso.

Perché stai accedendo ai dati, 6 livelli nidificati che non conosci? Quale caso d'uso lo giustifica?

Di solito, ti piacerebbe convalidare effettivamente il tipo di oggetto con cui hai a che fare.

Inoltre, in una nota a margine non dovresti usare istruzioni come if(a.b)perché restituirà false se ab è 0 o anche se è "0". Invece controlla sea.b !== undefined


1
Riguardo alla tua prima modifica: è giustificata; Ho a che fare con voci di database strutturate JSON, in modo tale che gli oggetti scalino più livelli di campi (es. Entry.users.messages.date ecc., Dove non tutti i casi hanno dati inseriti)
Ari

"restituirà vero se ab è 0" - no. typeof a.b === "undefined" && a.b!=null- non è necessario fare la seconda parte dopo la prima, e ha più senso farlo soloif ("b" in a)
Ian

@Ian sì, ovviamente intendevo il contrario, restituirà false anche se ab è "0". Bella cattura
Benjamin Gruenbaum

@BenjaminGruenbaum Suona bene, non ero sicuro che lo intendessi. Inoltre, penso che tu voglia typeof a.b !== "undefined" && ab! = Null` - nota il!==
Ian

3
Se non vuoi tedio di ab && abc && console.log (abc), questo è l'unico modo per registrare in modo coerente le incognite.
Brian Cray

14

Se capisco correttamente la tua domanda, vuoi il modo più sicuro per determinare se un oggetto contiene una proprietà.

Il modo più semplice è utilizzare l' inoperatore .

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

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

Ovviamente puoi annidarlo in profondità quanto vuoi

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

Non sono sicuro che questo aiuti.


9
Non puoi annidarlo in sicurezza così profondamente come vuoi. E se window.bnon è definito? Riceverai un errore di tipo:Cannot use 'in' operator to search for 'c' in undefined
Trevor

13

Se stai usando lodash , potresti usare la loro funzione "has". È simile al nativo "in", ma consente percorsi.

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

2
Meglio, possiamo usare _.get()con l'impostazione predefinita per una facile lettura:_.get(object, 'a.b.c', 'default');
Se non il

12

Prova questo. Se a.bnon è definito, lascerà l' ifistruzione senza alcuna eccezione.

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

5

Questo è un problema comune quando si lavora con oggetti json profondi o complessi, quindi cerco di evitare di provare / catturare o incorporare più controlli che renderebbero illeggibile il codice, di solito uso questo piccolo pezzo di codice in tutto il mio processo per fare il lavoro.

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

Se hai lodash puoi usare il suo .getmetodo

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

o dagli un valore predefinito

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

4

Uso religiosamente undefsafe . Verifica ogni livello nell'oggetto fino a quando non ottiene il valore richiesto o restituisce "undefined". Ma mai errori.


2
che è simile a lodash_.get
Filype

Bel grido! Ancora utile se non hai bisogno delle altre funzionalità di lodash.
martinedwards


3

Mi piace la risposta di Cao Shouguang, ma non mi piace passare una funzione come parametro nella funzione getSafe ogni volta che eseguo la chiamata. Ho modificato la funzione getSafe per accettare parametri semplici e ES5 puro.

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

Non funziona davvero con getSafe(myObj.x.c). Ho provato le ultime versioni di Chrome e Firefox.
Rickard Elimää

2

Nella risposta di str, verrà restituito il valore "undefined" invece del valore predefinito impostato se la proprietà non è definita. Questo a volte può causare bug. Quanto segue assicurerà che defaultVal venga sempre restituito quando la proprietà o l'oggetto non sono definiti.

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

La versione migliorata del mio codice di Hardy Le Roux non funziona con let myObj = {} getSafe (() => myObj.ab, "nice"), mentre il mio funziona. Qualcuno spiega perché?
Cao Shouguang

2

Lodash ha un getmetodo che consente un valore predefinito come terzo parametro opzionale, come mostrato di seguito:

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

Immagina di voler applicare una serie di funzioni a xse e solo se xnon è nullo:

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

Ora diciamo che dobbiamo fare lo stesso per y:

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

E lo stesso per z:

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

Come puoi vedere senza un'astrazione adeguata, finiremo per duplicare il codice più e più volte. Una tale astrazione esiste già: la monade Forse .

La monade Forse contiene sia un valore che un contesto computazionale:

  1. La monade mantiene il valore al sicuro e applica le funzioni ad esso.
  2. Il contesto computazionale è un controllo nullo prima di applicare una funzione.

Un'implementazione ingenua sarebbe simile a questa:

⚠️ Questa implementazione è solo a scopo illustrativo! Non è così che dovrebbe essere fatto ed è sbagliato a molti livelli. Tuttavia questo dovrebbe darti un'idea migliore di cosa sto parlando.

Come puoi vedere niente può rompersi:

  1. Applichiamo una serie di funzioni al nostro valore
  2. Se in qualsiasi momento il valore diventa nullo (o non definito) semplicemente non applichiamo più alcuna funzione.

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>


Appendice 1

Non riesco a spiegare cosa sono le monadi perché questo non è lo scopo di questo post e ci sono persone là fuori migliori di me in questo. Tuttavia, come ha detto Eric Elliot nel suo post sul blog JavaScript Monads Made Simple :

Indipendentemente dal tuo livello di abilità o dalla comprensione della teoria delle categorie, l'uso delle monadi rende più facile lavorare con il tuo codice. Non riuscire a trarre vantaggio dalle monadi può rendere più difficile lavorare con il codice (ad esempio, callback hell, rami condizionali annidati, più verbosità).


Appendice 2

Ecco come risolverei il tuo problema usando il Forse monade di

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

Ho risposto prima e oggi stavo facendo un controllo simile. Una semplificazione per verificare se esiste una proprietà puntata annidata. È possibile modificarlo per restituire il valore o un valore predefinito per raggiungere il proprio obiettivo.

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;

}

Nella tua domanda potresti fare qualcosa come:

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

0

Di solito uso in questo modo:

 var x = object.any ? object.any.a : 'def';

0

È possibile evitare di ottenere un errore fornendo un valore predefinito prima di ottenere la proprietà

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

Funziona benissimo anche con gli array:

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)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.