Teste para existência de chave de object JavaScript aninhada

Se eu tenho uma referência a um object:

var test = {}; 

que potencialmente (mas não imediatamente) terá objects nesteds, algo como:

 {level1: {level2: {level3: "level3"}}}; 

Qual é a melhor maneira de testar a existência de chaves nos objects mais profundamente nesteds?

alert(test.level1); rende undefined , mas alert(test.level1.level2.level3); falha.

Atualmente estou fazendo algo assim:

 if(test.level1 && test.level1.level2 && test.level1.level2.level3) { alert(test.level1.level2.level3); } 

mas eu queria saber se há um jeito melhor.

Você tem que fazer isso passo a passo se você não quer um TypeError , porque se um dos membros é null ou undefined , e você tentar acessar um membro, uma exceção será lançada.

Você pode simplesmente catch a exceção ou fazer uma function para testar a existência de vários níveis, algo assim:

 function checkNested(obj /*, level1, level2, ... levelN*/) { var args = Array.prototype.slice.call(arguments, 1); for (var i = 0; i < args.length; i++) { if (!obj || !obj.hasOwnProperty(args[i])) { return false; } obj = obj[args[i]]; } return true; } var test = {level1:{level2:{level3:'level3'}} }; checkNested(test, 'level1', 'level2', 'level3'); // true checkNested(test, 'level1', 'level2', 'foo'); // false 

Aqui está um padrão que eu peguei de Oliver Steele :

 var level3 = (((test || {}).level1 || {}).level2 || {}).level3; alert( level3 ); 

Na verdade, esse artigo inteiro é uma discussão de como você pode fazer isso em javascript. Ele decide usar a syntax acima (que não é tão difícil de ler depois de se acostumar com isso) como uma expressão idiomática.

Atualizar

Parece que o lodash adicionou _.get para todas as suas necessidades aninhadas.

 _.get(countries, 'greece.sparta.playwright') 

https://lodash.com/docs#get


Resposta anterior

Os usuários do lodash podem desfrutar do lodash.contrib, que possui alguns methods que atenuam esse problema .

getPath

Assinatura: _.getPath(obj:Object, ks:String|Array)

Obtém o valor em qualquer profundidade em um object nested com base no caminho descrito pelas chaves fornecidas. As chaves podem ser dadas como um array ou como uma string separada por pontos. Retorna undefined se o caminho não puder ser alcançado.

 var countries = { greece: { athens: { playwright: "Sophocles" } } } }; _.getPath(countries, "greece.athens.playwright"); // => "Sophocles" _.getPath(countries, "greece.sparta.playwright"); // => undefined _.getPath(countries, ["greece", "athens", "playwright"]); // => "Sophocles" _.getPath(countries, ["greece", "sparta", "playwright"]); // => undefined 

Eu fiz testes de desempenho (obrigado cdMinix para adicionar lodash) em algumas das sugestões propostas para esta pergunta com os resultados listados abaixo.

Disclaimer # 1 Transformar strings em referências é meta-programação desnecessária e, provavelmente, é melhor evitar. Não perca de vista suas referências para começar. Leia mais desta resposta para uma pergunta semelhante .

Disclaimer # 2 Estamos falando de milhões de operações por milissegundo aqui. É muito improvável que qualquer um deles faça muita diferença na maioria dos casos de uso. Escolha o que fizer mais sentido, conhecendo as limitações de cada um. Para mim eu iria com algo como reduce por conveniência.

Object Wrap (por Oliver Steele) – 34% – mais rápido

 var r1 = (((test || {}).level1 || {}).level2 || {}).level3; var r2 = (((test || {}).level1 || {}).level2 || {}).foo; 

Solução original (sugerida em questão) – 45%

 var r1 = test.level1 && test.level1.level2 && test.level1.level2.level3; var r2 = test.level1 && test.level1.level2 && test.level1.level2.foo; 

checkNested – 50%

 function checkNested(obj) { for (var i = 1; i < arguments.length; i++) { if (!obj.hasOwnProperty(arguments[i])) { return false; } obj = obj[arguments[i]]; } return true; } 

get_if_exist - 52%

 function get_if_exist(str) { try { return eval(str) } catch(e) { return undefined } } 

validChain - 54%

 function validChain( object, ...keys ) { return keys.reduce( ( a, b ) => ( a || { } )[ b ], object ) !== undefined; } 

objHasKeys - 63%

 function objHasKeys(obj, keys) { var next = keys.shift(); return obj[next] && (! keys.length || objHasKeys(obj[next], keys)); } 

nestedPropertyExists - 69%

 function nestedPropertyExists(obj, props) { var prop = props.shift(); return prop === undefined ? true : obj.hasOwnProperty(prop) ? nestedPropertyExists(obj[prop], props) : false; } 

_.get - 72%

deeptest - 86%

 function deeptest(target, s){ s= s.split('.') var obj= target[s.shift()]; while(obj && s.length) obj= obj[s.shift()]; return obj; } 

Palhaços tristes - 100% - mais lentos

 var o = function(obj) { return obj || {} }; var r1 = o(o(o(o(test).level1).level2).level3); var r2 = o(o(o(o(test).level1).level2).foo); 

Você pode ler uma propriedade de object a qualquer profundidade, se você manipular o nome como uma string: 't.level1.level2.level3' .

 window.t={level1:{level2:{level3: 'level3'}}}; function deeptest(s){ s= s.split('.') var obj= window[s.shift()]; while(obj && s.length) obj= obj[s.shift()]; return obj; } alert(deeptest('t.level1.level2.level3') || 'Undefined'); 

Ele retorna undefined se qualquer um dos segmentos for undefined .

 var a; a = { b: { c: 'd' } }; function isset (fn) { var value; try { value = fn(); } catch (e) { value = undefined; } finally { return value !== undefined; } }; // ES5 console.log( isset(function () { return abc; }), isset(function () { return abcdef; }) ); 

Se você estiver codificando no ambiente ES6 (ou usando 6to5 ), pode aproveitar a syntax da function de seta :

 // ES6 using the arrow function console.log( isset(() => abc), isset(() => abcdef) ); 

Em relação ao desempenho, não há penalidade de desempenho para usar o bloco try..catch se a propriedade estiver configurada. Há um impacto no desempenho se a propriedade não for definida.

Considere simplesmente usando _.has :

 var object = { 'a': { 'b': { 'c': 3 } } }; _.has(object, 'a'); // → true _.has(object, 'abc'); // → true _.has(object, ['a', 'b', 'c']); // → true 

e quanto a

 try { alert(test.level1.level2.level3) } catch(e) { ...whatever } 

Eu tentei uma abordagem recursiva:

 function objHasKeys(obj, keys) { var next = keys.shift(); return obj[next] && (! keys.length || objHasKeys(obj[next], keys)); } 

O ! keys.length || ! keys.length || sai da recursion para não executar a function sem nenhuma tecla para testar. Testes:

 obj = { path: { to: { the: { goodKey: "hello" } } } } console.log(objHasKeys(obj, ['path', 'to', 'the', 'goodKey'])); // true console.log(objHasKeys(obj, ['path', 'to', 'the', 'badKey'])); // undefined 

Eu estou usando para imprimir uma visão amigável em HTML de um monte de objects com valores / chaves desconhecidos, por exemplo:

 var biosName = objHasKeys(myObj, 'MachineInfo:BiosInfo:Name'.split(':')) ? myObj.MachineInfo.BiosInfo.Name : 'unknown'; 

Resposta ES6, exaustivamente testada 🙂

 const propExists = (obj, path) => { return !!path.split('.').reduce((obj, prop) => { return obj && obj[prop] ? obj[prop] : undefined; }, obj) } 

→ veja Codepen com cobertura de teste completa

Uma maneira simples é esta:

 try { alert(test.level1.level2.level3); } catch(e) { alert("undefined"); // this is optional to put any output here } 

O try/catch captura os casos para quando qualquer um dos objects de nível mais alto, como test, test.level1, test.level1.level2, não estiver definido.

Eu acho que o script a seguir dá uma representação mais legível.

declarar uma function:

 var o = function(obj) { return obj || {};}; 

então use-o assim:

 if (o(o(o(o(test).level1).level2).level3) { } 

Eu chamo de “técnica do palhaço triste” porque está usando o sinal o (


EDITAR:

aqui está uma versão para o TypeScript

ele dá verificações de tipo no tempo de compilation (assim como o intellisense se você usar uma ferramenta como o Visual Studio)

 export function o(someObject: T, defaultValue: T = {} as T) : T { if (typeof someObject === 'undefined' || someObject === null) return defaultValue; else return someObject; } 

o uso é o mesmo:

 o(o(o(o(test).level1).level2).level3 

mas desta vez intellisense funciona!

Além disso, você pode definir um valor padrão:

 o(o(o(o(o(test).level1).level2).level3, "none") 

Eu não vi nenhum exemplo de alguém usando Proxies

Então eu criei o meu próprio. O melhor de tudo é que você não precisa interpolar strings. Você pode, na verdade, retornar uma function de object com capacidade de cadeia e fazer algumas coisas mágicas com ela. Você pode até chamar funções e obter índices de array para verificar objects profundos

 function resolve(target) { var noop = () => {} // We us a noop function so we can call methods also return new Proxy(noop, { get(noop, key) { // return end result if key is _result return key === '_result' ? target : resolve( // resolve with target value or undefined target === undefined ? undefined : target[key] ) }, // if we want to test a function then we can do so alos thanks to using noop // instead of using target in our proxy apply(noop, that, args) { return resolve(typeof target === 'function' ? target.apply(that, args) : undefined) }, }) } // some modified examples from the accepted answer var test = {level1: {level2:() => ({level3:'level3'})}} var test1 = {key1: {key2: ['item0']}} // You need to get _result in the end to get the final result console.log(resolve(test).level1.level2().level3._result) console.log(resolve(test).level1.level2().level3.level4.level5._result) console.log(resolve(test1).key1.key2[0]._result) console.log(resolve(test1)[0].key._result) // don't exist 

Uma versão mais curta do ES5 da excelente resposta do @ CMS:

 // Check the obj has the keys in the order mentioned. Used for checking JSON results. var checkObjHasKeys = function(obj, keys) { var success = true; keys.forEach( function(key) { if ( ! obj.hasOwnProperty(key)) { success = false; } obj = obj[key]; }) return success; } 

Com um teste similar:

 var test = { level1:{level2:{level3:'result'}}}; utils.checkObjHasKeys(test, ['level1', 'level2', 'level3']); // true utils.checkObjHasKeys(test, ['level1', 'level2', 'foo']); // false 

Com base nessa resposta , desenvolvi essa function genérica usando o ES2015 que resolveria o problema

 function validChain( object, ...keys ) { return keys.reduce( ( a, b ) => ( a || { } )[ b ], object ) !== undefined; } var test = { first: { second: { third: "This is not the key your are looking for" } } } if ( validChain( test, "first", "second", "third" ) ) { console.log( test.first.second.third ); } 

A resposta dada pelo CMS funciona bem com a seguinte modificação para verificações nulas

 function checkNested(obj /*, level1, level2, ... levelN*/) { var args = Array.prototype.slice.call(arguments), obj = args.shift(); for (var i = 0; i < args.length; i++) { if (obj == null || !obj.hasOwnProperty(args[i]) ) { return false; } obj = obj[args[i]]; } return true; } 

As seguintes opções foram elaboradas a partir desta resposta . Mesma tree para ambos:

 var o = { a: { b: { c: 1 } } }; 

Parar de pesquisar quando estiver indefinido

 var u = undefined; oa ? oab ? oabc : u : u // 1 ox ? oxy ? oxyz : u : u // undefined (o = oa) ? (o = ob) ? oc : u : u // 1 

Assegure cada nível um por um

 var $ = function (empty) { return function (node) { return node || empty; }; }({}); $($(oa).b).c // 1 $($(ox).y).z // undefined 

Eu sei que esta pergunta é antiga, mas eu queria oferecer uma extensão adicionando isso a todos os objects. Eu sei que as pessoas tendem a frown usando o protótipo de object para a funcionalidade de object estendido, mas eu não acho nada mais fácil do que fazer isso. Além disso, agora é permitido com o método Object.defineProperty .

 Object.defineProperty( Object.prototype, "has", { value: function( needle ) { var obj = this; var needles = needle.split( "." ); for( var i = 0; i 

Agora, para testar qualquer propriedade em qualquer object, você pode simplesmente fazer:

 if( obj.has("some.deep.nested.object.somewhere") ) 

Aqui está um jsfiddle para testá-lo, e em particular ele inclui alguns jQuery que quebram se você modificar o Object.prototype diretamente por causa da propriedade se tornar enumerável. Isso deve funcionar bem com bibliotecas de terceiros.

Eu acho que isso é uma melhora leve (se torna um 1-liner):

  alert( test.level1 && test.level1.level2 && test.level1.level2.level3 ) 

Isso funciona porque o operador && retorna o operando final que ele avaliou (e causa curto-circuito).

Isso funciona com todos os objects e matrizes 🙂

ex:

 if( obj._has( "something.['deep']['under'][1][0].item" ) ) { //do something } 

esta é a minha versão melhorada da resposta de Brian

Eu usei _has como o nome da propriedade porque pode entrar em conflito com a propriedade existente (ex: mapas)

 Object.defineProperty( Object.prototype, "_has", { value: function( needle ) { var obj = this; var needles = needle.split( "." ); var needles_full=[]; var needles_square; for( var i = 0; i1){ for( var j = 0; j 

Aqui está o violino

Aqui está minha opinião sobre isso – a maioria dessas soluções ignora o caso de uma matriz aninhada como em:

  obj = { "l1":"something", "l2":[{k:0},{k:1}], "l3":{ "subL":"hello" } } 

Eu posso querer verificar por obj.l2[0].k

Com a function abaixo, você pode fazer deeptest('l2[0].k',obj)

A function retornará true se o object existir, false caso contrário

 function deeptest(keyPath, testObj) { var obj; keyPath = keyPath.split('.') var cKey = keyPath.shift(); function get(pObj, pKey) { var bracketStart, bracketEnd, o; bracketStart = pKey.indexOf("["); if (bracketStart > -1) { //check for nested arrays bracketEnd = pKey.indexOf("]"); var arrIndex = pKey.substr(bracketStart + 1, bracketEnd - bracketStart - 1); pKey = pKey.substr(0, bracketStart); var n = pObj[pKey]; o = n? n[arrIndex] : undefined; } else { o = pObj[pKey]; } return o; } obj = get(testObj, cKey); while (obj && keyPath.length) { obj = get(obj, keyPath.shift()); } return typeof(obj) !== 'undefined'; } var obj = { "l1":"level1", "arr1":[ {"k":0}, {"k":1}, {"k":2} ], "sub": { "a":"letter A", "b":"letter B" } }; console.log("l1: " + deeptest("l1",obj)); console.log("arr1[0]: " + deeptest("arr1[0]",obj)); console.log("arr1[1].k: " + deeptest("arr1[1].k",obj)); console.log("arr1[1].j: " + deeptest("arr1[1].j",obj)); console.log("arr1[3]: " + deeptest("arr1[3]",obj)); console.log("arr2: " + deeptest("arr2",obj)); 

Agora também podemos usar reduce para percorrer as chaves aninhadas:

 // @params o // @params path expects 'obj.prop1.prop2.prop3' // returns: obj[path] value or 'false' if prop doesn't exist const objPropIfExists = o => path => { const levels = path.split('.'); const res = (levels.length > 0) ? levels.reduce((a, c) => a[c] || 0, o) : o[path]; return (!!res) ? res : false } const obj = { name: 'Name', sys: { country: 'AU' }, main: { temp: '34', temp_min: '13' }, visibility: '35%' } const exists = objPropIfExists(obj)('main.temp') const doesntExist = objPropIfExists(obj)('main.temp.foo.bar.baz') console.log(exists, doesntExist) 

Eu pensei em adicionar outro que eu inventei hoje. O motivo pelo qual tenho orgulho dessa solução é que ela evita parêntesis nesteds usados ​​em muitas soluções, como o Object Wrap (de Oliver Steele) :

(neste exemplo, eu uso um sublinhado como uma variável de espaço reservado, mas qualquer nome de variável funcionará)

 //the 'test' object var test = {level1: {level2: {level3: 'level3'}}}; let _ = test; if ((_=_.level1) && (_=_.level2) && (_=_.level3)) { let level3 = _; //do stuff with level3 } 

Você também pode usar a proposta de encadeamento opcional tc39 juntamente com o babel 7 – tc39-proposal-optional-chaining

Código ficaria assim:

  const test = test?.level1?.level2?.level3; if (test) alert(test); 

Há uma function aqui no thecodeabode (safeRead) que fará isso de uma forma segura … ou seja

 safeRead(test, 'level1', 'level2', 'level3'); 

se alguma propriedade for nula ou indefinida, uma string vazia será retornada

Com base em um comentário anterior , aqui está outra versão em que o object principal não pôde ser definido:

 // Supposing that our property is at first.second.third.property: var property = (((typeof first !== 'undefined' ? first : {}).second || {}).third || {}).property; 

Eu escrevi minha própria function que leva o caminho desejado e tem uma function de retorno de chamada boa e ruim.

 function checkForPathInObject(object, path, callbackGood, callbackBad){ var pathParts = path.split("."); var currentObjectPath = object; // Test every step to see if it exists in object for(var i=0; i<(pathParts.length); i++){ var currentPathPart = pathParts[i]; if(!currentObjectPath.hasOwnProperty(pathParts[i])){ if(callbackBad){ callbackBad(); } return false; } else { currentObjectPath = currentObjectPath[pathParts[i]]; } } // call full path in callback callbackGood(); } 

Uso:

 var testObject = { level1:{ level2:{ level3:{ } } } }; checkForPathInObject(testObject, "level1.level2.level3", function(){alert("good!")}, function(){alert("bad!")}); // good checkForPathInObject(testObject, "level1.level2.level3.levelNotThere", function(){alert("good!")}, function(){alert("bad!")}); //bad 
 //Just in case is not supported or not included by your framework //*************************************************** Array.prototype.some = function(fn, thisObj) { var scope = thisObj || window; for ( var i=0, j=this.length; i < j; ++i ) { if ( fn.call(scope, this[i], i, this) ) { return true; } } return false; }; //**************************************************** function isSet (object, string) { if (!object) return false; var childs = string.split('.'); if (childs.length > 0 ) { return !childs.some(function (item) { if (item in object) { object = object[item]; return false; } else return true; }); } else if (string in object) { return true; } else return false; } var object = { data: { item: { sub_item: { bla: { here : { iam: true } } } } } }; console.log(isSet(object,'data.item')); // true console.log(isSet(object,'x')); // false console.log(isSet(object,'data.sub_item')); // false console.log(isSet(object,'data.item')); // true console.log(isSet(object,'data.item.sub_item.bla.here.iam')); // true 

Eu estava procurando o valor a ser retornado se a propriedade existe, então modifiquei a resposta pelo CMS acima. Aqui está o que eu criei:

 function getNestedProperty(obj, key) { // Get property array from key string var properties = key.split("."); // Iterate through properties, returning undefined if object is null or property doesn't exist for (var i = 0; i < properties.length; i++) { if (!obj || !obj.hasOwnProperty(properties[i])) { return; } obj = obj[properties[i]]; } // Nested property found, so return the value return obj; } Usage: getNestedProperty(test, "level1.level2.level3") // "level3" getNestedProperty(test, "level1.level2.foo") // undefined 

Eu estava tendo o mesmo problema e queria ver se poderia criar minha própria solução. Isso aceita o caminho que você deseja verificar como uma string.

 function checkPathForTruthy(obj, path) { if (/\[[a-zA-Z_]/.test(path)) { console.log("Cannot resolve variables in property accessors"); return false; } path = path.replace(/\[/g, "."); path = path.replace(/]|'|"/g, ""); path = path.split("."); var steps = 0; var lastRef = obj; var exists = path.every(key => { var currentItem = lastRef[path[steps]]; if (currentItem) { lastRef = currentItem; steps++; return true; } else { return false; } }); return exists; } 

Aqui está um trecho com alguns casos de registro e teste:

 console.clear(); var testCases = [ ["data.Messages[0].Code", true], ["data.Messages[1].Code", true], ["data.Messages[0]['Code']", true], ['data.Messages[0]["Code"]', true], ["data[Messages][0]['Code']", false], ["data['Messages'][0]['Code']", true] ]; var path = "data.Messages[0].Code"; var obj = { data: { Messages: [{ Code: "0" }, { Code: "1" }] } } function checkPathForTruthy(obj, path) { if (/\[[a-zA-Z_]/.test(path)) { console.log("Cannot resolve variables in property accessors"); return false; } path = path.replace(/\[/g, "."); path = path.replace(/]|'|"/g, ""); path = path.split("."); var steps = 0; var lastRef = obj; var logOutput = []; var exists = path.every(key => { var currentItem = lastRef[path[steps]]; if (currentItem) { logOutput.push(currentItem); lastRef = currentItem; steps++; return true; } else { return false; } }); console.log(exists, logOutput); return exists; } testCases.forEach(testCase => { if (checkPathForTruthy(obj, testCase[0]) === testCase[1]) { console.log("Passed: " + testCase[0]); } else { console.log("Failed: " + testCase[0] + " expected " + testCase[1]); } }); 

Slight edit to this answer to allow nested arrays in the path

 var has = function (obj, key) { return key.split(".").every(function (x) { if (typeof obj != "object" || obj === null || !x in obj) return false; if (obj.constructor === Array) obj = obj[0]; obj = obj[x]; return true; }); }