Dynamisch festgelegte Eigenschaft eines verschachtelten Objekts

84

Ich habe ein Objekt, das eine beliebige Anzahl von Ebenen tief sein kann und vorhandene Eigenschaften haben kann. Zum Beispiel:

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

Darauf möchte ich Eigenschaften wie folgt setzen (oder überschreiben):

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

Wobei die Eigenschaftszeichenfolge eine beliebige Tiefe haben kann und der Wert ein beliebiger Typ / Gegenstand sein kann.
Objekte und Arrays als Werte müssen nicht zusammengeführt werden, falls der Eigenschaftsschlüssel bereits vorhanden ist.

Das vorherige Beispiel würde folgendes Objekt erzeugen:

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

Wie kann ich eine solche Funktion realisieren?

John B.
quelle
Wofür sollte das Ergebnis sein set('foo', 'bar'); set('foo.baz', 'qux');, wo foozuerst ein Stringdann ein wird Object? Was passiert mit 'bar'?
Jonathan Lonowski
könnte diese Hilfe möglich sein: stackoverflow.com/questions/695050/…
Rene M.
1
Wenn Sie die set()Methode entfernen und nur obj.db.mongodb.user = 'root';genau das haben, was Sie zu wollen scheinen?
Adeneo
@ JonathanLonowski Lonowski barwird von der überschrieben Object. @adeneo und @rmertins In der Tat :) Aber ich muss leider eine andere Logik umwickeln. @ Robert Levy Ich fand das und bekam den Zugriff zum Laufen, aber das Einstellen scheint so viel komplizierter ...
John B.

Antworten:

92

Diese Funktion sollte unter Verwendung der von Ihnen angegebenen Argumente die Daten im objContainer hinzufügen / aktualisieren . Beachten Sie, dass Sie objnachverfolgen müssen, welche Elemente im Schema Container und welche Werte (Zeichenfolgen, Ints usw.) sind. Andernfalls werden Ausnahmen ausgelöst.

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
quelle
2
@ bpmason1 könntest du erklären, warum du var schema = objstatt objüberall verwendet hast ?
sman591
3
@ sman591 schemaist ein Zeiger, mit dem der Pfad entlang bewegt wird schema = schema[elem]. schema[pList[len - 1]]Zeigt also nach der for-Schleife auf mongo.db.user in obj.
Webjay
Das hat mein Problem gelöst, danke, konnte dies nicht in MDN-Dokumenten finden. Aber ich habe einen anderen Zweifel, wenn der Zuweisungsoperator einen Verweis auf die internen Objekte gibt, wie man ein separates Objekt2 vom Objekt1 macht, so dass Änderungen, die an Objekt2 vorgenommen werden, sich nicht auf das Objekt1 auswirken.
Onix
@Onix Hierfür können Sie die lodash- cloneDeepFunktion nutzen.
Aakash Thakur
@Onix const clone = JSON.parse (JSON.stringify (obj))
Mike Makuch
72

Lodash hat eine _.set () -Methode.

_.set(obj, 'db.mongodb.user', 'root');
_.set(obj, 'foo.bar', 'baz');
aheuermann
quelle
1
Kann damit auch der Wert für key festgelegt werden? Wenn ja, können Sie ein Beispiel teilen. Vielen Dank
Salbei Poudel
Das ist großartig, aber wie würden Sie den Pfad verfolgen / bestimmen?
Tom
19

Ein bisschen spät, aber hier ist eine nicht bibliothekarische, einfachere Antwort:

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

Diese Funktion, die ich gemacht habe, kann genau das tun, was Sie brauchen und ein bisschen mehr.

Angenommen, wir möchten den Zielwert ändern, der tief in diesem Objekt verschachtelt ist:

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

Also würden wir unsere Funktion so nennen:

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

wird darin enden, dass:

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

Wenn Sie das Set-Flag rekursiv auf true setzen, werden Objekte festgelegt, wenn sie nicht vorhanden sind.

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

wird dazu führen:

obj = myObj = {
    new: {
         path: {
             target: 3
         }
    },
    level1: {
        level2: {
           target: 3
       }
    }
}
Philll_t
quelle
1
Verwendet diesen Code, sauber und einfach. Anstatt zu rechnen, habe levelich das reducedritte Argument verwendet.
Juan Lanus
Ich glaube, das levelmuss +1 oder path.length-1 sein
ThomasReggi
12

Wir können eine Rekursionsfunktion verwenden:

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

Es ist einfacher!

Hemã Vidal
quelle
3
sieht gut aus! Sie müssen nur den obj-Parameter überprüfen, um sicherzustellen, dass er nicht falsch ist. Es wird ein Fehler ausgegeben, wenn eine der Requisiten in der Kette nicht vorhanden ist.
C Smith
2
Sie können einfach path.slice (1) verwenden.
Marcos Pereira
1
Hervorragende Antwort, eine schöne und prägnante Lösung.
Chim
Ich glaube daran, dass if-Anweisung hätte sein sollen, obj[path[0]] = value;weil sie pathimmer vom Typ ist string[], auch wenn nur noch 1 Zeichenfolge übrig ist.
Valorad
Javascript-Objekte sollten mit funktionieren obj[['a']] = 'new value'. Überprüfen Sie den Code: jsfiddle.net/upsdne03
Hemã Vidal
11

Ich schreibe nur eine kleine Funktion mit ES6 + Rekursion, um das Ziel zu erreichen.

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

Ich habe es oft benutzt, um auf den Update-Status zu reagieren. Es hat ziemlich gut für mich funktioniert.

Bruno Joaquim
quelle
1
Das war praktisch, ich musste ein toString () auf proPath setzen, damit es mit verschachtelten Eigenschaften funktioniert, aber danach funktionierte es großartig. const [head, ... rest] = propPath.toString (). split ('.');
WizardsOfWor
1
@ user738048 @ Bruno-Joaquim die Linie this.updateStateProp(obj[head], value, rest);sollte seinthis.updateStateProp(obj[head], value, rest.join());
ma.mehralian
8

ES6 hat eine ziemlich coole Möglichkeit, dies auch mit dem Namen der berechneten Eigenschaften und dem Rest-Parameter zu tun .

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

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

Wenn levelThreees sich um eine dynamische Eigenschaft handelt, dh zum Festlegen einer Eigenschaft levelTwo, können Sie verwenden, [propertyName]: "I am now updated!"wo propertyNameder Name der Eigenschaft enthalten ist levelTwo.

ron4ex
quelle
7

Lodash hat eine Methode namens Update , die genau das tut, was Sie brauchen.

Diese Methode empfängt die folgenden Parameter:

  1. Das zu aktualisierende Objekt
  2. Der Pfad der zu aktualisierenden Eigenschaft (die Eigenschaft kann tief verschachtelt sein)
  3. Eine Funktion, die den zu aktualisierenden Wert zurückgibt (unter Angabe des ursprünglichen Werts als Parameter)

In Ihrem Beispiel würde es so aussehen:

_.update(obj, 'db.mongodb.user', function(originalValue) {
  return 'root'
})
brafdlog
quelle
7

Inspiriert von der Antwort von @ 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;
}

Beispiel:

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
quelle
2

Ich habe das Wesentliche erstellt , um obj-Werte anhand einer Zeichenfolge basierend auf der richtigen Antwort festzulegen und abzurufen. Sie können es herunterladen oder als npm / yarn-Paket verwenden.

// 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
quelle
1

Wenn Sie nur tiefere verschachtelte Objekte ändern müssen, besteht eine andere Methode darin, auf das Objekt zu verweisen. Da JS-Objekte von ihren Referenzen behandelt werden, können Sie eine Referenz auf ein Objekt erstellen, auf das Sie Zugriff auf einen Zeichenfolgenschlüssel haben.

Beispiel:

// 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
    }
};
aggregat1166877
quelle
1

Ein anderer Ansatz besteht darin, die Rekursion zu verwenden, um durch das Objekt zu graben:

(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"}}
ed.
quelle
1

Ich musste dasselbe erreichen, aber in Node.js ... Also fand ich dieses schöne Modul: https://www.npmjs.com/package/nested-property

Beispiel:

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
quelle
wie man nach komplexen verschachtelten Objekten löst. `` `const x = {'eins': 1, 'zwei': 2, 'drei': {'eins': 1, 'zwei': 2, 'drei': [{'eins': 1}, { 'eins': 'EINS'}, {'eins': 'I'}]}, 'vier': [0, 1, 2]}; console.log (np.get (x, 'drei.drei [0] .one')); `` `
Sumukha HS
1

Ich habe meine eigene Lösung mit reinem es6 und Rekursion entwickelt, die das ursprüngliche Objekt nicht mutiert.

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
quelle
Sie können den ersten if-Block entfernen, indem Sie "obj" mit einem Standardwert deklarieren. SetNestedProp = (obj = {}, Schlüssel, Wert) => {
blindChicken
1
Yh schön. Ein Rückblick könnte das Schlüsselargument auch in situ zerstören und eine weitere Codezeile speichern
Henry Ing-Simmons
Im Grunde genommen jetzt ein Einzeiler 👍
Henry Ing-Simmons
0

Wenn Sie möchten, dass eine Funktion vorhanden ist, für die vorherige Eigenschaften erforderlich sind, können Sie Folgendes verwenden. Außerdem wird ein Flag zurückgegeben, das angibt, ob es gelungen ist, die verschachtelte Eigenschaft zu finden und festzulegen.

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
quelle
0

Inspiriert von ClojureScript assoc-in( https://github.com/clojure/clojurescript/blob/master/src/main/cljs/cljs/core.cljs#L5280 ) unter Verwendung der Rekursion:

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

Hinweis:

Mit der bereitgestellten Implementierung

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

kehrt zurück

{"a": 1}

Ich würde es vorziehen, dass es in diesem Fall einen Fehler auslöst. Falls gewünscht, können Sie einen Check-in hinzufügen, um assoczu überprüfen, ob mes sich entweder um ein Objekt oder ein Array handelt, und andernfalls einen Fehler auslösen.

Lucas Leblow
quelle
0

Ich habe versucht, diese Set-Methode kurz zu schreiben , sie kann jemandem helfen!

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
quelle