Converter string JavaScript em notação de ponto em uma referência de object

Dado um Objeto JS: var obj = { a: { b: '1', c: '2' } } e uma string "ab" como eu posso converter a string para notação de ponto para que eu possa ir: var val = obj.ab ;

Se a string era apenas ‘a’ eu posso usar obj[a] mas isso é mais complexo. Eu imagino que há algum método simples, mas ele escapa no presente.

Nota recente: Embora eu esteja lisonjeado com o fato de que essa resposta tenha obtido muitos votos positivos, também estou um pouco horrorizada. Se alguém precisar converter strings de notação de ponto como “xabc” em referências, é provavelmente um sinal de que há algo muito errado acontecendo (a menos que você esteja realizando alguma estranha desserialização). É um exagero porque é uma metaprogramação desnecessária e também viola um pouco o estilo funcional de codificação sem efeitos colaterais. Além disso, espere acertos de desempenho massivos também se você fizer isso mais do que precisa (por exemplo, como a forma padrão do seu aplicativo de passar objects ao redor e desreferenciando-os). Se, por alguma razão, isso for um js do lado do servidor, o usual é o caso da sanitização de insumos. Os novatos que encontrarem o caminho para essa resposta devem considerar trabalhar com representações de matriz, por exemplo, [‘x’, ‘a’, ‘b’, ‘c’], ou até mesmo algo mais direto / simples / direto, se possível, como não perder rastreio das próprias referências, ou talvez algum ID único preexistente, etc.

Aqui está um elegante one-liner que é 10x menor que as outras soluções:

 function index(obj,i) {return obj[i]} 'abetc'.split('.').reduce(index, obj) 

[edit] Ou no ECMAScript 6:

 'abetc'.split('.').reduce((o,i)=>o[i], obj) 

(Não que eu pense que eval sempre é ruim como os outros sugerem que é (embora geralmente seja), no entanto, essas pessoas ficarão contentes que este método não use eval. O acima irá encontrar obj.abetc dado obj e a string "abetc" .)

Em resposta àqueles que ainda têm medo de usar reduce apesar de estarem no padrão ECMA-262 (5ª edição), aqui está uma implementação recursiva de duas linhas:

 function multiIndex(obj,is) { // obj,['1','2','3'] -> ((obj['1'])['2'])['3'] return is.length ? multiIndex(obj[is[0]],is.slice(1)) : obj } function pathIndex(obj,is) { // obj,'1.2.3' -> multiIndex(obj,['1','2','3']) return multiIndex(obj,is.split('.')) } pathIndex('abetc') 

Dependendo das otimizações que o compilador JS está fazendo, você pode querer certificar-se de que quaisquer funções aninhadas não sejam redefinidas em cada chamada pelos methods usuais (colocando-as em um fechamento, object ou espaço de nomes global).

editar :

Para responder uma questão interessante nos comentários:

como você transformaria isso em um setter também? Não apenas retornando os valores por caminho, mas também configurando-os se um novo valor for enviado para a function? – Swader 28 de junho às 21:42

(sidenote: infelizmente não pode retornar um object com um Setter, pois isso violaria a convenção de chamada; comentarista parece estar se referindo a uma function estilo setter geral com efeitos colaterais como index(obj,"abetc", value) fazendo obj.abetc = value .)

O estilo de reduce não é realmente adequado para isso, mas podemos modificar a implementação recursiva:

 function index(obj,is, value) { if (typeof is == 'string') return index(obj,is.split('.'), value); else if (is.length==1 && value!==undefined) return obj[is[0]] = value; else if (is.length==0) return obj; else return index(obj[is[0]],is.slice(1), value); } 

Demonstração:

 > obj = {a:{b:{etc:5}}} > index(obj,'abetc') 5 > index(obj,['a','b','etc']) #works with both strings and lists 5 > index(obj,'abetc', 123) #setter-mode - third argument (possibly poor form) 123 > index(obj,'abetc') 123 

… pessoalmente, eu recomendo fazer uma function separada setIndex(...) . Eu gostaria de terminar em uma nota lateral que o poser original da questão poderia (deveria?) .split trabalhando com matrizes de índices (que eles podem obter de .split ), ao invés de strings; embora geralmente não haja nada de errado com uma function de conveniência.


Um comentarista perguntou:

o que acontece com matrizes? algo como “ab [4] .cd [1] [2] [3]”? –AlexS

Javascript é uma linguagem muito estranha; em geral objects só podem ter strings como suas chaves de propriedade, assim, por exemplo, se x era um object genérico como x={} , então x[1] se tornaria x["1"] … você leu certo … Sim…

JavaScript Arrays (que são instâncias de Object) encorajam especificamente chaves inteiras, mesmo que você possa fazer algo como x=[]; x["puppy"]=5; x=[]; x["puppy"]=5; .

Mas em geral (e há exceções), x["somestring"]===x.somestring (quando é permitido, você não pode fazer x.123 ).

(Lembre-se de que, qualquer que seja o compilador JS que você esteja usando, pode escolher, talvez, compilá-los para representações melhores se puder provar que isso não violaria a especificação.)

Portanto, a resposta à sua pergunta dependeria de você estar assumindo que esses objects só aceitam inteiros (devido a uma restrição no domínio do problema) ou não. Vamos supor que não. Em seguida, uma expressão válida é uma concatenação de um identificador de base mais alguns .identifier s mais alguns ["stringindex"] s

Isto seria então equivalente a a["b"][4]["c"]["d"][1][2][3] , embora provavelmente devêssemos também suportar ab["c\"validjsstringliteral"][3] Você teria que verificar a seção de gramática ecmascript em literais de string para ver como analisar um literal de string válido.Tecicamente, você também gostaria de verificar (ao contrário de minha primeira resposta) que a é um identificador de javascript válido.

Uma resposta simples à sua pergunta, no entanto, se as suas strings não contiverem vírgulas ou parênteses , seria apenas combinar sequências de comprimento 1+ de caracteres que não estejam no conjunto , ou [ ou ] :

 > "abc[4].c.def[1][2][\"gh\"]".match(/[^\]\[.]+/g) // ^^^ ^ ^ ^^^ ^ ^ ^^^^^ ["abc", "4", "c", "def", "1", "2", ""gh""] 

Se suas strings não contiverem caracteres de escape ou " caracteres " , e como IdentifierNames é uma sublinguagem de StringLiterals (eu acho ???), você pode primeiro converter seus pontos em []:

 > var R=[], demoString="abc[4].c.def[1][2][\"gh\"]"; > for(var match,matcher=/^([^\.\[]+)|\.([^\.\[]+)|\["([^"]+)"\]|\[(\d+)\]/g; match=matcher.exec(demoString); ) { R.push(Array.from(match).slice(1).filter(x=>x!==undefined)[0]); // extremely bad code because js regexes are weird, don't use this } > R ["abc", "4", "c", "def", "1", "2", "gh"] 

Claro, sempre tenha cuidado e nunca confie em seus dados. Algumas maneiras ruins de fazer isso que podem funcionar em alguns casos de uso também incluem:

 // hackish/wrongish; preprocess your string into "ab4.cd1.2.3", eg: > yourstring.replace(/]/g,"").replace(/\[/g,".").split(".") "ab4.cd1.2.3" //use code from before 

Se você pode usar o lodash , existe uma function, que faz exatamente isso:

_.get (object, caminho, [defaultValue])

 var val = _.get(obj, "ab"); 

Um exemplo um pouco mais envolvido com recursion.

 function recompose(obj,string){ var parts = string.split('.'); var newObj = obj[parts[0]]; if(parts[1]){ parts.splice(0,1); var newString = parts.join('.'); return recompose(newObj,newString); } return newObj; } var obj = { a: { b: '1', c: '2', d:{a:{b:'blah'}}}}; alert(recompose(obj,'adab')); //blah 

Se você espera desreferenciar o mesmo caminho muitas vezes, construir uma function para cada caminho de notação de ponto realmente tem o melhor desempenho de longe (expandindo os testes de desempenho que James Wilkins vinculou nos comentários acima).

 var path = 'abx'; var getter = new Function("obj", "return obj." + path + ";"); getter(obj); 

Usar o construtor Function tem algumas das mesmas desvantagens de eval () em termos de segurança e desempenho de pior caso, mas o IMO é uma ferramenta mal usada para casos em que você precisa de uma combinação de extremo dinamismo e alto desempenho. Eu uso essa metodologia para criar funções de filtro de matriz e chamá-las dentro de um loop digest AngularJS. Meus perfis mostram consistentemente a etapa array.filter () levando menos de 1 ms para remover a referência e filtrar cerca de 2.000 objects complexos, usando caminhos definidos dinamicamente em 3-4 níveis de profundidade.

Uma metodologia similar poderia ser usada para criar funções de setter, é claro:

 var setter = new Function("obj", "newval", "obj." + path + " = newval;"); setter(obj, "some new val"); 
 var a = { b: { c: 9 } }; function value(layer, path, value) { var i = 0, path = path.split('.'); for (; i < path.length; i++) if (value != null && i + 1 === path.length) layer[path[i]] = value; layer = layer[path[i]]; return layer; }; value(a, 'b.c'); // 9 value(a, 'b.c', 4); value(a, 'b.c'); // 4 

Isso é um monte de código quando comparado com a maneira muito mais simples de fazer isso, mas, como Simon Willison diz, você nunca deve usar o eval .

Além disso, JSFiddle .

Outras propostas são um pouco enigmáticas, então eu pensei em contribuir:

 Object.prop = function(obj, prop, val){ var props = prop.split('.') , final = props.pop(), p while(p = props.shift()){ if (typeof obj[p] === 'undefined') return undefined; obj = obj[p] } return val ? (obj[final] = val) : obj[final] } var obj = { a: { b: '1', c: '2' } } // get console.log(Object.prop(obj, 'a.c')) // -> 2 // set Object.prop(obj, 'a.c', function(){}) console.log(obj) // -> { a: { b: '1', c: [Function] } } 

Eu estendi a resposta elegante por ninjagecko para que a function lide com referências de estilo de matriz e / ou pontilhadas e para que uma seqüência vazia faça com que o object pai seja retornado.

Aqui está:

 string_to_ref = function (object, reference) { function arr_deref(o, ref, i) { return !ref ? o : (o[ref.slice(0, i ? -1 : ref.length)]) } function dot_deref(o, ref) { return ref.split('[').reduce(arr_deref, o); } return !reference ? object : reference.split('.').reduce(dot_deref, object); }; 

Veja o meu exemplo de trabalho jsFiddle aqui: http://jsfiddle.net/sc0ttyd/q7zyd/

Note que se você já estiver usando Lodash você pode usar a property ou get funções:

 var obj = { a: { b: '1', c: '2' } }; _.property('a.b')(obj); // => 1 _.get(obj, 'a.b'); // => 1 

O sublinhado também tem uma function de property , mas não suporta notação de ponto.

Muitos anos desde o post original. Agora existe uma ótima biblioteca chamada ‘caminho do object’. https://github.com/mariocasciaro/object-path

Disponível no NPM e BOWER https://www.npmjs.com/package/object-path

É tão fácil como:

 objectPath.get(obj, "ac1"); //returns "f" objectPath.set(obj, "aj0.f", "m"); 

E funciona para propriedades e matrizes profundamente aninhadas.

Sugiro dividir o caminho e iterar e reduzir o object que você tem. Esta proposta funciona com um valor padrão para propriedades ausentes.

 function getValue(object, keys) { return keys.split('.').reduce(function (o, k) { return (o || {})[k]; }, object); } console.log(getValue({ a: { b: '1', c: '2' } }, 'a.b')); console.log(getValue({ a: { b: '1', c: '2' } }, 'foo.bar.baz')); 

você também pode usar o lodash.get

Você acabou de instalar este pacote (npm i –save lodash.get) e depois usá-lo assim:

 const get = require('lodash.get'); const myObj = { user: { firstName: 'Stacky', lastName: 'Overflowy' }, id: 123 }; console.log(get(myObj, 'user.firstName')); // prints Stacky console.log(get(myObj, 'id')); //prints 123 //You can also update values get(myObj, 'user').firstName = John; 
 var find = function(root, path) { var segments = path.split('.'), cursor = root, target; for (var i = 0; i < segments.length; ++i) { target = cursor[segments[i]]; if (typeof target == "undefined") return void 0; cursor = target; } return cursor; }; var obj = { a: { b: '1', c: '2' } } find(obj, "ab"); // 1 var set = function (root, path, value) { var segments = path.split('.'), cursor = root, target; for (var i = 0; i < segments.length - 1; ++i) { cursor = cursor[segments[i]] || { }; } cursor[segments[segments.length - 1]] = value; }; set(obj, "ak", function () { console.log("hello world"); }); find(obj, "ak")(); // hello world 

Você pode obter o valor de um membro do object pela notação de ponto com uma única linha de código:

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

No seu caso:

 var obj = { a: { b: '1', c: '2' } } var val = new Function('_', 'return _.a.b')(obj); 

Para simplificar, você pode escrever uma function como esta:

 function objGet(obj, path){ return new Function('_', 'return _.' + path)(obj); } 

Explicação:

O construtor Function cria um novo object Function. Em JavaScript, cada function é, na verdade, um object Function. Sintaxe para criar uma function explicitamente com o construtor Function é:

 new Function ([arg1[, arg2[, ...argN]],] functionBody) 

em que arguments(arg1 to argN) deve ser uma string que corresponda a um identificador javaScript válido e functionBody é uma string contendo as instruções javaScript que compõem a definição da function.

No nosso caso, aproveitamos o corpo da function string para recuperar o membro do object com a notação de ponto.

Espero que ajude.

Copiei o seguinte da resposta de Ricardo Tomasi e modifiquei também para criar sub-objects que ainda não existem conforme necessário. É um pouco menos eficiente (mais if e criação de objects vazios), mas deve ser muito bom.

Além disso, nos permitirá fazer Object.prop(obj, 'a.b', false) onde não poderíamos antes. Infelizmente, ainda não nos deixa atribuir undefined … Não tenho certeza como ir sobre isso ainda.

 /** * Object.prop() * * Allows dot-notation access to object properties for both getting and setting. * * @param {Object} obj The object we're getting from or setting * @param {string} prop The dot-notated string defining the property location * @param {mixed} val For setting only; the value to set */ Object.prop = function(obj, prop, val){ var props = prop.split('.'), final = props.pop(), p; for (var i = 0; i < props.length; i++) { p = props[i]; if (typeof obj[p] === 'undefined') { // If we're setting if (typeof val !== 'undefined') { // If we're not at the end of the props, keep adding new empty objects if (i != props.length) obj[p] = {}; } else return undefined; } obj = obj[p] } return typeof val !== "undefined" ? (obj[final] = val) : obj[final] } 

Resposta GET / SET que também funciona no reagir nativo (você não pode atribuir ao Object.prototype atualmente):

 Object.defineProperty(Object.prototype, 'getNestedProp', { value: function(desc) { var obj = this; var arr = desc.split("."); while(arr.length && (obj = obj[arr.shift()])); return obj; }, enumerable: false }); Object.defineProperty(Object.prototype, 'setNestedProp', { value: function(desc, value) { var obj = this; var arr = desc.split("."); var last = arr.pop(); while(arr.length && (obj = obj[arr.shift()])); obj[last] = value; }, enumerable: false }); 

Uso:

 var a = { values: [{ value: null }] }; var b = { one: { two: 'foo' } }; a.setNestedProp('values.0.value', b.getNestedProp('one.two')); console.log(a.values[0].value); // foo 

Aqui está o meu código sem usar eval . É fácil entender também.

 function value(obj, props) { if (!props) return obj; var propsArr = props.split('.'); var prop = propsArr.splice(0, 1); return value(obj[prop], propsArr.join('.')); } var obj = { a: { b: '1', c: '2', d:{a:{b:'blah'}}}}; console.log(value(obj, 'adab')); //returns blah 

Sim, foi perguntado há 4 anos e sim, estender os protótipos básicos geralmente não é uma boa idéia, mas, se você mantiver todas as extensões em um só lugar, elas podem ser úteis.
Então, aqui está o meu jeito de fazer isso.

  Object.defineProperty(Object.prototype, "getNestedProperty", { value : function (propertyName) { var result = this; var arr = propertyName.split("."); while (arr.length && result) { result = result[arr.shift()]; } return result; }, enumerable: false }); 

Agora você poderá obter propriedades aninhadas em todos os lugares sem importar o módulo com function ou function copiar / colar.

UPD.Example:

 {a:{b:11}}.getNestedProperty('a.b'); //returns 11 

UPD 2. Próxima extensão brokes mongoose no meu projeto. Também li que poderia quebrar jquery. Então, nunca faça isso no próximo caminho

  Object.prototype.getNestedProperty = function (propertyName) { var result = this; var arr = propertyName.split("."); while (arr.length && result) { result = result[arr.shift()]; } return result; }; 

Aqui está minha implementação

Implementação 1

 Object.prototype.access = function() { var ele = this[arguments[0]]; if(arguments.length === 1) return ele; return ele.access.apply(ele, [].slice.call(arguments, 1)); } 

Implementação 2 (usando matriz reduzir em vez de fatia)

 Object.prototype.access = function() { var self = this; return [].reduce.call(arguments,function(prev,cur) { return prev[cur]; }, self); } 

Exemplos:

 var myobj = {'a':{'b':{'c':{'d':'abcd','e':[11,22,33]}}}}; myobj.access('a','b','c'); // returns: {'d':'abcd', e:[0,1,2,3]} myobj.abaccess('c','d'); // returns: 'abcd' myobj.access('a','b','c','e',0); // returns: 11 

ele também pode manipular objects dentro de matrizes como para

 var myobj2 = {'a': {'b':[{'c':'ab0c'},{'d':'ab1d'}]}} myobj2.access('a','b','1','d'); // returns: 'ab1d' 

Esta é a minha solução estendida proposta por: ninjagecko

Para mim, a notação de strings simples não era suficiente, portanto, a versão abaixo suporta coisas como:

index (obj, ‘data.accounts [0] .address [0] .postcode’);

 /** * Get object by index * @supported * - arrays supported * - array indexes supported * @not-supported * - multiple arrays * @issues: * index(myAccount, 'accounts[0].address[0].id') - works fine * index(myAccount, 'accounts[].address[0].id') - doesnt work * @Example: * index(obj, 'data.accounts[].id') => returns array of id's * index(obj, 'data.accounts[0].id') => returns id of 0 element from array * index(obj, 'data.accounts[0].addresses.list[0].id') => error * @param obj * @param path * @returns {any} */ var index = function(obj, path, isArray?, arrIndex?){ // is an array if(typeof isArray === 'undefined') isArray = false; // array index, // if null, will take all indexes if(typeof arrIndex === 'undefined') arrIndex = null; var _arrIndex = null; var reduceArrayTag = function(i, subArrIndex){ return i.replace(/(\[)([\d]{0,})(\])/, (i) => { var tmp = i.match(/(\[)([\d]{0,})(\])/); isArray = true; if(subArrIndex){ _arrIndex = (tmp[2] !== '') ? tmp[2] : null; }else{ arrIndex = (tmp[2] !== '') ? tmp[2] : null; } return ''; }); } function byIndex(obj, i) { // if is an array if(isArray){ isArray = false; i = reduceArrayTag(i, true); // if array index is null, // return an array of with values from every index if(!arrIndex){ var arrValues = []; _.forEach(obj, (el) => { arrValues.push(index(el, i, isArray, arrIndex)); }) return arrValues; } // if array index is specified var value = obj[arrIndex][i]; if(isArray){ arrIndex = _arrIndex; }else{ arrIndex = null; } return value; }else{ // remove [] from notation, // if [] has been removed, check the index of array i = reduceArrayTag(i, false); return obj[i] } } // reduce with byIndex method return path.split('.').reduce(byIndex, obj) } 

Correndo o risco de vencer um cavalo morto … Eu acho isso mais útil ao atravessar objects nesteds para referenciar onde você está com relação ao object base ou a um object similar com a mesma estrutura. Para esse fim, isso é útil com uma function de passagem de object nested. Observe que usei uma matriz para manter o caminho. Seria trivial modificar isso para usar um caminho de string ou um array. Observe também que você pode atribuir “indefinido” ao valor, ao contrário de algumas das outras implementações.

 /* * Traverse each key in a nested object and call fn(curObject, key, value, baseObject, path) * on each. The path is an array of the keys required to get to curObject from * baseObject using objectPath(). If the call to fn() returns falsey, objects below * curObject are not traversed. Should be called as objectTaverse(baseObject, fn). * The third and fourth arguments are only used by recursion. */ function objectTraverse (o, fn, base, path) { path = path || []; base = base || o; Object.keys(o).forEach(function (key) { if (fn(o, key, o[key], base, path) && jQuery.isPlainObject(o[key])) { path.push(key); objectTraverse(o[key], fn, base, path); path.pop(); } }); } /* * Get/set a nested key in an object. Path is an array of the keys to reference each level * of nesting. If value is provided, the nested key is set. * The value of the nested key is returned. */ function objectPath (o, path, value) { var last = path.pop(); while (path.length && o) { o = o[path.shift()]; } if (arguments.length < 3) { return (o? o[last] : o); } return (o[last] = value); } 

Eu usei esse código no meu projeto

 const getValue = (obj, arrPath) => ( arrPath.reduce((x, y) => { if (y in x) return x[y] return {} }, obj) ) 

Uso:

 const obj = { id: { user: { local: 104 } } } const path = [ 'id', 'user', 'local' ] getValue(obj, path) // return 104 

Poucos anos depois, descobri isso que lida com escopo e matriz. eg a['b']["c"].d.etc

 function getScopedObj(scope, str) { let obj=scope, arr; try { arr = str.split(/[\[\]\.]/) // split by [,],. .filter(el => el) // filter out empty one .map(el => el.replace(/^['"]+|['"]+$/g, '')); // remove string quotation arr.forEach(el => obj = obj[el]) } catch(e) { obj = undefined; } return obj; } window.a = {b: {c: {d: {etc: 'success'}}}} getScopedObj(window, `abcdetc`) // success getScopedObj(window, `a['b']["c"].d.etc`) // success getScopedObj(window, `a['INVALID']["c"].d.etc`) // undefined 

Se você deseja converter qualquer object que contenha chaves de notação de ponto em uma versão com array dessas chaves, você pode usar isto.


Isso irá converter algo como

 { name: 'Andy', brothers.0: 'Bob' brothers.1: 'Steve' brothers.2: 'Jack' sisters.0: 'Sally' } 

para

 { name: 'Andy', brothers: ['Bob', 'Steve', 'Jack'] sisters: ['Sally'] } 

 convertDotNotationToArray(objectWithDotNotation) { Object.entries(objectWithDotNotation).forEach(([key, val]) => { // Is the key of dot notation if (key.includes('.')) { const [name, index] = key.split('.'); // If you have not created an array version, create one if (!objectWithDotNotation[name]) { objectWithDotNotation[name] = new Array(); } // Save the value in the newly created array at the specific index objectWithDotNotation[name][index] = val; // Delete the current dot notation key val delete objectWithDotNotation[key]; } }); } 

Não está claro qual é a sua pergunta. Dado seu object, obj.ab lhe daria “2” exatamente como é. Se você quisesse manipular a string para usar colchetes, você poderia fazer isso:

 var s = 'a.b'; s = 'obj["' + s.replace(/\./g, '"]["') + '"]'; alert(s); // displays obj["a"]["b"]