Konwertuj ciąg JavaScript w notacji kropkowej na odwołanie do obiektu

214

Biorąc pod uwagę obiekt JS

var obj = { a: { b: '1', c: '2' } }

i ciąg

"a.b"

jak mogę przekonwertować ciąg na notację kropkową, aby móc przejść

var val = obj.a.b

Gdyby ciąg był tylko 'a', mógłbym użyć obj[a]. Ale to jest bardziej złożone. Wyobrażam sobie, że istnieje jakaś prosta metoda, ale obecnie ucieka.

nevf
źródło
25
@Andrey evaljest zły; nie używaj tego
kevinji,
5
FYI: Oto kilka interesujących testów prędkości, które właśnie wykonałem: jsperf.com/dereference-object-property-path-from-string
James Wilkins
jeśli perf jest poważnym czynnikiem i często używasz tych samych ścieżek (np. w funkcji filtru tablicowego), użyj konstruktora funkcji, jak opisano w mojej odpowiedzi poniżej. Gdy ta sama ścieżka jest używana tysiące razy, metoda Function może być ponad 10 razy szybsza niż sprawdzanie lub dzielenie i redukowanie ścieżki przy każdym odchyleniu.
Kevin Crumley,

Odpowiedzi:

437

ostatnia uwaga: choć pochlebia mi, że ta odpowiedź spotkała się z dużym zainteresowaniem, jestem również nieco przerażona. Jeśli trzeba przekonwertować ciągi notacji kropkowej, takie jak „xabc”, na referencje, może to być (być może) znak, że dzieje się coś bardzo złego (chyba że wykonujesz jakąś dziwną deserializację).

Innymi słowy, nowicjusze, którzy znajdują drogę do tej odpowiedzi, muszą zadać sobie pytanie „dlaczego to robię?”.

Oczywiście dobrze jest to zrobić, jeśli twój przypadek użycia jest niewielki i nie napotkasz problemów z wydajnością, I nie będziesz musiał opierać się na swojej abstrakcji, aby później to skomplikować. W rzeczywistości, jeśli zmniejszy to złożoność kodu i uprości sprawy, prawdopodobnie powinieneś zrobić to, o co prosi OP. Jeśli jednak tak nie jest, zastanów się, czy któreś z nich ma zastosowanie:

przypadek 1 : Jako podstawowa metoda pracy z danymi (np. jako domyślna forma aplikacji do przekazywania i usuwania obiektów). Jak pytanie „jak mogę wyszukać nazwę funkcji lub zmiennej z ciągu znaków”.

  • Jest to zła praktyka programistyczna (szczególnie niepotrzebne metaprogramowanie i rodzaj narusza styl kodowania bez efektów ubocznych i będzie miał negatywny wpływ na wydajność). Nowicjusze, którzy znajdą się w tym przypadku, powinni zamiast tego rozważyć pracę z reprezentacjami tablic, np. ['X', 'a', 'b', 'c'], a nawet coś bardziej bezpośredniego / prostego / prostego, jeśli to możliwe: na przykład nie tracić śledzenie samych referencji w pierwszej kolejności (najbardziej idealne, jeśli jest to tylko po stronie klienta lub tylko po stronie serwera) itp. (Wcześniej istniejący unikalny identyfikator nie byłby dodawany, ale mógłby zostać użyty, jeśli specyfikacja wymaga jego istnienie niezależnie.)

przypadek 2 : Praca z serializowanymi danymi lub danymi, które będą wyświetlane użytkownikowi. Na przykład użycie daty jako ciągu „1999-12-30” zamiast obiektu Date (co może powodować błędy strefy czasowej lub zwiększać złożoność serializacji, jeśli nie jest to ostrożne). Lub wiesz, co robisz.

  • To może być w porządku. Uważaj, aby nie było ciągów kropek „”. w odkażonych fragmentach wejściowych.

Jeśli cały czas używasz tej odpowiedzi i konwertujesz w przód i w tył między łańcuchem a tablicą, możesz mieć zły przypadek i powinieneś rozważyć alternatywę.

Oto elegancki jednowarstwowy, który jest 10 razy krótszy niż inne rozwiązania:

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

[edytuj] Lub w ECMAScript 6:

'a.b.etc'.split('.').reduce((o,i)=>o[i], obj)

(Nie to, że myślę, że eval zawsze jest zły, jak sugerują inni (choć zwykle tak jest), ale ci ludzie będą zadowoleni, że ta metoda nie używa eval. Powyższe znajdzie obj.a.b.etcdane obji ciąg "a.b.etc".)

W odpowiedzi na tych, którzy nadal boją się używać, reducemimo że są w standardzie ECMA-262 (wydanie piąte), oto dwutorowa rekurencyjna implementacja:

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('a.b.etc')

W zależności od optymalizacji wykonywanych przez kompilator JS, możesz chcieć upewnić się, że żadne zagnieżdżone funkcje nie zostaną ponownie zdefiniowane przy każdym wywołaniu zwykłymi metodami (umieszczając je w zamknięciu, obiekcie lub globalnej przestrzeni nazw).

edycja :

Aby odpowiedzieć na interesujące pytanie w komentarzach:

jak zmieniłbyś to również w setera? Nie tylko zwraca wartości według ścieżki, ale także ustawia je, jeśli nowa funkcja zostanie wysłana do funkcji? - Swader 28 czerwca o 21:42

(sidenote: niestety nie może zwrócić obiektu za pomocą Settera, ponieważ naruszałoby to konwencję wywoływania; komentator wydaje się raczej odwoływać do ogólnej funkcji w stylu Setera z efektami ubocznymi, takimi jak index(obj,"a.b.etc", value)robienie obj.a.b.etc = value.)

reduceStyl jest naprawdę nie nadaje się do tego, ale możemy zmodyfikować rekurencyjną realizacji:

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

Próbny:

> obj = {a:{b:{etc:5}}}

> index(obj,'a.b.etc')
5
> index(obj,['a','b','etc'])   #works with both strings and lists
5

> index(obj,'a.b.etc', 123)    #setter-mode - third argument (possibly poor form)
123

> index(obj,'a.b.etc')
123

... choć osobiście poleciłbym osobną funkcję setIndex(...). Chciałbym zakończyć na marginesie, że oryginalny poser pytania mógł (powinien?) Pracować z tablicami indeksów (z których mogą uzyskać .split), a nie z łańcuchami; chociaż zwykle nie ma nic złego w funkcji wygody.


Komentator zapytał:

co z tablicami? coś w stylu „ab [4] .cd [1] [2] [3]”? –Aleksy

JavaScript jest bardzo dziwnym językiem; ogólnie rzecz biorąc, obiekty mogą mieć tylko łańcuchy jako klucze właściwości, więc na przykład, jeśli byłby to xobiekt ogólny x={}, to x[1]stałby się x["1"]... czytasz to dobrze ... tak ...

Tablice JavaScript (które same są instancjami Object) szczególnie zachęcają do wprowadzania liczb całkowitych, nawet jeśli można coś takiego zrobić x=[]; x["puppy"]=5;.

Ale ogólnie (i są wyjątki), x["somestring"]===x.somestring(gdy jest to dozwolone; nie można tego zrobić x.123).

(Należy pamiętać, że dowolny kompilator JS, którego używasz, może wybrać kompilację tych danych w celu uzyskania lepszej reprezentacji, jeśli może udowodnić, że nie naruszy specyfikacji).

Tak więc odpowiedź na twoje pytanie zależy od tego, czy zakładasz, że te obiekty akceptują tylko liczby całkowite (z powodu ograniczenia w Twojej problematycznej dziedzinie), czy nie. Załóżmy, że nie. Zatem poprawnym wyrażeniem jest konkatenacja identyfikatora podstawowego plus niektóre .identifiers plus niektóre ["stringindex"]s

Byłoby to wtedy równoważne a["b"][4]["c"]["d"][1][2][3], choć prawdopodobnie powinniśmy również wspierać a.b["c\"validjsstringliteral"][3]. Musisz sprawdzić sekcję gramatyki ekmascript na literałach łańcuchów, aby zobaczyć, jak parsować poprawne literały łańcuchowe. Technicznie chciałbyś również sprawdzić (inaczej niż w mojej pierwszej odpowiedzi), czy ajest to prawidłowy identyfikator javascript .

Prosta odpowiedź na pytanie jednak, czy struny nie zawierają przecinków lub nawiasów , byłoby po prostu dopasować długość 1+ sekwencje znaków nie w zestawie ,lub [lub ]:

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

Jeśli twoje ciągi nie zawierają znaków specjalnych ani "znaków , a ponieważ IdentifierNames są podjęzykiem StringLiterals (myślę, że?), Możesz najpierw przekonwertować swoje kropki na []:

> 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"]

Oczywiście zawsze bądź ostrożny i nigdy nie ufaj swoim danym. Niektóre złe sposoby na zrobienie tego, które mogą działać w niektórych przypadkach użycia, obejmują również:

// hackish/wrongish; preprocess your string into "a.b.4.c.d.1.2.3", e.g.: 
> yourstring.replace(/]/g,"").replace(/\[/g,".").split(".")
"a.b.4.c.d.1.2.3"  //use code from before

Edycja specjalna 2018:

Okrążmy koło i przygotujmy najbardziej nieefektywne, okropnie zaprogramowane rozwiązanie, jakie możemy wymyślić ... w interesie syntaktycznej czystości . Z obiektami ES6 Proxy! ... Zdefiniujmy również niektóre właściwości, które (imho są w porządku i cudowne, ale) mogą uszkodzić niepoprawnie napisane biblioteki. Być może powinieneś być ostrożny z korzystaniem z tego, jeśli zależy Ci na wydajności, zdrowiu psychicznym (swoim lub innych), pracy itp.

// [1,2,3][-1]==3 (or just use .slice(-1)[0])
if (![1][-1])
    Object.defineProperty(Array.prototype, -1, {get() {return this[this.length-1]}}); //credit to caub

// WARNING: THIS XTREME™ RADICAL METHOD IS VERY INEFFICIENT,
// ESPECIALLY IF INDEXING INTO MULTIPLE OBJECTS,
// because you are constantly creating wrapper objects on-the-fly and,
// even worse, going through Proxy i.e. runtime ~reflection, which prevents
// compiler optimization

// Proxy handler to override obj[*]/obj.* and obj[*]=...
var hyperIndexProxyHandler = {
    get: function(obj,key, proxy) {
        return key.split('.').reduce((o,i)=>o[i], obj);
    },
    set: function(obj,key,value, proxy) {
        var keys = key.split('.');
        var beforeLast = keys.slice(0,-1).reduce((o,i)=>o[i], obj);
        beforeLast[keys[-1]] = value;
    },
    has: function(obj,key) {
        //etc
    }
};
function hyperIndexOf(target) {
    return new Proxy(target, hyperIndexProxyHandler);
}

Próbny:

var obj = {a:{b:{c:1, d:2}}};
console.log("obj is:", JSON.stringify(obj));

var objHyper = hyperIndexOf(obj);
console.log("(proxy override get) objHyper['a.b.c'] is:", objHyper['a.b.c']);
objHyper['a.b.c'] = 3;
console.log("(proxy override set) objHyper['a.b.c']=3, now obj is:", JSON.stringify(obj));

console.log("(behind the scenes) objHyper is:", objHyper);

if (!({}).H)
    Object.defineProperties(Object.prototype, {
        H: {
            get: function() {
                return hyperIndexOf(this); // TODO:cache as a non-enumerable property for efficiency?
            }
        }
    });

console.log("(shortcut) obj.H['a.b.c']=4");
obj.H['a.b.c'] = 4;
console.log("(shortcut) obj.H['a.b.c'] is obj['a']['b']['c'] is", obj.H['a.b.c']);

Wynik:

obj to: {"a": {"b": {"c": 1, "d": 2}}}

(przesłanianie proxy get) objHyper ['abc'] to: 1

(zestaw zastępowania proxy) objHyper ['abc'] = 3, teraz obj to: {"a": {"b": {"c": 3, "d": 2}}}

(za kulisami) objHyper to: Proxy {a: {…}}

(skrót) obj.H ['abc'] = 4

(skrót) obj.H ['abc'] to obj ['a'] ['b'] ['c'] to: 4

nieefektywny pomysł: Możesz zmodyfikować powyższe do wysyłki w oparciu o argument wejściowy; albo użyj tej .match(/[^\]\[.]+/g)metody do obsługi obj['keys'].like[3]['this'], albo jeśli instanceof Array, a następnie po prostu zaakceptuj tablicę jako dane wejściowe, takie jak keys = ['a','b','c']; obj.H[keys].


Zgodnie z sugestią, że być może chcesz obsługiwać niezdefiniowane wskaźniki w „bardziej miękki” sposób w stylu NaN (np. index({a:{b:{c:...}}}, 'a.x.c')Zwracaj niezdefiniowane, a nie wyłapane TypeError) ...:

1) Ma to sens z punktu widzenia „powinniśmy zwrócić niezdefiniowany zamiast rzucać błąd” w sytuacji indeksu jednowymiarowego ({}) [„np.”] == niezdefiniowany, więc „powinniśmy zwrócić niezdefiniowany zamiast rzucać błąd ”w sytuacji N-wymiarowej.

2) Nie ma to sensu z perspektywy, którą robimy x['a']['x']['c'], co zawiódłoby w przypadku błędu TypeError w powyższym przykładzie.

Powiedziałbyś, że sprawisz, że to zadziała, zastępując swoją funkcję redukującą:

(o,i)=>o===undefined?undefined:o[i]Lub (o,i)=>(o||{})[i].

(Możesz to uczynić bardziej wydajnym, używając pętli for i przerywania / zwracania za każdym razem, gdy wynik, w którym będziesz indeksować dalej, jest niezdefiniowany lub używając try-catch, jeśli spodziewacie się, że takie niepowodzenia będą wystarczająco rzadkie.)

ninjagecko
źródło
4
reducenie jest obsługiwany we wszystkich obecnie używanych przeglądarkach.
Ricardo Tomasi,
14
@ Ricardo: Array.reducejest częścią standardu ECMA-262. Jeśli naprawdę chcesz obsługiwać przestarzałe przeglądarki, możesz zdefiniować Array.prototype.reduceprzykładową implementację podaną gdzieś (np. Developer.mozilla.org/en/JavaScript/Reference/Global_Objects/… ).
ninjagecko
2
Tak, ale łatwo jest umieścić te dwie linie w funkcji. var setget = function( obj, path ){ function index( robj,i ) {return robj[i]}; return path.split('.').reduce( index, obj ); }
nevf
3
Uwielbiam ten elegancki przykład, dzięki ninjagecko. Rozszerzyłem go o obsługę notacji stylów tablic, a także pustych ciągów - patrz mój przykład tutaj: jsfiddle.net/sc0ttyd/q7zyd
Sc0ttyD
2
@ninjagecko, jak byś zmienił to również w setera? Nie tylko zwraca wartości według ścieżki, ale także ustawia je, jeśli nowa funkcja zostanie wysłana do funkcji?
Swader
64

Jeśli możesz użyć lodash , istnieje funkcja, która robi dokładnie to:

_.get (obiekt, ścieżka, [defaultValue])

var val = _.get(obj, "a.b");
Petar Iwanow
źródło
4
Uwaga: _.get(object, path)nie psuje się, jeśli ścieżka nie została znaleziona. 'a.b.etc'.split('.').reduce((o,i)=>o[i], obj)robi. W moim konkretnym przypadku - nie w każdym przypadku - dokładnie tego, czego potrzebowałem. Dzięki!
Pan B.
1
@ Mr.B. najnowsza wersja Lodash ma trzeci, opcjonalny argument za defaultValue. Że _.get()metoda zwraca wartość domyślną, jeśli _.get()postanawia undefined, więc ustawić go na cokolwiek chcesz i zegarek do wartości ustawionej.
steampowered 21.04.17
7
Dla każdego, kto się zastanawia, obsługuje również _.set(object, path, value).
Jeffrey Roosendaal
19

Nieco bardziej zaangażowany przykład z rekurencją.

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,'a.d.a.b')); //blah
joekarl
źródło
19

możesz także użyć lodash.get

Wystarczy zainstalować ten pakiet (npm i --save lodash.get), a następnie użyć go w następujący sposób:

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;
DarkCrazy
źródło
10

Jeśli spodziewasz się wyłuskać tę samą ścieżkę wiele razy, zbudowanie funkcji dla każdej ścieżki notacji kropkowej ma jak dotąd najlepszą wydajność (rozwinięcie testów perfów, do których James Wilkins przywołał w komentarzach powyżej).

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

Korzystanie z konstruktora funkcji ma te same wady co eval () pod względem bezpieczeństwa i wydajności w najgorszym przypadku, ale IMO jest bardzo niewykorzystanym narzędziem w przypadkach, gdy potrzebujesz połączenia ekstremalnej dynamiki i wysokiej wydajności. Używam tej metodologii do budowania funkcji filtrów tablicowych i wywoływania ich w pętli AngularJS. Moje profile konsekwentnie pokazują krok array.filter (), biorąc dereferencję w czasie krótszym niż 1ms i filtrują około 2000 złożonych obiektów, używając dynamicznie zdefiniowanych ścieżek o głębokości 3-4 poziomów.

Podobną metodologię można oczywiście zastosować do tworzenia funkcji ustawiających:

var setter = new Function("obj", "newval", "obj." + path + " = newval;");
setter(obj, "some new val");
Kevin Crumley
źródło
1
jeśli chcesz wyrejestrować te same ścieżki z daleka od siebie, powyższy link jsperf.com pokazuje przykład, jak zapisać i wyszukać funkcję później. Wywołanie konstruktora funkcji jest dość powolne, więc kod o wysokiej wydajności powinien zapamiętać wyniki, aby uniknąć powtórzenia go, jeśli to możliwe.
Kevin Crumley,
7

Proponuję podzielić ścieżkę, powtórzyć ją i zmniejszyć posiadany obiekt. Ta propozycja działa z wartością domyślną dla brakujących właściwości.

const getValue = (object, keys) => keys.split('.').reduce((o, k) => (o || {})[k], object);

console.log(getValue({ a: { b: '1', c: '2' } }, 'a.b'));
console.log(getValue({ a: { b: '1', c: '2' } }, 'foo.bar.baz'));

Nina Scholz
źródło
6

Uwaga: jeśli już korzystasz z Lodash , możesz użyć funkcji propertylub get:

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

Znak podkreślenia ma również propertyfunkcję, ale nie obsługuje notacji kropkowej.

Tamlyn
źródło
1
Wydaje się, że podkreślenie nie obsługuje notacji kropkowej. Odpowiedź zaktualizowana.
Tamlyn,
5

Inne propozycje są trochę tajemnicze, więc pomyślałem, że wrócę:

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] } }
Ricardo Tomasi
źródło
5

Możesz skorzystać z biblioteki dostępnej w npm, co upraszcza ten proces. https://www.npmjs.com/package/dot-object

 var dot = require('dot-object');

var obj = {
 some: {
   nested: {
     value: 'Hi there!'
   }
 }
};

var val = dot.pick('some.nested.value', obj);
console.log(val);

// Result: Hi there!
Jeremias Santos
źródło
1
Dziękujemy za zwrócenie nam na to uwagi. Wygląda bardzo przydatnie.
nevf
I dlatego każdy projekt wprowadza 10000 zależności :-).
Marvin
4
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

Jest to dużo kodu w porównaniu do znacznie prostszego evalsposobu robienia tego, ale jak mówi Simon Willison, nigdy nie powinieneś używać eval .

Również JSFiddle .

McKayla
źródło
Świetnie, to działa na ucztę i jest krótsze niż CD Sanchez. Dzięki.
nevf
wartość (a, „bbbbb”) nie zwraca wartości niezdefiniowanej
frumbert
4

Rozszerzyłem elegancką odpowiedź przez ninjagecko, aby funkcja obsługiwała zarówno odwołania do stylu kropkowanego, jak i tablicowego oraz aby pusty ciąg spowodował zwrócenie obiektu nadrzędnego.

Proszę bardzo:

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

Zobacz mój działający przykład jsFiddle tutaj: http://jsfiddle.net/sc0ttyd/q7zyd/

Sc0ttyD
źródło
Naprawdę dobre rozwiązanie. Jest tylko jeden problem, zakłada się, że []zapis jest zawsze dla tablic. w ten sposób mogą być reprezentowane klucze obiektów, na przykład obj['some-problem/name'].list[1]Aby to naprawić, musiałem zaktualizować arr_dereftaką funkcję javascript function arr_deref(o, ref, i) { return !ref ? o : (o[(ref.slice(0, i ? -1 : ref.length)).replace(/^['"]|['"]$/g, '')]); }
Serkan Yersen
1
Chociaż w dzisiejszych czasach nie zrobiłbym tego. Użyłbym Lodash: lodash.com/docs#get
Sc0ttyD
2
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, "a.b"); // 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, "a.k", function () { console.log("hello world"); });

find(obj, "a.k")(); // hello world
Cristian Sanchez
źródło
Dziękuję za wszystkie szybkie odpowiedzi. Nie lubię rozwiązań eval (). Ten i podobne posty wyglądają najlepiej. Jednak nadal mam problem. Próbuję ustawić wartość obj.ab = nowa wartość. Aby być precyzyjnym, wartość b jest funkcją, więc muszę użyć obj.ab (nowa_wartość). Funkcja jest wywoływana, ale wartość nie jest ustawiona. Myślę, że to kwestia zakresu, ale wciąż kopie. Zdaję sobie sprawę, że jest to poza zakresem pierwotnego pytania. Mój kod używa Knockout.js, a b jest ko.observable.
nevf
@nevf: Dodałem drugą funkcję, która moim zdaniem robi to, co chcesz. Możesz dostosować go do swoich upodobań w zależności od pożądanego zachowania (np. Czy powinien tworzyć obiekty, jeśli nie istnieją? Itp.).
Cristian Sanchez
@nevf Ale mój robi to z jedną funkcją. ; D
McKayla,
dzięki za aktualizację, której mogłem użyć. @tylermwashburn - i dziękuję za krótszą implementację, która również działa na ucztę. Życzymy wszystkim świetnych wrażeń.
nevf
2

Wartość elementu członkowskiego można uzyskać za pomocą notacji kropkowej za pomocą jednego wiersza kodu:

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

W twoim przypadku:

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

Aby to uprościć, możesz napisać taką funkcję:

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

Wyjaśnienie:

Konstruktor funkcji tworzy nowy obiekt Function. W JavaScript każda funkcja jest w rzeczywistości obiektem Function. Składnia do jawnego tworzenia funkcji za pomocą Konstruktora funkcji to:

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

gdzie arguments(arg1 to argN)musi być ciągiem, który odpowiada prawidłowemu identyfikatorowi javaScript i functionBodyjest ciągiem zawierającym instrukcje javaScript zawierające definicję funkcji.

W naszym przypadku korzystamy z funkcji ciągu znaków, aby pobrać element obiektu z notacją kropkową.

Mam nadzieję, że to pomoże.

Harish Anchu
źródło
Czy potrafisz dokładnie wyjaśnić, co to robi? A w jaki sposób przekażesz „ab” itp. Jako parametr funkcji?
nevf
1
Dałem to +1, ale JSLint ostrzega, że „konstruktor funkcji jest formą eval” .
gabe
2

Odpowiedź GET / SET, która działa również w reakcji native (nie można Object.prototypeobecnie przypisać ):

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

Stosowanie:

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
Aleksander Daniłow
źródło
1

Skopiowałem poniższe z odpowiedzi Ricardo Tomasi i zmodyfikowałem, aby również utworzyć podobiekty, które nie istnieją jeszcze w razie potrzeby. Jest to nieco mniej wydajne (więcej ifsi tworzenia pustych obiektów), ale powinno być całkiem dobre.

Pozwoli nam to również robić Object.prop(obj, 'a.b', false)to, czego wcześniej nie mogliśmy. Niestety nadal nie pozwala nam to przypisać undefined... Jeszcze nie wiem, jak sobie z tym poradzić.

/**
 * 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]
 }
Chris V.
źródło
1

Jeśli chcesz przekonwertować dowolny obiekt zawierający klucze notacji kropkowej na wersję tablicową tych kluczy, możesz tego użyć.


Spowoduje to konwersję czegoś takiego

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

do

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

}
Matthew Mullin
źródło
Nie przetworzył wartości z „Brother.0.one” jak JSON.
Karthick
Czy masz jakieś sugestie dotyczące przetwarzania bardziej złożonego JSON z kolekcją tablic.?
Karthick
0

Oto mój kod bez użycia eval. Łatwo to też zrozumieć.

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, 'a.d.a.b')); //returns blah
Alvin Magalona
źródło
0

Tak, pytano 4 lata temu i tak, rozszerzenie prototypów bazowych zwykle nie jest dobrym pomysłem, ale jeśli wszystkie rozszerzenia są przechowywane w jednym miejscu, mogą być przydatne.
Oto mój sposób na zrobienie tego.

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

Teraz będziesz mógł uzyskać wszędzie zagnieżdżoną właściwość bez importowania modułu z funkcją lub funkcją kopiuj / wklej.

UPD. Przykład:

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

UPD 2. Kolejne rozszerzenie łamie mongoose w moim projekcie. Przeczytałem również, że może to przerwać jquery. Więc nigdy nie rób tego w następny sposób

 Object.prototype.getNestedProperty = function (propertyName) {
    var result = this;
    var arr = propertyName.split(".");

    while (arr.length && result) {
        result = result[arr.shift()];
    }

    return result;
};
Michael Kapustey
źródło
0

Oto moja implementacja

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

Implementacja 2 (użycie redukcji tablicy zamiast plasterka)

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

Przykłady:

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.a.b.access('c','d'); // returns: 'abcd'
myobj.access('a','b','c','e',0); // returns: 11

jak również może obsługiwać obiekty wewnątrz tablic

var myobj2 = {'a': {'b':[{'c':'ab0c'},{'d':'ab1d'}]}}
myobj2.access('a','b','1','d'); // returns: 'ab1d'
Xeltor
źródło
0

To jest moje rozszerzone rozwiązanie zaproponowane przez: ninjagecko

Dla mnie zwykły zapis ciągów nie był wystarczający, więc poniżej wersja obsługuje takie rzeczy jak:

indeks (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)
}
Yabree
źródło
0

Ryzykując pokonanie martwego konia ... Uważam, że jest to najbardziej przydatne w przemierzaniu zagnieżdżonych obiektów w celu odniesienia się tam, gdzie jesteś, w odniesieniu do obiektu podstawowego lub podobnego obiektu o tej samej strukturze. W tym celu jest to przydatne w przypadku funkcji przejścia przez obiekt zagnieżdżony. Zauważ, że użyłem tablicy do przechowywania ścieżki. Byłoby trywialne zmodyfikować to, aby użyć albo ścieżki ciągu, albo tablicy. Zauważ też, że możesz przypisać wartość „niezdefiniowana” do wartości, w przeciwieństwie do niektórych innych implementacji.

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

srkleiman
źródło
0

Użyłem tego kodu w moim projekcie

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

Stosowanie:

const obj = { id: { user: { local: 104 } } }
const path = [ 'id', 'user', 'local' ]
getValue(obj, path) // return 104
Prathak Paisanwatcharakit
źródło
0

Kilka lat później odkryłem, że obsługuje zakres i tablicę. na przykłada['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, `a.b.c.d.etc`)             // success
getScopedObj(window, `a['b']["c"].d.etc`)       // success
getScopedObj(window, `a['INVALID']["c"].d.etc`) // undefined
allenhwkim
źródło
-1

Nie jest jasne, jakie jest twoje pytanie. Biorąc pod uwagę twój przedmiot, obj.a.bdałbyś ci „2” tak jak jest. Jeśli chcesz manipulować ciągiem w celu użycia nawiasów, możesz to zrobić:

var s = 'a.b';
s = 'obj["' + s.replace(/\./g, '"]["') + '"]';
alert(s); // displays obj["a"]["b"]
Mark Eirich
źródło
1
To nie działa a.b.ci tak naprawdę nie osiąga tego, czego chcą. Chcą wartości, a nie evalścieżki.
McKayla,
Teraz to naprawiłem, więc działa a.b.c, ale masz rację, najwyraźniej chciał uzyskać / ustawić wartość właściwości na obj.a.b. Pytanie było dla mnie mylące, ponieważ powiedział, że chce „przekonwertować ciąg”…
Mark Eirich,
Dobra robota. :) To było trochę niejasne. Wykonałeś jednak dobrą robotę nawrócenia.
McKayla,
-1

oto moje 10 centów za przypadek, poniżej funkcja zostanie ustawiona / ustawiona na podstawie podanej ścieżki, .. na pewno możesz ją poprawić, usuń || i zamień na, Object.hasOwnPropertyjeśli pomylnie troszczysz się o fałszywe wartości,

przetestowałem go z a.b.cab2.c {a:{b:[0,1,{c:7}]}}i działa zarówno przy ustawianiu, jak i uzyskiwaniu :).

Cheerz

function helper(obj, path, setValue){
  const l = String(path).split('.');
  return l.reduce((o,i, idx)=>{
   if( l.length-idx===1)  { o[i] = setValue || o[i];return setValue ? obj : o[i];}
  o[i] = o[i] || {};
   return o[i];
  }, x)
}
Zalaboza
źródło