Javascript: как динамически создавать вложенные объекты, используя имена объектов, заданные массивом

Я надеюсь, что кто-то может помочь мне с этим Javascript.

У меня есть объект под названием "Настройки", и я хотел бы написать функцию, которая добавляет новые настройки для этого объекта.

Имя и значение новой настройки предоставляются в виде строк. Строка, дающая имя параметра, затем разделяется подчеркиванием на массив. Новый параметр должен быть добавлен к существующему объекту "Настройки" путем создания новых вложенных объектов с именами, заданными каждой частью массива, кроме последней части, которая должна быть строкой, дающей значение параметра. Затем я должен иметь возможность обратиться к настройке и, например, предупредить ее значение. Я могу сделать это статичным образом, как это...

var Settings = {};
var newSettingName = "Modules_Video_Plugin";
var newSettingValue = "JWPlayer";
var newSettingNameArray = newSettingName.split("_");

Settings[newSettingNameArray[0]] = {};
Settings[newSettingNameArray[0]][newSettingNameArray[1]] = {};
Settings[newSettingNameArray[0]][newSettingNameArray[1]][newSettingNameArray[2]] = newSettingValue;

alert(Settings.Modules.Mediaplayers.Video.Plugin);

... часть, которая создает вложенные объекты, делает это...

Settings["Modules"] = {};
Settings["Modules"]["Video"] = {};
Settings["Modules"]["Video"]["Plugin"] = "JWPlayer";

Однако, поскольку количество частей, составляющих имя параметра, может варьироваться, например, newSettingName может быть "Modules_Floorplan_Image_Src", я хотел бы сделать это динамически, используя такую ​​функцию, как...

createSetting (newSettingNameArray, newSettingValue);

function createSetting(setting, value) {
    // code to create new setting goes here
}

Может кто-нибудь помочь мне решить, как сделать это динамически?

Я предполагаю, что там должен быть цикл for... для перехода через массив, но я не смог найти способ создания вложенных объектов.

Если у вас так далеко, большое спасибо, что нашли время, чтобы прочитать, даже если вы не можете помочь.

26 ответов

Решение
function assign(obj, keyPath, value) {
   lastKeyIndex = keyPath.length-1;
   for (var i = 0; i < lastKeyIndex; ++ i) {
     key = keyPath[i];
     if (!(key in obj))
       obj[key] = {}
     obj = obj[key];
   }
   obj[keyPath[lastKeyIndex]] = value;
}

Использование:

var settings = {};
assign(settings, ['Modules', 'Video', 'Plugin'], 'JWPlayer');

Вставьте функцию, короткую и быструю (без рекурсии).

var createNestedObject = function( base, names ) {
    for( var i = 0; i < names.length; i++ ) {
        base = base[ names[i] ] = base[ names[i] ] || {};
    }
};

// Usage:
createNestedObject( window, ["shapes", "triangle", "points"] );
// Now window.shapes.triangle.points is an empty object, ready to be used.

Он пропускает уже существующие части иерархии. Полезно, если вы не уверены, была ли иерархия уже создана.

Или же:

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

// Function: createNestedObject( base, names[, value] )
//   base: the object on which to create the hierarchy
//   names: an array of strings contaning the names of the objects
//   value (optional): if given, will be the last object in the hierarchy
// Returns: the last object in the hierarchy
var createNestedObject = function( base, names, value ) {
    // If a value is given, remove the last name and keep it for later:
    var lastName = arguments.length === 3 ? names.pop() : false;

    // Walk the hierarchy, creating new objects where needed.
    // If the lastName was removed, then the last object is not set yet:
    for( var i = 0; i < names.length; i++ ) {
        base = base[ names[i] ] = base[ names[i] ] || {};
    }

    // If a value was given, set it to the last name:
    if( lastName ) base = base[ lastName ] = value;

    // Return the last object in the hierarchy:
    return base;
};

// Usages:

createNestedObject( window, ["shapes", "circle"] );
// Now window.shapes.circle is an empty object, ready to be used.

var obj = {}; // Works with any object other that window too
createNestedObject( obj, ["shapes", "rectangle", "width"], 300 );
// Now we have: obj.shapes.rectangle.width === 300

createNestedObject( obj, "shapes.rectangle.height".split('.'), 400 );
// Now we have: obj.shapes.rectangle.height === 400

Примечание: если ваша иерархия должна быть построена из значений, отличных от стандартных объектов (т.е. не {}), см. также ответ TimDog ниже.

Редактировать: использует обычные циклы вместо for...in петли. Это безопаснее в тех случаях, когда библиотека модифицирует прототип Array.

Мое решение ES2015. Сохраняет существующие значения.

const set = (obj, path, val) => { 
    const keys = path.split('.');
    const lastKey = keys.pop();
    const lastObj = keys.reduce((obj, key) => 
        obj[key] = obj[key] || {}, 
        obj); 
    lastObj[lastKey] = val;
};

Пример:

const obj = {'a': {'prop': {'that': 'exists'}}};
set(obj, 'a.very.deep.prop', 'value');
console.log(JSON.stringify(obj));
// {"a":{"prop":{"that":"exists"},"very":{"deep":{"prop":"value"}}}}

Использование ES6 сокращено. Установите свой путь в массив. Во-первых, вы должны обратить массив, чтобы начать заполнение объекта.

let obj = ['a','b','c'] // {a:{b:{c:{}}}
obj.reverse();

const nestedObject = obj.reduce((prev, current) => (
    {[current]:{...prev}}
), {});

Еще одно рекурсивное решение:

var nest = function(obj, keys, v) {
    if (keys.length === 1) {
      obj[keys[0]] = v;
    } else {
      var key = keys.shift();
      obj[key] = nest(typeof obj[key] === 'undefined' ? {} : obj[key], keys, v);
    }

    return obj;
};

Пример использования:

var dog = {bark: {sound: 'bark!'}};
nest(dog, ['bark', 'loudness'], 66);
nest(dog, ['woff', 'sound'], 'woff!');
console.log(dog); // {bark: {loudness: 66, sound: "bark!"}, woff: {sound: "woff!"}}

Мне нравится этот неизменный способ ES6 для установки определенного значения во вложенном поле:

const setValueToField = (fields, value) => {
  const reducer = (acc, item, index, arr) => ({ [item]: index + 1 < arr.length ? acc : value });
  return fields.reduceRight(reducer, {});
};

А затем используйте его для создания целевого объекта.

const targetObject = setValueToField(['one', 'two', 'three'], 'nice');
console.log(targetObject); // Output: { one: { two: { three: 'nice' } } }

Вот простая настройка ответа jlgrall, позволяющая устанавливать различные значения для каждого элемента во вложенной иерархии:

var createNestedObject = function( base, names, values ) {
    for( var i in names ) base = base[ names[i] ] = base[ names[i] ] || (values[i] || {});
};

Надеюсь, поможет.

В Lodash есть метод _.set для достижения этой цели.

      let obj = {}

_.set(obj, ['a', 'b', 'c', 'd'], 'e')

or

_.set(obj, 'a.b.c.d', 'e')

// which generate the following object
{
   "a": {
      "b": {
         "c": {
            "d": "e"
         }
      }
   }
}

Вот функциональное решение для динамического создания вложенных объектов.

const nest = (path, obj) => {
  const reversedPath = path.split('.').reverse();

  const iter = ([head, ...tail], obj) => {
    if (!head) {
      return obj;
    }
    const newObj = {[head]: {...obj}};
    return iter(tail, newObj);
  }
  return iter(reversedPath, obj);
}

Пример:

const data = {prop: 'someData'};
const path = 'a.deep.path';
const result = nest(path, data);
console.log(JSON.stringify(result));
// {"a":{"deep":{"path":{"prop":"someData"}}}}

Вдохновленный методом setIn ImmutableJS, который никогда не изменит оригинал. Это работает со смешанными массивами и вложенными значениями объектов.

      function setIn(obj = {}, [prop, ...rest], value) {
    const newObj = Array.isArray(obj) ? [...obj] : {...obj};
    newObj[prop] = rest.length ? setIn(obj[prop], rest, value) : value;
    return newObj;
}

var obj = {
  a: {
    b: {
      c: [
        {d: 5}
      ]
    }
  }
};

const newObj = setIn(obj, ["a", "b", "c", 0, "x"], "new");

//obj === {a: {b: {c: [{d: 5}]}}}
//newObj === {a: {b: {c: [{d: 5, x: "new"}]}}}

Внутри вашего цикла вы можете использовать lodash.set и создаст для вас путь:

...
const set = require('lodash.set');

const p = {};
const [type, lang, name] = f.split('.');
set(p, [lang, type, name], '');

console.log(p);
// { lang: { 'type': { 'name': '' }}}

Цените, что этот вопрос мега старый! Но, столкнувшись с необходимостью сделать что-то подобное в узле, я создал модуль и опубликовал его в npm. Nestob

var nestob = require('nestob');

//Create a new nestable object - instead of the standard js object ({})
var newNested = new nestob.Nestable();

//Set nested object properties without having to create the objects first!
newNested.setNested('biscuits.oblong.marmaduke', 'cheese');
newNested.setNested(['orange', 'tartan', 'pipedream'], { poppers: 'astray', numbers: [123,456,789]});

console.log(newNested, newNested.orange.tartan.pipedream);
//{ biscuits: { oblong: { marmaduke: 'cheese' } },
  orange: { tartan: { pipedream: [Object] } } } { poppers: 'astray', numbers: [ 123, 456, 789 ] }

//Get nested object properties without having to worry about whether the objects exist
//Pass in a default value to be returned if desired
console.log(newNested.getNested('generic.yoghurt.asguard', 'autodrome'));
//autodrome

//You can also pass in an array containing the object keys
console.log(newNested.getNested(['chosp', 'umbridge', 'dollar'], 'symbols'));
//symbols

//You can also use nestob to modify objects not created using nestob
var normalObj = {};

nestob.setNested(normalObj, 'running.out.of', 'words');

console.log(normalObj);
//{ running: { out: { of: 'words' } } }

console.log(nestob.getNested(normalObj, 'random.things', 'indigo'));
//indigo
console.log(nestob.getNested(normalObj, 'improbable.apricots'));
//false
      function initPath(obj, path) {
  path.split('.').reduce((o, key) => (
    Object.assign(o, {[key]: Object(o[key])}),
    o[key]
  ), obj);
  return obj;
}

использование

      const obj = { a: { b: 'value1' } };
initPath(obj, 'a.c.d').a.c.d='value2';
/*
{
  "a": {
    "b": "value1",
    "c": {
      "d": "value2"
    }
  }
}
*/

Попробуйте это: https://github.com/silkyland/object-to-formdata

var obj2fd = require('obj2fd/es5').default
var fd = obj2fd({
             a:1,
             b:[
                {c: 3},
                {d: 4}
             ]
})

Результат:

fd = [
       a => 1,
       b => [
         c => 3,
         d => 4
       ]
]

Фрагмент для тех, кому необходимо создать вложенные объекты с поддержкой ключей массива, чтобы установить значение в конец пути. Путь это строка вроде: modal.product.action.review.2.write.survey.data, Основано на версии jlgrall.

var updateStateQuery = function(state, path, value) {
    var names = path.split('.');
    for (var i = 0, len = names.length; i < len; i++) {
        if (i == (len - 1)) {
            state = state[names[i]] = state[names[i]] || value;
        }
        else if (parseInt(names[i+1]) >= 0) {
            state = state[names[i]] = state[names[i]] || [];
        }
        else {
            state = state[names[i]] = state[names[i]] || {};
        }
    }
};

Я нашел ответ @jlgrall великолепным, но после упрощения он не работал в Chrome. Вот мое исправление, если кто-то хочет облегченную версию:

var callback = 'fn.item1.item2.callbackfunction',
    cb = callback.split('.'),
    baseObj = window;

function createNestedObject(base, items){
    $.each(items, function(i, v){
        base = base[v] = (base[v] || {});
    });
}

callbackFunction = createNestedObject(baseObj, cb);

console.log(callbackFunction);

Я надеюсь, что это полезно и актуально. Извините, я только что разбил этот пример...

Since I started with something from this page, I wanted to contribute back

Other examples overwrote the final node even if it was set, and that wasn't what I wanted.

Also, if returnObj is set to true, it returns the base object. By default, falsy, it returns the deepest node.

Naturally, when the numeric keys are greater than 0, you might see some undefined gaps.

Practical uses of this might be

      function AddNote(book, page, line) {
  // assume a global global notes collection
  var myNotes = param(allNotes, [book, page, line], []);
  myNotes.push('This was a great twist!')
  return myNotes;
}

var allNotes = {}
var youthfulHopes = AddNote('A Game of Thrones', 4, 2, "I'm already hooked, at least I won't have to wait long for the books to come out!");

console.log(allNotes)
// {"A Game of Thrones": [undefined, undefined, undefined, undefined, [undefined, undefined, ["I'm already hooked, at least I won't have to wait long for the books to come out!"]]]}
console.log(youthfulHopes)
// ["I'm already hooked, at least I won't have to wait long for the books to come out!"]

простой ответ. на es6 я использую это

      const assign = (obj, path, value) => {
  let keyPath = path.split('.')
  let lastKeyIndex = keyPath.length - 1
  for (let i = 0; i < lastKeyIndex; ++i) {
    let key = keyPath[i]
    if (!(key in obj)) {
      obj[key] = {}
    }
    obj = obj[key]
  }
  obj[keyPath[lastKeyIndex]] = value
}

пример json

      const obj = {
  b: 'hello'
}

вы можете добавить новый ключ

      assign(obj, 'c.d.e', 'this value')

и вы получаете как рев

      console.log(obj)
//response example
obj = {
   b: 'hello',
   c: {
      d: {
         e: 'this value'
      }
   }
}

После ответа TimDog это добавляет ценность только последнему элементу.

Результат:

      {
"a": {
    "b": {
        "c": "val"
    }
}

Вот разложение на несколько полезных функций, каждая из которых сохраняет существующие данные. Не обрабатывает массивы.

  • setDeep: Отвечает на вопрос. Неразрушающий для других данных в объекте.
  • setDefaultDeep: То же самое, но устанавливается, только если еще не установлено.
  • setDefault: Устанавливает ключ, если он еще не установлен. То же, что и у Python setdefault.
  • setStructure: Вспомогательная функция, которая строит путь.

         // Create a nested structure of objects along path within obj. Only overwrites the final value.
let setDeep = (obj, path, value) =>
    setStructure(obj, path.slice(0, -1))[path[path.length - 1]] = value

// Create a nested structure of objects along path within obj. Does not overwrite any value.
let setDefaultDeep = (obj, path, value) =>
    setDefault(setStructure(obj, path.slice(0, -1)), path[path.length - 1], value)

// Set obj[key] to value if key is not in object, and return obj[key]
let setDefault = (obj, key, value) =>
    obj[key] = key in obj ? obj[key] : value;

// Create a nested structure of objects along path within obj. Does not overwrite any value.
let setStructure = (obj, path) => 
    path.reduce((obj, segment) => setDefault(obj, segment, {}), obj);



// EXAMPLES
let temp = {};

// returns the set value, similar to assignment
console.log('temp.a.b.c.d:', 
            setDeep(temp, ['a', 'b', 'c', 'd'], 'one'))

// not destructive to 'one'
setDeep(temp, ['a', 'b', 'z'], 'two')

// does not overwrite, returns previously set value
console.log('temp.a.b.z:  ', 
            setDefaultDeep(temp, ['a', 'b', 'z'], 'unused'))

// creates new, returns current value
console.log('temp["a.1"]: ', 
            setDefault(temp, 'a.1', 'three'))

// can also be used as a getter
console.log("temp.x.y.z:  ", 
            setStructure(temp, ['x', 'y', 'z']))


console.log("final object:", temp)

Я не уверен, зачем кому-то нужны строковые пути:

  1. Они неоднозначны для ключей с точками
  2. В первую очередь вам нужно построить струны

Попробуйте использовать рекурсивную функцию:

function createSetting(setting, value, index) {
  if (typeof index !== 'number') {
    index = 0;
  }

  if (index+1 == setting.length ) {
    settings[setting[index]] = value;
  }
  else {
    settings[setting[index]] = {};
    createSetting(setting, value, ++index);
  }
}

Установить вложенные данные:

function setNestedData(root, path, value) {
  var paths = path.split('.');
  var last_index = paths.length - 1;
  paths.forEach(function(key, index) {
    if (!(key in root)) root[key] = {};
    if (index==last_index) root[key] = value;
    root = root[key];
  });
  return root;
}

var obj = {'existing': 'value'};
setNestedData(obj, 'animal.fish.pet', 'derp');
setNestedData(obj, 'animal.cat.pet', 'musubi');
console.log(JSON.stringify(obj));
// {"existing":"value","animal":{"fish":{"pet":"derp"},"cat":{"pet":"musubi"}}}

Получить вложенные данные:

function getNestedData(obj, path) {
  var index = function(obj, i) { return obj && obj[i]; };
  return path.split('.').reduce(index, obj);
}
getNestedData(obj, 'animal.cat.pet')
// "musubi"
getNestedData(obj, 'animal.dog.pet')
// undefined

Я думаю, это короче

Settings = {};
newSettingName = "Modules_Floorplan_Image_Src";
newSettingValue = "JWPlayer";
newSettingNameArray = newSettingName.split("_");

a = Settings;
for (var i = 0 in newSettingNameArray) {
    var x = newSettingNameArray[i];
    a[x] = i == newSettingNameArray.length-1 ? newSettingValue : {};
    a = a[x];
}

Вы можете определить свои собственные методы Object; также я использую подчеркивание для краткости:

var _ = require('underscore');

// a fast get method for object, by specifying an address with depth
Object.prototype.pick = function(addr) {
    if (!_.isArray(addr)) return this[addr]; // if isn't array, just get normally
    var tmpo = this;
    while (i = addr.shift())
        tmpo = tmpo[i];
    return tmpo;
};
// a fast set method for object, put value at obj[addr]
Object.prototype.put = function(addr, val) {
    if (!_.isArray(addr)) this[addr] = val; // if isn't array, just set normally
    this.pick(_.initial(addr))[_.last(addr)] = val;
};

Пример использования:

var obj = { 
           'foo': {
                   'bar': 0 }}

obj.pick('foo'); // returns { bar: 0 }
obj.pick(['foo','bar']); // returns 0
obj.put(['foo', 'bar'], -1) // obj becomes {'foo': {'bar': -1}}

Eval, вероятно, излишний, но результат прост для визуализации, без вложенных циклов или рекурсии.

 function buildDir(obj, path){
   var paths = path.split('_');
   var final = paths.pop();
   for (let i = 1; i <= paths.length; i++) {
     var key = "obj['" + paths.slice(0, i).join("']['") + "']"
     console.log(key)
     eval(`${key} = {}`)
   }
   eval(`${key} = '${final}'`)
   return obj
 }

 var newSettingName = "Modules_Video_Plugin_JWPlayer";
 var Settings = buildDir( {}, newSettingName );

В основном вы постепенно пишете строку "obj['one']= {}", "obj['one']['two']"= {} и оценивая это;

Другие вопросы по тегам