¿Cómo evitar errores de 'no se puede leer la propiedad de indefinidos'?

118

En mi código, trato con una matriz que tiene algunas entradas con muchos objetos anidados entre sí, mientras que algunos no. Se parece a lo siguiente:

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

Esto me está dando problemas porque necesito iterar a través de la matriz a veces, y la inconsistencia me arroja errores como este:

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

Soy consciente de que puedo decirlo if(a.b){ console.log(a.b.c)}, pero esto es extraordinariamente tedioso en los casos en los que hay hasta 5 o 6 objetos anidados entre sí. ¿Hay alguna otra forma (más fácil) de hacer que SOLO haga el archivo console.log si existe, pero sin arrojar un error?

Ari
fuente
3
El error es probablemente una excepción regular de Javascript, así que pruebe la try..catchdeclaración. Dicho esto, una matriz que contiene elementos tremendamente heterogéneos me parece un problema de diseño.
millimoose
3
Si su estructura no es consistente en todos los elementos, ¿qué hay de malo en verificar la existencia? Realmente, usaría if ("b" in a && "c" in a.b). Puede ser "tedioso", pero eso es lo que obtienes por inconsistencia ... lógica normal.
Ian
2
¿Por qué accedería a propiedades que no existen? ¿Por qué no sabe cómo se ven los objetos?
Bergi
9
Puedo entender por qué alguien no querría que un error bloqueara todo. No siempre se puede confiar en que las propiedades de un objeto existan o no existan. Si tiene algo en su lugar que pueda manejar el evento de que el objeto está mal formado, entonces su código es mucho más eficiente y menos frágil.
SSH Este
3
Te sorprendería
saber

Respuestas:

120

Actualización :

  • Si usa JavaScript de acuerdo con ECMAScript 2020 o posterior, consulte encadenamiento opcional .
  • TypeScript ha agregado soporte para el encadenamiento opcional en la versión 3.7 .
// use it like this
obj?.a?.lot?.of?.properties

Solución para JavaScript anterior a ECMASCript 2020 o TypeScript anterior a la versión 3.7 :

Una solución rápida es utilizar una función auxiliar de prueba / captura con la función de flecha 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');

Fragmento de trabajo:

Consulte este artículo para obtener más detalles.

str
fuente
2
¡Me encanta! lo único que agregaría es un console.warn dentro de la captura, para que sepa del error pero continúa.
Rab Shuki Gur
La captura de todas las excepciones sin volver a lanzarlas es malo y, en general, usar excepciones como parte del flujo de ejecución esperado tampoco es excelente, aunque en este caso está bastante bien contenido.
hugo
50

Lo que está haciendo genera una excepción (y con razón).

Siempre puedes hacer

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

Pero yo no pensaría en su caso de uso.

¿Por qué está accediendo a datos, 6 niveles anidados con los que no está familiarizado? ¿Qué caso de uso justifica esto?

Por lo general, le gustaría validar realmente con qué tipo de objeto está tratando.

Además, en una nota al margen, no debe usar declaraciones como if(a.b)porque devolverá falso si ab es 0 o incluso si es "0". En su lugar, compruebe sia.b !== undefined

Benjamin Gruenbaum
fuente
1
En lo que respecta a su primera edición: está justificado; Estoy tratando con entradas de bases de datos estructuradas JSON, de modo que los objetos escalarán múltiples niveles de campos (es decir, entradas.usuarios.mensajes.fecha, etc., donde no todos los casos tienen datos ingresados)
Ari
"Devolverá verdadero si ab es 0", no. typeof a.b === "undefined" && a.b!=null- innecesario hacer la segunda parte después de la primera, y tiene más sentido simplemente hacerloif ("b" in a)
Ian
@Ian sí, obviamente quise decir lo contrario, devolverá falso incluso si ab es "0". Buena captura
Benjamin Gruenbaum
@BenjaminGruenbaum Suena bien, no estaba seguro de si lo decía en serio. Además, creo que quiere typeof a.b !== "indefinido" && ab! = Null` - observe el!==
Ian
3
Si no desea el tedio de ab && abc && console.log (abc), entonces esta es la única forma de registrar constantemente las incógnitas.
Brian Cray
14

Si entiendo su pregunta correctamente, quiere la forma más segura de determinar si un objeto contiene una propiedad.

La forma más sencilla es utilizar el inoperador .

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

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

Por supuesto que puedes anidar esto tan profundo como quieras

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

No estoy seguro si esto ayuda.

Matt Weiss
fuente
9
No puedes anidar esto con seguridad tan profundamente como quieras. ¿Y si window.bno está definido? Recibirá un error de tipo:Cannot use 'in' operator to search for 'c' in undefined
Trevor
13

Si está usando lodash , podría usar su función "has". Es similar al nativo "in", pero permite rutas.

var testObject = {a: {b: {c: 'walrus'}}};
if(_.has(testObject, 'a.b.c')) {
  //Safely access your walrus here
}
tehwalris
fuente
2
Lo mejor es que podemos usar el _.get()valor predeterminado para una lectura fácil:_.get(object, 'a.b.c', 'default');
Si no es el
12

Prueba esto. Si a.bno está definido, dejará la ifdeclaración sin excepción.

if (a.b && a.b.c) {
  console.log(a.b.c);
}
Sean Chen
fuente
5

Este es un problema común cuando se trabaja con un objeto json profundo o complejo, por lo que trato de evitar try / catch o incrustar múltiples comprobaciones que harían que el código sea ilegible, por lo general uso este pequeño fragmento de código en todo mi procedimiento para hacer el trabajo.

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

Si tienes lodash puedes usar su .getmétodo

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

o darle un valor predeterminado

_.get(a, 'b.c.d.e', default)
Brandon Dyer
fuente
4

Yo uso religiosamente undefsafe . Prueba cada nivel en su objeto hasta que obtiene el valor que solicitó o devuelve "indefinido". Pero nunca errores.

martinedwards
fuente
2
que es similar a lodash_.get
Filype
¡Buen grito! Sigue siendo útil si no necesita las otras funciones de lodash.
Martinedwards
3

Me gusta la respuesta de Cao Shouguang, pero no me gusta pasar una función como parámetro a la función getSafe cada vez que hago la llamada. He modificado la función getSafe para aceptar parámetros simples y 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);
}
Hardy Le Roux
fuente
Realmente no funciona con getSafe(myObj.x.c). Probé las últimas versiones de Chrome y Firefox.
Rickard Elimää
2

En la respuesta de str, el valor 'indefinido' se devolverá en lugar del valor predeterminado establecido si la propiedad no está definida. A veces, esto puede provocar errores. Lo siguiente garantizará que el valor predeterminado siempre se devuelva cuando la propiedad o el objeto no estén definidos.

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;
    }
}
Cao Shouguang
fuente
La versión mejorada de Hardy Le Roux de mi código no funciona con let myObj = {} getSafe (() => myObj.ab, "nice"), mientras que el mío funciona. ¿Alguien explica por qué?
Cao Shouguang
2

Lodash tiene un getmétodo que permite un valor predeterminado como tercer parámetro opcional, como se muestra a continuación:

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>

Pureferret
fuente
2

Imagina que queremos aplicar una serie de funciones a xsi y solo si xno es nulo:

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

Ahora digamos que necesitamos hacer lo mismo para y:

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

Y lo mismo para z:

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

Como puede ver, sin una abstracción adecuada, terminaremos duplicando el código una y otra vez. Tal abstracción ya existe: la mónada Quizás .

La mónada Maybe tiene tanto un valor como un contexto computacional:

  1. La mónada mantiene el valor seguro y le aplica funciones.
  2. El contexto computacional es una verificación nula antes de aplicar una función.

Una implementación ingenua se vería así:

⚠️ ¡Esta implementación es solo para fines ilustrativos! No es así como debería hacerse y está mal en muchos niveles. Sin embargo, esto debería darle una mejor idea de lo que estoy hablando.

Como puede ver, nada se puede romper:

  1. Aplicamos una serie de funciones a nuestro valor
  2. Si en algún momento, el valor se vuelve nulo (o indefinido), ya no aplicamos ninguna función.

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>


Apéndice 1

No puedo explicar qué son las mónadas ya que este no es el propósito de esta publicación y hay gente mejor que yo en esto. Sin embargo, como dijo Eric Elliot en la publicación de su blog, JavaScript Monads Made Simple :

Independientemente de su nivel de habilidad o comprensión de la teoría de categorías, el uso de mónadas facilita el trabajo con su código. No aprovechar las mónadas puede dificultar el trabajo con su código (por ejemplo, infierno de devolución de llamada, ramas condicionales anidadas, más verbosidad).


Apéndice 2

Así es como resolvería su problema usando la mónada Quizás 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/[email protected]/dist/monet.js"></script>
<script>const {Maybe} = Monet;</script>

comandante personalizado
fuente
0

Respondí a esto antes y hoy estaba haciendo una verificación similar. Una simplificación para comprobar si existe una propiedad con puntos anidados. Puede modificar esto para devolver el valor o algún valor predeterminado para lograr su objetivo.

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;

}

En su pregunta, podría hacer algo como:

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

Yo suelo usar así:

 var x = object.any ? object.any.a : 'def';
Vansuita Jr.
fuente
0

Puede evitar obtener un error dando un valor predeterminado antes de obtener la propiedad

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

Esto también funciona muy bien con matrices:

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)

Igor Parra
fuente