Como evitar erros "não é possível ler propriedade de indefinido"?

118

No meu código, lido com um array que possui algumas entradas com muitos objetos aninhados uns dentro dos outros, enquanto alguns não. É algo parecido com o seguinte:

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

Isso está me causando problemas porque às vezes preciso iterar pela matriz, e a inconsistência está me gerando erros como:

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

Estou ciente do que posso dizer if(a.b){ console.log(a.b.c)}, mas isso é extremamente tedioso nos casos em que há até 5 ou 6 objetos aninhados um dentro do outro. Existe alguma outra maneira (mais fácil) de fazer com que SOMENTE faça o console.log se ele existir, mas sem gerar um erro?

Ari
fonte
3
O erro é provavelmente uma exceção normal do Javascript, então tente a try..catchinstrução. Dito isso, uma matriz que contém elementos extremamente heterogêneos parece um problema de design para mim.
millimoose de
3
Se sua estrutura não for consistente entre os itens, o que há de errado em verificar a existência? Realmente, eu usaria if ("b" in a && "c" in a.b). Pode ser "tedioso", mas é o que você obtém para inconsistência ... lógica normal.
Ian
2
Por que você acessaria propriedades não existentes, por que não sabe como os objetos se parecem?
Bergi
9
Posso entender por que alguém não gostaria que um erro travasse tudo. Você nem sempre pode contar com as propriedades de um objeto para existir ou não existir. Se você possui algo que pode controlar o evento de que o objeto está malformado, então seu código é muito mais eficiente e menos frágil.
SSH em
3
Você ficaria surpreso com a quantidade de objetos / matrizes malformados em situações da vida real
OneMoreQuestion

Respostas:

121

Atualização :

  • Se você usar JavaScript de acordo com ECMAScript 2020 ou posterior, consulte encadeamento opcional .
  • TypeScript adicionou suporte para encadeamento opcional na versão 3.7 .
// use it like this
obj?.a?.lot?.of?.properties

Solução para JavaScript anterior ao ECMASCript 2020 ou TypeScript anterior à versão 3.7 :

Uma solução rápida é usar uma função auxiliar try / catch com a função de seta 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');

Trecho de trabalho:

Veja este artigo para detalhes.

str
fonte
2
Eu amo isso! a única coisa que eu adicionaria é um console.warn dentro da captura, para que você saiba do erro, mas ele continua.
Rabino Shuki Gur
Capturar todas as exceções sem relançar é ruim e, geralmente, usar exceções como parte do fluxo de execução esperado também não é ótimo - embora, neste caso, esteja muito bem contido.
hugo
50

O que você está fazendo levanta uma exceção (e com razão).

Você sempre pode fazer

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

Mas eu não faria isso, em vez disso, pensaria em seu caso de uso.

Por que você está acessando dados, 6 níveis aninhados que você não conhece? Qual caso de uso justifica isso?

Normalmente, você gostaria de realmente validar o tipo de objeto com o qual está lidando.

Além disso, em uma nota lateral, você não deve usar declarações como if(a.b)porque retornará falso se ab for 0 ou mesmo se for "0". Em vez disso, verifique sea.b !== undefined

Benjamin Gruenbaum
fonte
1
Em relação à sua primeira edição: É justificado; Estou lidando com entradas de banco de dados estruturado JSON, de modo que os objetos escalem vários níveis de campos (ou seja, entradas.usuários.mensagens.data etc., onde nem todos os casos têm dados inseridos)
Ari
"ele retornará verdadeiro se ab for 0" - não. typeof a.b === "undefined" && a.b!=null- desnecessário fazer a segunda parte após a primeira, e faz mais sentido apenas fazerif ("b" in a)
Ian
@Ian sim, eu obviamente quis dizer o contrário, ele retornará falso mesmo se ab for "0". Boa pegada
Benjamin Gruenbaum
@BenjaminGruenbaum Parece bom, não tinha certeza se você queria dizer isso. Além disso, acho que você quer typeof a.b !== "undefined" && ab! = Null` - observe o!==
Ian
3
Se você não quer entediar ab && abc && console.log (abc), então esta é a única maneira de registrar incógnitas de forma consistente.
Brian Cray
14

Se estou entendendo sua pergunta corretamente, você deseja a maneira mais segura de determinar se um objeto contém uma propriedade.

A maneira mais fácil é usar o inoperador .

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

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

Claro que você pode aninhar isso tão profundo quanto quiser

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

Não tenho certeza se isso ajuda.

Matt Weiss
fonte
9
Você não pode aninhar isso com segurança tão profundamente quanto deseja. E se window.bfor indefinido? Você receberá um erro de tipo:Cannot use 'in' operator to search for 'c' in undefined
Trevor
13

Se você estiver usando lodash , poderá usar a função "tem". É semelhante ao "in" nativo, mas permite caminhos.

var testObject = {a: {b: {c: 'walrus'}}};
if(_.has(testObject, 'a.b.c')) {
  //Safely access your walrus here
}
Tehwalris
fonte
2
Melhor, podemos usar _.get()com o padrão para facilitar a leitura:_.get(object, 'a.b.c', 'default');
Ifnot
12

Experimente isso. Se a.bfor indefinido, ele deixará a ifinstrução sem nenhuma exceção.

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

Este é um problema comum ao trabalhar com objetos json complexos ou profundos, então tento evitar try / catch ou incorporar várias verificações que tornariam o código ilegível. Normalmente uso esse pequeno pedaço de código em todo o meu processo para fazer o trabalho.

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

Se você tem lodash, você pode usar seu .getmétodo

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

ou dê a ele um valor padrão

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

Eu uso o undefsafe religiosamente. Ele testa cada nível em seu objeto até obter o valor que você solicitou ou retornar "indefinido". Mas nunca erros.

Martinedwards
fonte
2
que é semelhante a lodash_.get
Filype
Bom grito! Ainda é útil se você não precisar dos outros recursos do lodash.
martinedwards de
3

Gosto da resposta de Cao Shouguang, mas não gosto de passar uma função como parâmetro para a função getSafe cada vez que faço a chamada. Eu modifiquei a função getSafe para aceitar parâmetros simples 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);
}
Hardy Le Roux
fonte
Realmente não funciona com getSafe(myObj.x.c). Tentei as versões mais recentes do Chrome e Firefox.
Rickard Elimää
2

Na resposta de str, o valor 'undefined' será retornado em vez do valor padrão definido se a propriedade for undefined. Isso às vezes pode causar bugs. O seguinte garantirá que defaultVal sempre será retornado quando a propriedade ou o objeto estiver indefinido.

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
fonte
A versão aprimorada de Hardy Le Roux do meu código não funciona com let myObj = {} getSafe (() => myObj.ab, "nice"), enquanto o meu funciona. Alguém explica por quê?
Cao Shouguang
2

Lodash possui um getmétodo que permite um padrão como um terceiro parâmetro opcional, conforme mostrado abaixo:

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
fonte
2

Imagine que queremos aplicar uma série de funções xse, e somente se, xnão for nulo:

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

Agora, digamos que precisamos fazer o mesmo para y:

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

E o mesmo para z:

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

Como você pode ver, sem uma abstração adequada, acabaremos duplicando o código continuamente. Essa abstração já existe: a mônada Maybe .

A mônada Maybe contém um valor e um contexto computacional:

  1. A mônada mantém o valor seguro e aplica funções a ele.
  2. O contexto computacional é uma verificação nula antes de aplicar uma função.

Uma implementação ingênua seria assim:

⚠️ Esta implementação é apenas para fins ilustrativos! Não é assim que deve ser feito e está errado em muitos níveis. No entanto, isso deve dar a você uma idéia melhor do que estou falando.

Como você pode ver, nada pode quebrar:

  1. Aplicamos uma série de funções ao nosso valor
  2. Se em algum ponto o valor se tornar nulo (ou indefinido), simplesmente não aplicamos mais nenhuma função.

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

Não posso explicar o que são mônadas, pois este não é o propósito deste post e existem pessoas melhores nisso do que eu. No entanto, como Eric Elliot disse em sua postagem em seu blog JavaScript Monads Made Simple :

Independentemente do seu nível de habilidade ou compreensão da teoria das categorias, o uso de mônadas facilita o trabalho com o código. Deixar de tirar proveito das mônadas pode tornar seu código mais difícil de trabalhar (por exemplo, inferno de callback, ramificações condicionais aninhadas, mais verbosidade).


Apêndice 2

Veja como eu resolveria seu problema usando a mônada 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/[email protected]/dist/monet.js"></script>
<script>const {Maybe} = Monet;</script>

comandante personalizado
fonte
0

Eu respondi isso antes e aconteceu de estar fazendo uma verificação semelhante hoje. Uma simplificação para verificar se existe uma propriedade pontilhada aninhada. Você pode modificar isso para retornar o valor ou algum padrão para atingir seu 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;

}

Em sua pergunta, você poderia fazer algo como:

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

Eu costumo usar assim:

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

Você pode evitar obter um erro fornecendo um valor padrão antes de obter a propriedade

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

Isso também funciona bem com arrays:

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
fonte