Comment éviter les erreurs «impossible de lire la propriété des indéfinis»?


118

Dans mon code, je traite d'un tableau qui a des entrées avec de nombreux objets imbriqués les uns dans les autres, alors que certains ne le font pas. Cela ressemble à ceci:

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

Cela me pose des problèmes car je dois parfois parcourir le tableau et l'incohérence me lance des erreurs comme celle-ci:

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

Je sais que je peux le dire if(a.b){ console.log(a.b.c)}, mais c'est extrêmement fastidieux dans les cas où il y a jusqu'à 5 ou 6 objets imbriqués les uns dans les autres. Existe-t-il un autre moyen (plus simple) que je puisse lui faire faire UNIQUEMENT le console.log s'il existe, mais sans générer d'erreur?


3
L'erreur est probablement une exception Javascript régulière, alors essayez l' try..catchinstruction. Cela dit, un tableau contenant des éléments extrêmement hétérogènes me semble être un problème de conception.
millimoose

3
Si votre structure n'est pas cohérente entre les éléments, qu'est-ce qui ne va pas avec la vérification de l'existence? Vraiment, j'utiliserais if ("b" in a && "c" in a.b). C'est peut-être «fastidieux», mais c'est ce que vous obtenez par incohérence ... logique normale.
Ian

2
Pourquoi accéderiez-vous à des propriétés non existantes, pourquoi ne savez-vous pas à quoi ressemblent les objets?
Bergi

9
Je peux comprendre pourquoi quelqu'un ne voudrait pas qu'une erreur fasse tout planter. Vous ne pouvez pas toujours compter sur les propriétés d'un objet pour exister ou non. Si vous avez quelque chose en place qui peut gérer l'événement où l'objet est malformé, alors votre code est beaucoup plus efficace et moins fragile.
SSH ce

3
Vous seriez surpris de voir combien d'objets / tableaux sont malformés dans des situations
réelles

Réponses:


121

Mise à jour :

  • Si vous utilisez JavaScript selon ECMAScript 2020 ou version ultérieure, consultez chaînage facultatif .
  • TypeScript a ajouté la prise en charge du chaînage facultatif dans la version 3.7 .
// use it like this
obj?.a?.lot?.of?.properties

Solution pour JavaScript avant ECMASCript 2020 ou TypeScript antérieur à la version 3.7 :

Une solution rapide consiste à utiliser une fonction d'assistance try / catch avec la fonction de flèche 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');

Extrait de travail:

Consultez cet article pour plus de détails.


2
J'aime cela! la seule chose que j'ajouterais est un console.warn à l'intérieur de la capture, afin que vous sachiez l'erreur, mais cela continue.
Rabbi Shuki Gur

Attraper toutes les exceptions sans relancer est mauvais , et généralement utiliser des exceptions dans le cadre du flux d'exécution attendu n'est pas non plus génial - même si dans ce cas, il est assez bien contenu.
hugo

50

Ce que vous faites soulève une exception (et à juste titre).

Tu peux toujours faire

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

Mais je ne voudrais pas, plutôt penser à votre cas d'utilisation.

Pourquoi accédez-vous aux données, 6 niveaux imbriqués que vous n'êtes pas familier? Quel cas d'utilisation justifie cela?

Habituellement, vous souhaitez valider le type d'objet avec lequel vous avez affaire.

De plus, vous ne devez pas utiliser d'instructions comme celles if(a.b)-ci car elles renverront false si ab vaut 0 ou même s'il vaut "0". Vérifiez plutôt sia.b !== undefined


1
En ce qui concerne votre première modification: c'est justifié; Je traite des entrées de base de données structurées JSON, de sorte que les objets mettent à l'échelle plusieurs niveaux de champs (c'est-à-dire entrées.users.messages.date, etc., où tous les cas n'ont pas de données saisies)
Ari

"il retournera vrai si ab vaut 0" - non. typeof a.b === "undefined" && a.b!=null- inutile de faire la deuxième partie après la première, et il est plus logique de simplement le faireif ("b" in a)
Ian

@Ian ouais, je l'ai évidemment voulu dire dans l'autre sens, il retournera faux même si ab vaut "0". Nice catch
Benjamin Gruenbaum

@BenjaminGruenbaum Ça sonne bien, je ne savais pas si vous le pensiez. Aussi, je pense que vous voulez typeof a.b !== "undefined" && ab! = Null` - notez le!==
Ian

3
Si vous ne voulez pas ennuyer ab && abc && console.log (abc), c'est le seul moyen de consigner systématiquement les inconnues.
Brian Cray

14

Si je comprends bien votre question, vous voulez le moyen le plus sûr de déterminer si un objet contient une propriété.

Le moyen le plus simple est d'utiliser l' inopérateur .

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

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

Bien sûr, vous pouvez imbriquer cela aussi profondément que vous le souhaitez

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

Je ne sais pas si cela aide.


9
Vous ne pouvez pas imbriquer cela en toute sécurité aussi profondément que vous le souhaitez. Et si window.bn'est pas défini? Vous obtiendrez une erreur de type:Cannot use 'in' operator to search for 'c' in undefined
Trevor

13

Si vous utilisez lodash , vous pouvez utiliser leur fonction "has". Il est similaire au "in" natif, mais autorise les chemins.

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

2
Mieux, nous pouvons utiliser _.get()par défaut pour une lecture facile:_.get(object, 'a.b.c', 'default');
Sinon,

12

Essaye ça. Si a.bn'est pas défini, il laissera l' ifinstruction sans aucune exception.

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

5

C'est un problème courant lorsque vous travaillez avec un objet json profond ou complexe, donc j'essaie d'éviter d'essayer / attraper ou d'incorporer plusieurs vérifications qui rendraient le code illisible, j'utilise généralement ce petit morceau de code dans tout mon processus pour faire le travail.

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

Si vous avez du lodash, vous pouvez utiliser sa .getméthode

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

ou donnez-lui une valeur par défaut

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

4

J'utilise undefsafe religieusement. Il teste chaque niveau vers le bas dans votre objet jusqu'à ce qu'il obtienne la valeur que vous avez demandée, ou qu'il retourne "indéfini". Mais jamais d'erreurs.


2
qui est similaire à lodash_.get
Filype

Bon cri! Toujours utile si vous n'avez pas besoin des autres fonctionnalités de lodash.
martinedwards


3

J'aime la réponse de Cao Shouguang, mais je n'aime pas passer une fonction comme paramètre dans la fonction getSafe à chaque fois que je fais l'appel. J'ai modifié la fonction getSafe pour accepter des paramètres simples et ES5 pur.

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

Cela ne fonctionne pas vraiment avec getSafe(myObj.x.c). J'ai essayé les dernières versions de Chrome et Firefox.
Rickard Elimää

2

Dans la réponse de str, la valeur «undefined» sera renvoyée à la place de la valeur par défaut définie si la propriété n'est pas définie. Cela peut parfois provoquer des bogues. Ce qui suit garantira que defaultVal sera toujours retourné lorsque la propriété ou l'objet n'est pas défini.

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 version améliorée de Hardy Le Roux de mon code ne fonctionne pas avec let myObj = {} getSafe (() => myObj.ab, "nice"), alors que la mienne fonctionne. Quelqu'un explique pourquoi?
Cao Shouguang

2

Lodash a une getméthode qui permet une valeur par défaut comme troisième paramètre facultatif, comme indiqué ci-dessous:

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

Imaginons que nous voulions appliquer une série de fonctions à xsi et seulement si xest non nul:

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

Disons maintenant que nous devons faire de même pour y:

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

Et la même chose pour z:

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

Comme vous pouvez le voir sans une abstraction appropriée, nous finirons par dupliquer le code encore et encore. Une telle abstraction existe déjà: la monade peut - être .

La monade Maybe contient à la fois une valeur et un contexte de calcul:

  1. La monade garde la valeur en sécurité et lui applique des fonctions.
  2. Le contexte de calcul est un contrôle nul avant d'appliquer une fonction.

Une implémentation naïve ressemblerait à ceci:

⚠️ Cette implémentation est à des fins d'illustration uniquement! Ce n'est pas ainsi que cela devrait être fait et cela est faux à plusieurs niveaux. Cependant, cela devrait vous donner une meilleure idée de ce dont je parle.

Comme vous pouvez le voir, rien ne peut casser:

  1. Nous appliquons une série de fonctions à notre valeur
  2. Si à tout moment la valeur devient nulle (ou indéfinie), nous n'appliquons plus aucune fonction.

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>


Annexe 1

Je ne peux pas expliquer ce que sont les monades car ce n'est pas le but de cet article et il y a des gens meilleurs que moi dans ce domaine. Cependant, comme Eric Elliot l'a dit dans le billet de blog de l'hist, JavaScript Monads Made Simple :

Quel que soit votre niveau de compétence ou votre compréhension de la théorie des catégories, l'utilisation de monades rend votre code plus facile à utiliser. Ne pas profiter des monades peut rendre votre code plus difficile à utiliser (par exemple, l'enfer des rappels, les branches conditionnelles imbriquées, plus de verbosité).


Annexe 2

Voici comment résoudre votre problème en utilisant la monade Maybe de

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

J'ai déjà répondu à cette question et je faisais une vérification similaire aujourd'hui. Une simplification pour vérifier si une propriété en pointillés imbriquée existe. Vous pouvez le modifier pour renvoyer la valeur, ou une valeur par défaut pour atteindre votre objectif.

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;

}

Dans votre question, vous pourriez faire quelque chose comme:

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

0

J'utilise généralement comme ceci:

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

0

Vous pouvez éviter d'obtenir une erreur en donnant une valeur par défaut avant d'obtenir la propriété

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

Cela fonctionne également très bien avec les tableaux:

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)

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.