Dynamicznie ustawiana właściwość obiektu zagnieżdżonego

89

Mam obiekt, który może mieć dowolną liczbę poziomów głębokości i może mieć dowolne istniejące właściwości. Na przykład:

var obj = {
    db: {
        mongodb: {
            host: 'localhost'
        }
    }
};

Na tym chciałbym ustawić (lub nadpisać) takie właściwości:

set('db.mongodb.user', 'root');
// or:
set('foo.bar', 'baz');

Gdzie ciąg właściwości może mieć dowolną głębokość, a wartość może być dowolnym typem / rzeczą.
Obiekty i tablice jako wartości nie muszą być łączone, jeśli klucz właściwości już istnieje.

Poprzedni przykład dałby następujący obiekt:

var obj = {
    db: {
        mongodb: {
            host: 'localhost',
            user: 'root'
        }
    },
    foo: {
        bar: baz
    }
};

Jak mogę zrealizować taką funkcję?

John B.
źródło
Jaki powinien być wynik set('foo', 'bar'); set('foo.baz', 'qux');, gdzie foonajpierw trzyma a, Stringa potem staje się Object? Co się dzieje 'bar'?
Jonathan Lonowski
może to pomóc: stackoverflow.com/questions/695050/…
Rene M.
1
Jeśli usuniesz set()metodę i po prostu obj.db.mongodb.user = 'root';uzyskasz dokładnie to, czego chcesz?
adeneo
@JonathanLonowski Lonowski barzostanie nadpisany przez Object. @adeneo i @rmertins Owszem :) Ale niestety muszę owinąć inną logikę. @Robert Levy Znalazłem to i uzyskałem dostęp do pracy, ale ustawienie wydaje się o wiele bardziej skomplikowane ...
John B.

Odpowiedzi:

98

Ta funkcja, używając podanych argumentów, powinna dodać / zaktualizować dane w objkontenerze. Zauważ, że musisz śledzić, które elementy w objschemacie są kontenerami, a które wartościami (ciągi znaków, liczby wewnętrzne itp.) W przeciwnym razie zaczniesz rzucać wyjątki.

obj = {};  // global object

function set(path, value) {
    var schema = obj;  // a moving reference to internal objects within obj
    var pList = path.split('.');
    var len = pList.length;
    for(var i = 0; i < len-1; i++) {
        var elem = pList[i];
        if( !schema[elem] ) schema[elem] = {}
        schema = schema[elem];
    }

    schema[pList[len-1]] = value;
}

set('mongo.db.user', 'root');
bpmason1
źródło
2
@ bpmason1 czy możesz wyjaśnić, dlaczego używałeś var schema = objzamiast objwszędzie?
sman591
3
@ sman591 schemato wskaźnik, który jest przesuwany w dół ścieżki za pomocą schema = schema[elem]. Więc po pętli for schema[pList[len - 1]]wskazuje na plik mongo.db.user obj.
webjay
to rozwiązało mój problem dzięki, nie mogłem znaleźć tego w dokumentach MDN. Ale mam inną wątpliwość, jeśli operator przypisania podaje odniesienie do obiektów wewnętrznych, to jak utworzyć oddzielny obiekt object2 od object1, aby zmiany dokonane na object2 nie odzwierciedlały obiektu object1.
Onix,
@Onix Możesz w tym celu skorzystać z cloneDeepfunkcji lodash .
Aakash Thakur
@Onix const clone = JSON.parse (JSON.stringify (obj))
Mike Makuch
79

Lodash ma metodę _.set () .

_.set(obj, 'db.mongodb.user', 'root');
_.set(obj, 'foo.bar', 'baz');
aheuermann
źródło
1
Czy można go również użyć do ustawienia wartości klucza? jeśli tak, czy możesz podzielić się przykładem. Dziękuję
sage poudel
To świetnie, ale jak możesz śledzić / określać ścieżkę?
Tom
19

Trochę późno, ale oto prostsza odpowiedź, która nie jest biblioteką:

/**
 * Dynamically sets a deeply nested value in an object.
 * Optionally "bores" a path to it if its undefined.
 * @function
 * @param {!object} obj  - The object which contains the value you want to change/set.
 * @param {!array} path  - The array representation of path to the value you want to change/set.
 * @param {!mixed} value - The value you want to set it to.
 * @param {boolean} setrecursively - If true, will set value of non-existing path as well.
 */
function setDeep(obj, path, value, setrecursively = false) {
    path.reduce((a, b, level) => {
        if (setrecursively && typeof a[b] === "undefined" && level !== path.length){
            a[b] = {};
            return a[b];
        }

        if (level === path.length){
            a[b] = value;
            return value;
        } 
        return a[b];
    }, obj);
}

Ta funkcja, którą stworzyłem, może zrobić dokładnie to, czego potrzebujesz i trochę więcej.

powiedzmy, że chcemy zmienić wartość docelową, która jest głęboko zagnieżdżona w tym obiekcie:

let myObj = {
    level1: {
        level2: {
           target: 1
       }
    }
}

Więc nazwalibyśmy naszą funkcję w ten sposób:

setDeep(myObj, ["level1", "level2", "target1"], 3);

spowoduje:

myObj = {level1: {level2: {target: 3}}}

Ustawienie rekurencyjnej flagi set na true spowoduje ustawienie obiektów, jeśli nie istnieją.

setDeep(myObj, ["new", "path", "target"], 3, true);

spowoduje to:

obj = myObj = {
    new: {
         path: {
             target: 3
         }
    },
    level1: {
        level2: {
           target: 3
       }
    }
}
Philll_t
źródło
1
Użyłem tego kodu, czystego i prostego. Zamiast obliczać levelużyłem reducetrzeciego argumentu.
Juan Lanus
1
Uważam, że levelmusi to być +1 lub path.length-1
ThomasReggi
12

Możemy użyć funkcji rekurencyjnej:

/**
 * Sets a value of nested key string descriptor inside a Object.
 * It changes the passed object.
 * Ex:
 *    let obj = {a: {b:{c:'initial'}}}
 *    setNestedKey(obj, ['a', 'b', 'c'], 'changed-value')
 *    assert(obj === {a: {b:{c:'changed-value'}}})
 *
 * @param {[Object]} obj   Object to set the nested key
 * @param {[Array]} path  An array to describe the path(Ex: ['a', 'b', 'c'])
 * @param {[Object]} value Any value
 */
export const setNestedKey = (obj, path, value) => {
  if (path.length === 1) {
    obj[path] = value
    return
  }
  return setNestedKey(obj[path[0]], path.slice(1), value)
}

To jest prostsze!

Hemã Vidal
źródło
3
wygląda dobrze! wystarczy sprawdzić parametr obj, aby upewnić się, że nie jest błędny, zgłosi błąd, jeśli którykolwiek z elementów w łańcuchu nie istnieje.
C Smith
2
możesz po prostu użyć path.slice (1);
Marcos Pereira
1
Doskonała odpowiedź, ładne i zwięzłe rozwiązanie.
chim
Uważam, że jeśli instrukcja, powinna być, obj[path[0]] = value;ponieważ pathjest zawsze typu string[], nawet jeśli został tylko 1 ciąg.
Valorad
Obiekty JavaScript powinny działać przy użyciu obj[['a']] = 'new value'. Sprawdź kod: jsfiddle.net/upsdne03
Hemã Vidal
12

Po prostu piszę małą funkcję przy użyciu rekurencji ES6 +, aby osiągnąć cel.

updateObjProp = (obj, value, propPath) => {
    const [head, ...rest] = propPath.split('.');

    !rest.length
        ? obj[head] = value
        : this.updateObjProp(obj[head], value, rest.join('.'));
}

const user = {profile: {name: 'foo'}};
updateObjProp(user, 'fooChanged', 'profile.name');

Dużo go używałem do reagowania na stan aktualizacji, działało całkiem dobrze dla mnie.

Bruno Joaquim
źródło
1
to było przydatne, musiałem umieścić toString () w proPath, aby działał z zagnieżdżonymi właściwościami, ale potem działało świetnie. const [head, ... rest] = propPath.toString (). split ('.');
WizardsOfWor
1
@ user738048 @ Bruno-Joaquim linia this.updateStateProp(obj[head], value, rest);powinna byćthis.updateStateProp(obj[head], value, rest.join());
ma.mehralian
9

ES6 ma całkiem fajny sposób, aby to zrobić, używając obliczonej nazwy właściwości i parametru reszt .

const obj = {
  levelOne: {
    levelTwo: {
      levelThree: "Set this one!"
    }
  }
}

const updatedObj = {
  ...obj,
  levelOne: {
    ...obj.levelOne,
    levelTwo: {
      ...obj.levelOne.levelTwo,
      levelThree: "I am now updated!"
    }
  }
}

Jeśli levelThreejest to właściwość dynamiczna, tj. Aby ustawić dowolną właściwość w levelTwo, możesz użyć[propertyName]: "I am now updated!" gdzie propertyNameprzechowuje nazwę właściwości w levelTwo.

ron4ex
źródło
8

Zainspirowany odpowiedzią @ bpmason1:

function leaf(obj, path, value) {
  const pList = path.split('.');
  const key = pList.pop();
  const pointer = pList.reduce((accumulator, currentValue) => {
    if (accumulator[currentValue] === undefined) accumulator[currentValue] = {};
    return accumulator[currentValue];
  }, obj);
  pointer[key] = value;
  return obj;
}

Przykład:

const obj = {
  boats: {
    m1: 'lady blue'
  }
};
leaf(obj, 'boats.m1', 'lady blue II');
leaf(obj, 'boats.m2', 'lady bird');
console.log(obj); // { boats: { m1: 'lady blue II', m2: 'lady bird' } }
webjay
źródło
7

Lodash ma metodę o nazwie update która robi dokładnie to, czego potrzebujesz.

Ta metoda otrzymuje następujące parametry:

  1. Obiekt do aktualizacji
  2. Ścieżka właściwości do aktualizacji (właściwość może być głęboko zagnieżdżona)
  3. Funkcja zwracająca wartość do zaktualizowania (podana oryginalna wartość jako parametr)

W twoim przykładzie wyglądałoby to tak:

_.update(obj, 'db.mongodb.user', function(originalValue) {
  return 'root'
})
brafdlog
źródło
2

Stworzyłem sedno do ustawiania i pobierania wartości obj za pomocą ciągu znaków na podstawie poprawnej odpowiedzi. Możesz go pobrać lub użyć jako pakietu npm / yarn.

// yarn add gist:5ceba1081bbf0162b98860b34a511a92
// npm install gist:5ceba1081bbf0162b98860b34a511a92
export const DeepObject = {
  set: setDeep,
  get: getDeep
};

// https://stackoverflow.com/a/6491621
function getDeep(obj: Object, path: string) {
  path = path.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
  path = path.replace(/^\./, '');           // strip a leading dot
  const a = path.split('.');
  for (let i = 0, l = a.length; i < l; ++i) {
    const n = a[i];
    if (n in obj) {
      obj = obj[n];
    } else {
      return;
    }
  }

  return obj;
}

// https://stackoverflow.com/a/18937118
function setDeep(obj: Object, path: string, value: any) {
  let schema = obj;  // a moving reference to internal objects within obj
  const pList = path.split('.');
  const len = pList.length;
  for (let i = 0; i < len - 1; i++) {
    const elem = pList[i];
    if (!schema[elem]) {
      schema[elem] = {};
    }
    schema = schema[elem];
  }

  schema[pList[len - 1]] = value;
}

// Usage
// import {DeepObject} from 'somePath'
//
// const obj = {
//   a: 4,
//   b: {
//     c: {
//       d: 2
//     }
//   }
// };
//
// DeepObject.set(obj, 'b.c.d', 10); // sets obj.b.c.d to 10
// console.log(DeepObject.get(obj, 'b.c.d')); // returns 10
Chiffie
źródło
1

Jeśli chcesz zmienić tylko głębiej zagnieżdżone obiekty, inną metodą może być odniesienie do obiektu. Ponieważ obiekty JS są obsługiwane przez ich odwołania, możesz utworzyć odniesienie do obiektu, do którego masz dostęp za pomocą klucza łańcuchowego.

Przykład:

// The object we want to modify:
var obj = {
    db: {
        mongodb: {
            host: 'localhost',
            user: 'root'
        }
    },
    foo: {
        bar: baz
    }
};

var key1 = 'mongodb';
var key2 = 'host';

var myRef = obj.db[key1]; //this creates a reference to obj.db['mongodb']

myRef[key2] = 'my new string';

// The object now looks like:
var obj = {
    db: {
        mongodb: {
            host: 'my new string',
            user: 'root'
        }
    },
    foo: {
        bar: baz
    }
};
agregat1166877
źródło
1

Innym podejściem jest użycie rekurencji do przekopania się przez obiekt:

(function(root){

  function NestedSetterAndGetter(){
    function setValueByArray(obj, parts, value){

      if(!parts){
        throw 'No parts array passed in';
      }

      if(parts.length === 0){
        throw 'parts should never have a length of 0';
      }

      if(parts.length === 1){
        obj[parts[0]] = value;
      } else {
        var next = parts.shift();

        if(!obj[next]){
          obj[next] = {};
        }
        setValueByArray(obj[next], parts, value);
      }
    }

    function getValueByArray(obj, parts, value){

      if(!parts) {
        return null;
      }

      if(parts.length === 1){
        return obj[parts[0]];
      } else {
        var next = parts.shift();

        if(!obj[next]){
          return null;
        }
        return getValueByArray(obj[next], parts, value);
      }
    }

    this.set = function(obj, path, value) {
      setValueByArray(obj, path.split('.'), value);
    };

    this.get = function(obj, path){
      return getValueByArray(obj, path.split('.'));
    };

  }
  root.NestedSetterAndGetter = NestedSetterAndGetter;

})(this);

var setter = new this.NestedSetterAndGetter();

var o = {};
setter.set(o, 'a.b.c', 'apple');
console.log(o); //=> { a: { b: { c: 'apple'}}}

var z = { a: { b: { c: { d: 'test' } } } };
setter.set(z, 'a.b.c', {dd: 'zzz'}); 

console.log(JSON.stringify(z)); //=> {"a":{"b":{"c":{"dd":"zzz"}}}}
console.log(JSON.stringify(setter.get(z, 'a.b.c'))); //=> {"dd":"zzz"}
console.log(JSON.stringify(setter.get(z, 'a.b'))); //=> {"c":{"dd":"zzz"}}
wyd.
źródło
1

Musiałem osiągnąć to samo, ale w Node.js ... Więc znalazłem ten fajny moduł: https://www.npmjs.com/package/nested-property

Przykład:

var mod = require("nested-property");
var obj = {
  a: {
    b: {
      c: {
        d: 5
      }
    }
  }
};
console.log(mod.get(obj, "a.b.c.d"));
mod.set(obj, "a.b.c.d", 6);
console.log(mod.get(obj, "a.b.c.d"));
Rehmat
źródło
jak rozwiązywać złożone obiekty zagnieżdżone. `` `const x = {'one': 1, 'two': 2, 'three': {'one': 1, 'two': 2, 'three': [{'one': 1}, { 'one': 'ONE'}, {'one': 'I'}]}, 'four': [0, 1, 2]}; console.log (np.get (x, 'three.three [0] .one')); ``
Sumukha HS
1

Wymyśliłem własne rozwiązanie, używając czystego es6 i rekurencji, które nie powoduje mutacji oryginalnego obiektu.

const setNestedProp = (obj = {}, [first, ...rest] , value) => ({
  ...obj,
  [first]: rest.length
    ? setNestedProp(obj[first], rest, value)
    : value
});

const result = setNestedProp({}, ["first", "second", "a"], 
"foo");
const result2 = setNestedProp(result, ["first", "second", "b"], "bar");

console.log(result);
console.log(result2);

Henry Ing-Simmons
źródło
Możesz wyeliminować pierwszy blok if, deklarując "obj" z wartością domyślną setNestedProp = (obj = {}, keys, value) => {
blindChicken
1
Yh fajnie. Spojrzenie wstecz może zniszczyć argument kluczy również in situ i uratować kolejną linię kodu
Henry Ing-Simmons,
Zasadniczo jeden liniowiec teraz 👍
Henry Ing-Simmons,
0

Jeśli chciałbyś, aby istniała funkcja, która wymaga wcześniejszych właściwości, możesz użyć czegoś takiego, co zwróci również flagę stwierdzającą, czy udało jej się znaleźć i ustawić zagnieżdżoną właściwość.

function set(obj, path, value) {
    var parts = (path || '').split('.');
    // using 'every' so we can return a flag stating whether we managed to set the value.
    return parts.every((p, i) => {
        if (!obj) return false; // cancel early as we havent found a nested prop.
        if (i === parts.length - 1){ // we're at the final part of the path.
            obj[parts[i]] = value;          
        }else{
            obj = obj[parts[i]]; // overwrite the functions reference of the object with the nested one.            
        }   
        return true;        
    });
}
C Smith
źródło
0

Zainspirowany ClojureScript's assoc-in( https://github.com/clojure/clojurescript/blob/master/src/main/cljs/cljs/core.cljs#L5280 ), używając rekurencji:

/**
 * Associate value (v) in object/array (m) at key/index (k).
 * If m is falsy, use new object.
 * Returns the updated object/array.
 */
function assoc(m, k, v) {
    m = (m || {});
    m[k] = v;
    return m;
}

/**
 * Associate value (v) in nested object/array (m) using sequence of keys (ks)
 * to identify the path to the nested key/index.
 * If one of the values in the nested object/array doesn't exist, it adds
 * a new object.
 */
function assoc_in(m={}, [k, ...ks], v) {
    return ks.length ? assoc(m, k, assoc_in(m[k], ks, v)) : assoc(m, k, v);
}

/**
 * Associate value (v) in nested object/array (m) using key string notation (s)
 * (e.g. "k1.k2").
 */
function set(m, s, v) {
    ks = s.split(".");
    return assoc_in(m, ks, v);
}

Uwaga:

Dzięki dostarczonej realizacji,

assoc_in({"a": 1}, ["a", "b"], 2) 

zwroty

{"a": 1}

Wolałbym, żeby w tym przypadku wyrzucał błąd. W razie potrzeby możesz dodać wpis, assocaby sprawdzić, czy mjest to obiekt lub tablica, iw przeciwnym razie zgłosić błąd.

Lucas Leblow
źródło
0

Próbowałem napisać tę metodę w skrócie , może komuś pomóc!

function set(obj, key, value) {
 let keys = key.split('.');
 if(keys.length<2){ obj[key] = value; return obj; }

 let lastKey = keys.pop();

 let fun = `obj.${keys.join('.')} = {${lastKey}: '${value}'};`;
 return new Function(fun)();
}

var obj = {
"hello": {
    "world": "test"
}
};

set(obj, "hello.world", 'test updated'); 
console.log(obj);

set(obj, "hello.world.again", 'hello again'); 
console.log(obj);

set(obj, "hello.world.again.onece_again", 'hello once again');
console.log(obj);

Arun Saini
źródło