Динамічно задана властивість вкладеного об'єкта


84

У мене є об’єкт, який може бути глибиною будь-якої кількості рівнів і мати будь-які існуючі властивості. Наприклад:

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

На цьому я хотів би встановити (або перезаписати) такі властивості:

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

Де рядок властивостей може мати будь-яку глибину, а значення може бути будь-якого типу / речі.
Об'єкти та масиви як значення не потрібно об'єднувати, якщо ключ властивості вже існує.

Попередній приклад дасть такий об'єкт:

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

Як я можу реалізувати таку функцію?


Яким повинен бути результат set('foo', 'bar'); set('foo.baz', 'qux');, де fooспочатку тримається, а Stringпотім стає Object? Що відбувається 'bar'?
Джонатан Лоновський


міг би отримати цю допомогу: stackoverflow.com/questions/695050/…
Рене М.

1
Якщо ви видалите set()метод і просто у obj.db.mongodb.user = 'root';вас буде саме те, чого ви, здається, хочете?
adeneo

@JonathanLonowski Lonowski barотримує заміну Object. @adeneo та @rmertins Справді :) Але я мушу обернути якусь іншу логіку, на жаль. @ Роберт Леві Я знайшов це і запустив доступ, але налаштування здається набагато складнішим ...
Джон Б.

Відповіді:


92

Ця функція, використовуючи вказані вами аргументи, повинна додавати / оновлювати дані в objконтейнері. Зверніть увагу, що вам потрібно відстежувати, які елементи у objсхемі є контейнерами, а які значеннями (рядки, ints тощо), інакше ви почнете викидати виключення.

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

2
@ bpmason1 не могли б ви пояснити, чому ви використовували var schema = objзамість цього objскрізь?
sman591

3
@ sman591 schema- це вказівник, який переміщується по шляху за допомогою schema = schema[elem]. Тож після циклу for schema[pList[len - 1]]вказує на mongo.db.user у obj.
webjay

це вирішило мою проблему, дякую, не вдалося знайти це в документах MDN. Але я маю ще один сумнів: якщо оператор присвоєння посилається на внутрішні об'єкти, то як зробити окремий об'єкт2 з об'єкта1, щоб зміни, внесені в об'єкт2, не відображались на об'єкті1.
Onix

@Onix Для цього ви можете використовувати cloneDeepфункцію lodash .
Aakash Thakur

@Onix const clone = JSON.parse (JSON.stringify (obj))
Майк Макуч,

72

Лодаш має метод _.set () .

_.set(obj, 'db.mongodb.user', 'root');
_.set(obj, 'foo.bar', 'baz');

1
Чи можна використовувати його для встановлення значення і для ключа? якщо так, можете поділитися прикладом. Дякую
sage poudel

Це чудово, але як би ви відстежували / визначали шлях?
Том

19

Трохи пізно, але ось небібліотечна, простіша відповідь:

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

Ця функція, яку я зробив, може робити саме те, що вам потрібно, і трохи більше.

скажімо, ми хочемо змінити цільове значення, глибоко вкладене в цей об’єкт:

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

Тому ми б називали свою функцію так:

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

призведе до:

myObj = {рівень1: {рівень2: {ціль: 3}}}

Встановлення рекурсивному прапору набору значення true встановить об’єкти, якщо вони не існують.

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

призведе до цього:

obj = myObj = {
    new: {
         path: {
             target: 3
         }
    },
    level1: {
        level2: {
           target: 3
       }
    }
}

1
Використовував цей код, чисто і просто. Замість обчислень levelя використав reduceтретій аргумент.
Хуан Ланус,

Я вважаю, що це levelмає бути +1 або path.length-1
ThomasReggi

12

Ми можемо використовувати функцію рекурсії:

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

Це все простіше!


3
виглядає добре! просто потрібно перевірити параметр obj, щоб переконатися, що він не хибний, викине помилку, якщо якийсь реквізит по ланцюжку не існує.
C Smith

2
ви можете просто використовувати path.slice (1);
Маркос Перейра,

1
Відмінна відповідь, приємне та стисле рішення.
чим

Я вірю в те, що якщо твердження if, це мало бути, obj[path[0]] = value;оскільки pathвоно завжди типу string[], навіть коли залишився лише 1 рядок.
Валорад,

Об'єкти Javascript повинні працювати, використовуючи obj[['a']] = 'new value'. Перевірте код: jsfiddle.net/upsdne03
Хема Відаль,

11

Я просто пишу невеличку функцію за допомогою рекурсії ES6 + для досягнення мети.

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

Я багато використовував його для реагування на стан оновлення, він працював у мене досить добре.


1
це було зручно, мені довелося поставити toString () на proPath, щоб він працював із вкладеними властивостями, але після цього він працював чудово. const [head, ... rest] = propPath.toString (). split ('.');
WizardsOfWor

1
@ user738048 @ Bruno-Joaquim рядок this.updateStateProp(obj[head], value, rest);повинен бутиthis.updateStateProp(obj[head], value, rest.join());
ma.mehralian

8

ES6 має досить класний спосіб зробити це теж за допомогою обчислюваної назви властивості та параметра відпочинку .

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

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

Якщо levelThree є динамічною властивістю, тобто для встановлення будь-якої властивості в levelTwo, ви можете використовувати [propertyName]: "I am now updated!"де propertyNameмістить ім'я властивості в levelTwo.


7

Lodash має метод, який називається update який робить саме те, що вам потрібно.

Цей метод отримує такі параметри:

  1. Об’єкт для оновлення
  2. Шлях властивості, яку потрібно оновити (властивість може бути глибоко вкладена)
  3. Функція, яка повертає значення для оновлення (з урахуванням вихідного значення як параметра)

У вашому прикладі це виглядатиме так:

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

7

Натхненний відповіддю @ 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;
}

Приклад:

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' } }

2

Я створив суть для встановлення та отримання значень obj за допомогою рядка на основі правильної відповіді. Ви можете завантажити його або використовувати як пакет npm / пряжі.

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

1

Якщо вам потрібно змінити лише глибше вкладені об'єкти, то іншим методом може бути посилання на об'єкт. Оскільки об’єкти JS обробляються за допомогою їх посилань, ви можете створити посилання на об’єкт, до якого ви маєте доступ до рядкового ключа.

Приклад:

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

1

Інший підхід полягає у використанні рекурсії для копання об’єкта:

(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"}}

1

Мені потрібно було досягти того самого, але в Node.js ... Отже, я знайшов цей приємний модуль: https://www.npmjs.com/package/nested-property

Приклад:

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

як вирішити складні вкладені об'єкти. `` 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')); ``
Сумуха HS

1

Я придумав своє власне рішення, використовуючи чистий es6 та рекурсію, яка не мутує вихідний об’єкт.

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


Ви можете усунути перший блок if, оголосивши "obj" зі значенням за замовчуванням setNestedProp = (obj = {}, ключі, значення) => {
blindChicken

1
Yh приємно. Озирнувшись назад, можна деструктурувати аргумент ключів in situ і зберегти ще один рядок коду
Генрі Інг-Сіммонс,

В основному зараз один лайнер 👍
Генрі Інг-Сіммонс,

0

Якщо ви хочете, щоб функція вимагала існування попередніх властивостей, тоді ви можете використовувати щось подібне, вона також поверне прапор із зазначенням того, чи вдалося знайти та встановити вкладене властивість.

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

0

Натхненний ClojureScript's assoc-in( https://github.com/clojure/clojurescript/blob/master/src/main/cljs/cljs/core.cljs#L5280 ), використовуючи рекурсію:

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

Примітка:

За умови реалізації,

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

повертається

{"a": 1}

Я вважаю за краще, щоб у цьому випадку він видавав помилку. За бажанням ви можете додати реєстрацію, assocщоб переконатися m, що це об’єкт або масив, і в іншому випадку вивести помилку.


0

Я намагався написати цей метод набору коротше , це може комусь допомогти!

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


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.