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.
Parece que o lodash adicionou _.get
para todas as suas necessidades aninhadas.
_.get(countries, 'greece.sparta.playwright')
Os usuários do lodash podem desfrutar do lodash.contrib, que possui alguns methods que atenuam esse problema .
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';
const propExists = (obj, path) => { return !!path.split('.').reduce((obj, prop) => { return obj && obj[prop] ? obj[prop] : undefined; }, obj) }
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 } } };
var u = undefined; oa ? oab ? oabc : u : u // 1 ox ? oxy ? oxyz : u : u // undefined (o = oa) ? (o = ob) ? oc : u : u // 1
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
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; }); }