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 可以是“ Module _ Floorplan _ Image _ Src”,因此我想动态地使用一个函数,比如..。

createSetting (newSettingNameArray, newSettingValue);


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

有人能帮我解决如何做到这一点动态?

我假设这里必须有一个 for... 循环来遍历数组,但是我还没有找到创建嵌套对象的方法。

如果你已经读了这么多,非常感谢你花时间阅读,即使你不能帮助。

119357 次浏览
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;
}

Usage:

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

try using recursive function:

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

I think, this is shorter:

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

Put in a function, short and fast (no recursion).

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.

It skips already existing parts of the hierarchy. Useful if you are not sure whether the hierarchy was already created.

Or:

A fancier version where you can directly assign the value to the last object in the hierarchy, and you can chain function calls because it returns the last object.

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

Note: if your hierarchy needs to be built from values other that standard objects (ie. not {}), see also TimDog's answer below.

Edit: uses regular loops instead of for...in loops. It's safer in cases where a library modifies the Array prototype.

Here is a simple tweak to jlgrall's answer that allows setting distinct values on each element in the nested hierarchy:

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

Hope it helps.

I found @jlgrall's answer was great but after simplifying it, it didn't work in Chrome. Here's my fixed should anyone want a lite version:

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

I hope this is useful and relevant. Sorry, I've just smashed this example out...

Another recursive solution:

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

Example usage:

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

You can define your own Object methods; also I'm using underscore for brevity:

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

Sample usage:

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

Appreciate that this question is mega old! But after coming across a need to do something like this in node, I made a module and published it to 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

A snippet for those who need to create a nested objects with support of array keys to set a value to the end of path. Path is the string like: modal.product.action.review.2.write.survey.data. Based on jlgrall version.

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

Set Nested Data:

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

Get Nested Data:

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

My ES2015 solution. Keeps existing values.

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

Example:

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

Eval is probably overkill but the result is simple to visualize, with no nested loops or recursion.

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

Basically you are progressively writing a string "obj['one']= {}", "obj['one']['two']"= {} and evaling it;

Try this: https://github.com/silkyland/object-to-formdata

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

Result :

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

Here is a functional solution to dynamically create nested objects.

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

Example:

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

I love this ES6 immutable way to set certain value on nested field:

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

And then use it with creating your target object.

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

Using ES6 is shorten. Set your path into an array. first, you have to reverse the array, to start filling the object.

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


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

Inside your loop you can use lodash.set and will create the path for you:

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


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


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

Here is a decomposition to several useful functions, that each preserve existing data. Does not handle arrays.

  • setDeep: Answers question. Non-destructive to other data in the object.
  • setDefaultDeep: Same, but only sets if not already set.
  • setDefault: Sets a key if not already set. Same as Python's setdefault.
  • setStructure: Helper function that builds the path.

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

I'm not sure why anyone would want string paths:

  1. They are ambiguous for keys with periods
  2. You have to build the strings in the first place

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.

function param(obj, path, value, returnObj) {
if (typeof path == 'string') path = path.split(".");
var child = obj;
path.forEach((key, i) => {
if (!(key in child)) {
child[key] = (i < path.length-1) ? {} : value || {};
}
child = child[key];
});
return returnObj ? obj : child;
}


var x = {};
var xOut = param(x, "y.z", "setting")
console.log(xOut);
xOut = param(x, "y.z", "overwrite") // won't set
console.log(xOut);
xOut = param(x, "y.a", "setting2")
console.log(xOut);
xOut = param(x, "y.a", "setting2", true) // get object rather than deepest node.
console.log(xOut);

You can also do something where numeric keys are placed in arrays (if they don't already exist). Note that numeric keys won't convert to arrays for the first element of the path, since that's set by the type of your base-object.

function isNumber(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
}


function param(obj, path, value, returnObj) {
if (typeof path == 'string') path = path.split(".");
var child = obj;
path.forEach((key, i) => {
var nextKey = path[i+1];
if (!(key in child)) {
child[key] = (nextKey == undefined && value != undefined
? value
: isNumber(nextKey)
? []
: {});
}
child = child[key];
});
return returnObj ? obj : child;
}


var x = {};


var xOut = param(x, "y.z", "setting")
console.log(xOut);
xOut = param(x, "y.z", "overwrite") // won't set
console.log(xOut);
xOut = param(x, "y.a", "setting2")
console.log(xOut);
xOut = param(x, "y.a", "setting2", true) // get object rather than deepest node.
xOut = param(x, "1.0.2.a", "setting")
xOut = param(x, "1.0.1.a", "try to override") // won't set
xOut = param(x, "1.0.5.a", "new-setting", true) // get object rather than deepest node.
console.log(xOut);

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!"]
function initPath(obj, path) {
path.split('.').reduce((o, key) => (
Object.assign(o, {[key]: Object(o[key])}),
o[key]
), obj);
return obj;
}

Usage

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

Inspired by ImmutableJS setIn method which will never mutate the original. This works with mixed array and object nested values.

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 has a _.set method to achieve this

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

simple answer. on es6, im using this

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
}

example json

const obj = {
b: 'hello'
}

you can add new key

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

and you get like bellow

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

function createObj(keys, value) {
let obj = {}
let schema = obj
keys = keys.split('.')
  

for (let i = 0; i < keys.length - 1; i++) {
schema[keys[i]] = {}
schema = schema[keys[i]]
}
  

schema[keys.pop()] = value


return obj
}




let keys = 'value1.value2.value3'
let value = 'Hello'


let obj = createObj(keys, value)