Acessando Objetos JavaScript Aninhados com Chave de Cadeia

Eu tenho uma estrutura de dados como esta:

var someObject = { 'part1' : { 'name': 'Part 1', 'size': '20', 'qty' : '50' }, 'part2' : { 'name': 'Part 2', 'size': '15', 'qty' : '60' }, 'part3' : [ { 'name': 'Part 3A', 'size': '10', 'qty' : '20' }, { 'name': 'Part 3B', 'size': '5', 'qty' : '20' }, { 'name': 'Part 3C', 'size': '7.5', 'qty' : '20' } ] }; 

E gostaria de acessar os dados usando estas variables:

 var part1name = "part1.name"; var part2quantity = "part2.qty"; var part3name1 = "part3[0].name"; 

part1name deve ser preenchido com o valor de someObject.part1.name , que é “Parte 1”. Mesma coisa com part2quantity que preenchido com 60.

Existe alguma maneira de conseguir isso com javascript puro ou JQuery?

Acabei de fazer isso com base em algum código semelhante que eu já tinha, parece funcionar:

 Object.byString = function(o, s) { s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties s = s.replace(/^\./, ''); // strip a leading dot var a = s.split('.'); for (var i = 0, n = a.length; i < n; ++i) { var k = a[i]; if (k in o) { o = o[k]; } else { return; } } return o; } 

Uso::

 Object.byString(someObj, 'part3[0].name'); 

Veja uma demonstração de trabalho em http://jsfiddle.net/alnitak/hEsys/

EDIT alguns notaram que este código lançará um erro se passado uma corda onde os índices mais à esquerda não correspondem a uma input aninhada corretamente dentro do object. Esta é uma preocupação válida, mas IMHO é melhor tratada com um bloco try / catch ao chamar, em vez de ter essa function silenciosamente retornando undefined para um índice inválido.

Esta é a solução que eu uso:

 function resolve(path, obj=self, separator='.') { var properties = Array.isArray(path) ? path : path.split(separator) return properties.reduce((prev, curr) => prev && prev[curr], obj) } 

Exemplo de uso:

 // accessing property path on global scope resolve("document.body.style.width") // or resolve("style.width", document.body) // accessing array indexes // (someObject has been defined in the question) resolve("part3.0.size", someObject) // returns '10' // accessing non-existent properties // returns undefined when intermediate properties are not defined: resolve('properties.that.do.not.exist', {hello:'world'}) // accessing properties with unusual keys by changing the separator var obj = { object: { 'a.property.name.with.periods': 42 } } resolve('object->a.property.name.with.periods', obj, '->') // returns 42 // accessing properties with unusual keys by passing a property name array resolve(['object', 'a.property.name.with.periods'], obj) // returns 42 

Limitações:

  • Não é possível usar colchetes ( [] ) para índices de matriz – embora a especificação de índices de matriz entre o token separador (por exemplo . ) Funcione bem, conforme mostrado acima.

Isso agora é suportado pelo lodash usando _.get(obj, property) . Veja https://lodash.com/docs#get

Exemplo dos documentos:

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

Você teria que analisar a string você mesmo:

 function getProperty(obj, prop) { var parts = prop.split('.'); if (Array.isArray(parts)) { var last = parts.pop(), l = parts.length, i = 1, current = parts[0]; while((obj = obj[current]) && i < l) { current = parts[i]; i++; } if(obj) { return obj[last]; } } else { throw 'parts is not valid array'; } } 

Isso requer que você também defina índices de matriz com notação de ponto:

 var part3name1 = "part3.0.name"; 

Isso torna a análise mais fácil.

DEMO

Funciona para matrizes / matrizes dentro do object também. Defensiva contra valores inválidos.

 /** * Retrieve nested item from object/array * @param {Object|Array} obj * @param {String} path dot separated * @param {*} def default value ( if result undefined ) * @returns {*} */ function path(obj, path, def){ var i, len; for(i = 0,path = path.split('.'), len = path.length; i < len; i++){ if(!obj || typeof obj !== 'object') return def; obj = obj[path[i]]; } if(obj === undefined) return def; return obj; } ////////////////////////// // TEST // ////////////////////////// var arr = [true, {'sp ace': true}, true] var obj = { 'sp ace': true, arr: arr, nested: {'dotted.str.ing': true}, arr3: arr } shouldThrow(`path(obj, "arr.0")`); shouldBeDefined(`path(obj, "arr[0]")`); shouldBeEqualToNumber(`path(obj, "arr.length")`, 3); shouldBeTrue(`path(obj, "sp ace")`); shouldBeEqualToString(`path(obj, "none.existed.prop", "fallback")`, "fallback"); shouldBeTrue(`path(obj, "nested['dotted.str.ing'])`); 
  

ES6 : Apenas uma linha no Vanila JS (retorna null se não encontrar ao invés de dar erro):

 'path.string'.split('.').reduce((p,c)=>p&&p[c]||null, MyOBJ) 

ou exemplo:

 'abc'.split('.').reduce((p,c)=>p&&p[c]||null, {a:{b:{c:1}}}) 

Para uma function pronta para uso que também reconhece false, 0 e número negativo e aceita valores padrão como parâmetro:

 const resolvePath = (object, path, defaultValue) => path .split('.') .reduce((o, p) => o ? o[p] : defaultValue, object) 

Exemplo para usar:

 resolvePath(window,'document.body') =>  resolvePath(window,'document.body.xyz') => undefined resolvePath(window,'document.body.xyz', null) => null resolvePath(window,'document.body.xyz', 1) => 1 

Bônus :

Para definir um caminho (Solicitado por @ rob-gordon), você pode usar:

 const setPath = (object, path, value) => path .split('.') .reduce((o,p) => o[p] = path.split('.').pop() === p ? value : o[p] || {}, object) 

Exemplo:

 let myVar = {} setPath(myVar, 'abc', 42) => 42 console.log(myVar) => {a: {b: {c: 42}}} 

Acessar matriz com [] :

 const resolvePath = (object, path, defaultValue) => path .split(/[\.\[\]\'\"]/) .filter(p => p) .reduce((o, p) => o ? o[p] : defaultValue, object) 

exemplo

 const myVar = {a:{b:[{c:1}]}} resolvePath(myVar,'ab[0].c') => 1 resolvePath(myVar,'a["b"][\'0\'].c') => 1 

usando eval:

 var part1name = eval("someObject.part1.name"); 

wrap para retornar indefinido em erro

 function path(obj, path) { try { return eval("obj." + path); } catch(e) { return undefined; } } 

http://jsfiddle.net/shanimal/b3xTw/

Por favor, use o bom senso e caucanvas ao exercer o poder de eval. É um pouco como um sabre de luz, se você ligá-lo, há 90% de chance de você cortar um membro. Não é para todos.

Você pode conseguir obter o valor de um membro de object profundo com notação de ponto sem nenhuma biblioteca JavaScript externa com o truque simples a seguir:

 new Function('_', 'return _.' + path)(obj); 

No seu caso para obter o valor de part1.name de someObject apenas faça:

 new Function('_', 'return _.part1.name')(someObject); 

Aqui está uma demonstração de violino simples: https://jsfiddle.net/harishanchu/oq5esowf/

Eu acho que você está pedindo por isso:

 var part1name = someObject.part1.name; var part2quantity = someObject.part2.qty; var part3name1 = someObject.part3[0].name; 

Você poderia estar pedindo por isso:

 var part1name = someObject["part1"]["name"]; var part2quantity = someObject["part2"]["qty"]; var part3name1 = someObject["part3"][0]["name"]; 

Ambos irão funcionar


Ou talvez você esteja pedindo por isso

 var partName = "part1"; var nameStr = "name"; var part1name = someObject[partName][nameStr]; 

Finalmente você poderia estar pedindo por isso

 var partName = "part1.name"; var partBits = partName.split("."); var part1name = someObject[partBits[0]][partBits[1]]; 

Aqui eu ofereço mais maneiras, que parecem mais rápidas em muitos aspectos:

Opção 1: Dividir cadeia em. ou [ou] ou ‘ou “, inverta, pule itens vazios.

 function getValue(path, origin) { if (origin === void 0 || origin === null) origin = self ? self : this; if (typeof path !== 'string') path = '' + path; var parts = path.split(/\[|\]|\.|'|"/g).reverse(), name; // (why reverse? because it's usually faster to pop off the end of an array) while (parts.length) { name=parts.pop(); if (name) origin=origin[name]; } return origin; } 

Opção 2 (mais rápida de todas, exceto eval ): Varredura de caracteres de baixo nível (sem regex / split / etc, apenas uma varredura de caractere rápida). Nota: Este não suporta cotações para índices.

 function getValue(path, origin) { if (origin === void 0 || origin === null) origin = self ? self : this; if (typeof path !== 'string') path = '' + path; var c = '', pc, i = 0, n = path.length, name = ''; if (n) while (i< =n) ((c = path[i++]) == '.' || c == '[' || c == ']' || c == void 0) ? (name?(origin = origin[name], name = ''):(pc=='.'||pc=='['||pc==']'&&c==']'?i=n+2:void 0),pc=c) : name += c; if (i==n+2) throw "Invalid path: "+path; return origin; } // (around 1,000,000+/- ops/sec) 

Opção 3: ( novo : opção 2 expandida para oferecer suporte a cotações - um pouco mais lenta, mas ainda assim rápida)

 function getValue(path, origin) { if (origin === void 0 || origin === null) origin = self ? self : this; if (typeof path !== 'string') path = '' + path; var c, pc, i = 0, n = path.length, name = '', q; while (i< =n) ((c = path[i++]) == '.' || c == '[' || c == ']' || c == "'" || c == '"' || c == void 0) ? (c==q&&path[i]==']'?q='':q?name+=c:name?(origin?origin=origin[name]:i=n+2,name='') : (pc=='['&&(c=='"'||c=="'")?q=c:pc=='.'||pc=='['||pc==']'&&c==']'||pc=='"'||pc=="'"?i=n+2:void 0), pc=c) : name += c; if (i==n+2 || name) throw "Invalid path: "+path; return origin; } 

JSPerf: http://jsperf.com/ways-to-dereference-a-delimited-structure-string/3

"eval (...)" ainda é rei (desempenho sábio). Se você tem caminhos de propriedade diretamente sob seu controle, não deve haver nenhum problema com o uso de 'eval' (especialmente se a velocidade for desejada). Se puxar caminhos de propriedade "sobre o fio" ( na linha ! Lol: P), então sim, use outra coisa para ser seguro. Apenas um idiota diria para nunca usar "eval", pois há boas razões para usá-lo. Além disso, "é usado no analisador JSON de Doug Crockford ". Se a input é segura, então nenhum problema. Use a ferramenta certa para o trabalho certo, é isso.

A abordagem do Speigg é muito simples e limpa, embora eu tenha encontrado essa resposta enquanto procurava a solução de acessar as propriedades do escopo AngularJS $ pelo caminho da string e com uma pequena modificação ele faz o trabalho:

 $scope.resolve = function( path, obj ) { return path.split('.').reduce( function( prev, curr ) { return prev[curr]; }, obj || this ); } 

Basta colocar essa function no seu controlador raiz e usá-lo em qualquer escopo filho assim:

 $scope.resolve( 'path.to.any.object.in.scope') 

É um forro com lodash.

 const deep = { l1: { l2: { l3: "Hello" } } }; const prop = "l1.l2.l3"; const val = _.reduce(prop.split('.'), function(result, value) { return result ? result[value] : undefined; }, deep); // val === "Hello" 

Ou melhor ainda …

 const val = _.get(deep, prop); 

Ou versão ES6 com redução …

 const val = prop.split('.').reduce((r, val) => { return r ? r[val] : undefined; }, deep); 

Plunkr

Eu ainda não encontrei um pacote para fazer todas as operações com um caminho de string, então acabei escrevendo meu próprio pequeno pacote que suporta insert (), get () (com retorno padrão), set () e remove ( ) operações.

Você pode usar a notação de ponto, colchetes, índices numéricos, propriedades de números de seqüência de caracteres e chaves com caracteres não pertencentes à palavra. Uso simples abaixo:

 > var jsocrud = require('jsocrud'); ... // Get (Read) --- > var obj = { > foo: [ > { > 'key w/ non-word chars': 'bar' > } > ] > }; undefined > jsocrud.get(obj, '.foo[0]["key w/ non-word chars"]'); 'bar' 

https://www.npmjs.com/package/jsocrud

https://github.com/vertical-knowledge/jsocrud

Função simples, permitindo um caminho de string ou array.

 function get(obj, path) { if(typeof path === 'string') path = path.split('.'); if(path.length === 0) return obj; return get(obj[path[0]], path.slice(1)); } const obj = {a: {b: {c: 'foo'}}}; console.log(get(obj, 'abc')); //foo 

OU

 console.log(get(obj, ['a', 'b', 'c'])); //foo 

Existe um módulo npm agora para fazer isso: https://github.com/erictrinh/safe-access

Exemplo de uso:

 var access = require('safe-access'); access(very, 'nested.property.and.array[0]'); 
 /** * Access a deep value inside a object * Works by passing a path like "foo.bar", also works with nested arrays like "foo[0][1].baz" * @author Victor B. https://gist.github.com/victornpb/4c7882c1b9d36292308e * Unit tests: http://jsfiddle.net/Victornpb/0u1qygrh/ */ function getDeepVal(obj, path) { if (typeof obj === "undefined" || obj === null) return; path = path.split(/[\.\[\]\"\']{1,2}/); for (var i = 0, l = path.length; i < l; i++) { if (path[i] === "") continue; obj = obj[path[i]]; if (typeof obj === "undefined" || obj === null) return; } return obj; } 

Funciona com

 getDeepVal(obj,'foo.bar') getDeepVal(obj,'foo.1.bar') getDeepVal(obj,'foo[0].baz') getDeepVal(obj,'foo[1][2]') getDeepVal(obj,"foo['bar'].baz") getDeepVal(obj,"foo['bar']['baz']") getDeepVal(obj,"foo.bar.0.baz[1]['2']['w'].aaa[\"f\"].bb") 

Isso provavelmente nunca verá a luz do dia … mas aqui está mesmo assim.

  1. Substitua a syntax do colchete [] por .
  2. Dividir em ' personagem
  3. Encontre o caminho (de outra forma undefined )
 // "one liner" (ES6) const deep_value = (obj, path) => path .replace(/\[|\]\.?/g, '.') .split('.') .reduce((acc, val) => acc && acc[val], obj); // ... and that's it. var someObject = { 'part1' : { 'name': 'Part 1', 'size': '20', 'qty' : '50' }, 'part2' : { 'name': 'Part 2', 'size': '15', 'qty' : '60' }, 'part3' : [ { 'name': 'Part 3A', 'size': '10', 'qty' : '20' } // ... ] }; console.log(deep_value(someObject, "part1.name")); // Part 1 console.log(deep_value(someObject, "part2.qty")); // 60 console.log(deep_value(someObject, "part3[0].name")); // Part 3A 

Apenas no caso, alguém está visitando esta questão em 2017 ou mais tarde e procurando uma maneira fácil de lembrar , aqui está uma elaborada postagem no blog sobre como acessar objects nesteds em JavaScript sem ser enganada por

Não é possível ler a propriedade ‘foo’ de erro indefinido

Acessar Objetos Aninhados Usando Redução de Matriz

Vamos pegar essa estrutura de exemplo

 const user = { id: 101, email: 'jack@dev.com', personalInfo: { name: 'Jack', address: [{ line1: 'westwish st', line2: 'washmasher', city: 'wallas', state: 'WX' }] } } 

Para poder acessar matrizes aninhadas, você pode escrever sua própria matriz, reduzir util.

 const getNestedObject = (nestedObj, pathArr) => { return pathArr.reduce((obj, key) => (obj && obj[key] !== 'undefined') ? obj[key] : undefined, nestedObj); } // pass in your object structure as array elements const name = getNestedObject(user, ['personalInfo', 'name']); // to access nested array, just pass in array index as an element the path array. const city = getNestedObject(user, ['personalInfo', 'address', 0, 'city']); // this will return the city from the first address item. 

Há também um excelente tipo de manipulação de biblioteca mínima que faz tudo isso para você.

Com o typy, seu código ficará assim

 const city = t(user, 'personalInfo.address[0].city').safeObject; 

Isenção de responsabilidade: Eu sou o autor deste pacote.

Se você precisar acessar uma chave aninhada diferente sem conhecê-la na hora da codificação (será trivial endereçá-las) você pode usar o acessador de notação de matriz:

 var part1name = someObject['part1']['name']; var part2quantity = someObject['part2']['qty']; var part3name1 = someObject['part3'][0]['name']; 

Eles são equivalentes ao assessor de notação de ponto e podem variar no tempo de execução, por exemplo:

 var part = 'part1'; var property = 'name'; var part1name = someObject[part][property]; 

é equivalente a

 var part1name = someObject['part1']['name']; 

ou

 var part1name = someObject.part1.name; 

Espero que isso resolva sua pergunta …

EDITAR

Não usarei uma string para manter um tipo de consulta do xpath para acessar um valor de object. Como você tem que chamar uma function para analisar a consulta e recuperar o valor, eu seguiria outro caminho (não:

 var part1name = function(){ return this.part1.name; } var part2quantity = function() { return this['part2']['qty']; } var part3name1 = function() { return this.part3[0]['name'];} // usage: part1name.apply(someObject); 

ou, se você estiver incomodado com o método apply

 var part1name = function(obj){ return obj.part1.name; } var part2quantity = function(obj) { return obj['part2']['qty']; } var part3name1 = function(obj) { return obj.part3[0]['name'];} // usage: part1name(someObject); 

As funções são mais curtas, mais claras, o intérprete as verifica por erros de syntax e assim por diante.

A propósito, sinto que uma tarefa simples feita na hora certa será suficiente …

Apenas tive a mesma pergunta recentemente e usei com sucesso https://npmjs.org/package/tea-properties que também set objects / matrizes nesteds:

obter:

 var o = { prop: { arr: [ {foo: 'bar'} ] } }; var properties = require('tea-properties'); var value = properties.get(o, 'prop.arr[0].foo'); assert(value, 'bar'); // true 

conjunto:

 var o = {}; var properties = require('tea-properties'); properties.set(o, 'prop.arr[0].foo', 'bar'); assert(o.prop.arr[0].foo, 'bar'); // true 

As soluções aqui são apenas para acessar as chaves profundamente aninhadas. Eu precisava de um para acessar, adicionar, modificar e excluir as chaves. Isto é o que eu criei:

 var deepAccessObject = function(object, path_to_key, type_of_function, value){ switch(type_of_function){ //Add key/modify key case 0: if(path_to_key.length === 1){ if(value) object[path_to_key[0]] = value; return object[path_to_key[0]]; }else{ if(object[path_to_key[0]]) return deepAccessObject(object[path_to_key[0]], path_to_key.slice(1), type_of_function, value); else object[path_to_key[0]] = {}; } break; //delete key case 1: if(path_to_key.length === 1){ delete object[path_to_key[0]]; return true; }else{ if(object[path_to_key[0]]) return deepAccessObject(object[path_to_key[0]], path_to_key.slice(1), type_of_function, value); else return false; } break; default: console.log("Wrong type of function"); } }; 
  • path_to_key : caminho em uma matriz. Você pode substituí-lo pelo seu string_path.split(".") .
  • type_of_function : 0 para acessar (não passa qualquer valor para value ), 0 para adicionar e modificar. 1 para excluir.

Em vez de uma string, um array pode ser usado para endereçar objects nesteds e matrizes, por exemplo: ["my_field", "another_field", 0, "last_field", 10]

Aqui está um exemplo que mudaria um campo com base nessa representação de matriz. Eu estou usando algo assim em react.js para campos de input controlados que alteram o estado de estruturas aninhadas.

 let state = { test: "test_value", nested: { level1: "level1 value" }, arr: [1, 2, 3], nested_arr: { arr: ["buh", "bah", "foo"] } } function handleChange(value, fields) { let update_field = state; for(var i = 0; i < fields.length - 1; i++){ update_field = update_field[fields[i]]; } update_field[fields[fields.length-1]] = value; } handleChange("update", ["test"]); handleChange("update_nested", ["nested","level1"]); handleChange(100, ["arr",0]); handleChange('changed_foo', ["nested_arr", "arr", 3]); console.log(state); 

Com base em uma resposta anterior, criei uma function que também pode manipular colchetes. Mas não há pontos dentro deles devido à divisão.

 function get(obj, str) { return str.split(/\.|\[/g).map(function(crumb) { return crumb.replace(/\]$/, '').trim().replace(/^(["'])((?:(?!\1)[^\\]|\\.)*?)\1$/, (match, quote, str) => str.replace(/\\(\\)?/g, "$1")); }).reduce(function(obj, prop) { return obj ? obj[prop] : undefined; }, obj); } 

Enquanto reduzir é bom, estou surpreso que ninguém usou para Each:

 function valueForKeyPath(obj, path){ const keys = path.split('.'); keys.forEach((key)=> obj = obj[key]); return obj; }; 

Teste

 // (IE9+) Two steps var pathString = "[0]['property'].others[3].next['final']"; var obj = [{ property: { others: [1, 2, 3, { next: { final: "SUCCESS" } }] } }]; // Turn string to path array var pathArray = pathString .replace(/\[["']?([\w]+)["']?\]/g,".$1") .split(".") .splice(1); // Add object prototype method Object.prototype.path = function (path) { try { return [this].concat(path).reduce(function (f, l) { return f[l]; }); } catch (e) { console.error(e); } }; // usage console.log(obj.path(pathArray)); console.log(obj.path([0,"doesNotExist"])); 

Trabalhando com a property ou propertyOf Underscore :

 var test = { foo: { bar: { baz: 'hello' } } } var string = 'foo.bar.baz'; // document.write(_.propertyOf(test)(string.split('.'))) document.write(_.property(string.split('.'))(test)); 
  

E esta solução:

 setJsonValue: function (json, field, val) { if (field !== undefined){ try { eval("json." + field + " = val"); } catch(e){ ; } } } 

E este aqui, para obter:

 getJsonValue: function (json, field){ var value = undefined; if (field !== undefined) { try { eval("value = json." + field); } catch(e){ ; } } return value; }; 

Provavelmente alguns irão considerá-los inseguros, mas eles devem ser muito mais rápidos, analisando a sequência.

Construindo fora da resposta de Alnitak:

 if(!Object.prototype.byString){ //NEW byString which can update values Object.prototype.byString = function(s, v, o) { var _o = o || this; s = s.replace(/\[(\w+)\]/g, '.$1'); // CONVERT INDEXES TO PROPERTIES s = s.replace(/^\./, ''); // STRIP A LEADING DOT var a = s.split('.'); //ARRAY OF STRINGS SPLIT BY '.' for (var i = 0; i < a.length; ++i) {//LOOP OVER ARRAY OF STRINGS var k = a[i]; if (k in _o) {//LOOP THROUGH OBJECT KEYS if(_o.hasOwnProperty(k)){//USE ONLY KEYS WE CREATED if(v !== undefined){//IF WE HAVE A NEW VALUE PARAM if(i === a.length -1){//IF IT'S THE LAST IN THE ARRAY _o[k] = v; } } _o = _o[k];//NO NEW VALUE SO JUST RETURN THE CURRENT VALUE } } else { return; } } return _o; }; 

}

Isso permite que você defina um valor também!

Eu criei um pacote npm e github com isso também