I'm having an issue with trying to populate a multidimensional object in javascript before all of the dimensions are defined.
For example this is what I want to do:
var multiVar = {};
var levelone = 'one';
var leveltwo = 'two';
multiVar[levelone][leveltwo]['levelthree'] = 'test'
It would be extremely cumbersome to have to create each dimension with a line like this:
var multiVar = {};
multiVar['levelone'] = {};
multiVar['levelone']['leveltwo'] = {};
multiVar['levelone']['leveltwo']['levelthree'] = 'test'
The reason why I need to do it without iterative priming is because I don't know how many dimensions there will be nor what the keys it will have. It needs to be dynamic.
Is there a way to do that in a dynamic way?
You could write a function which ensures the existence of the necessary "dimensions", but you won't be able to use dot or bracket notation to get this safety. Something like this:
function setPropertySafe(obj)
{
function isObject(o)
{
if (o === null) return false;
var type = typeof o;
return type === 'object' || type === 'function';
}
if (!isObject(obj)) return;
var prop;
for (var i=1; i < arguments.length-1; i++)
{
prop = arguments[i];
if (!isObject(obj[prop])) obj[prop] = {};
if (i < arguments.length-2) obj = obj[prop];
}
obj[prop] = arguments[i];
}
Example usage:
var multiVar = {};
setPropertySafe(multiVar, 'levelone', 'leveltwo', 'levelthree', 'test');
/*
multiVar = {
levelone: {
leveltwo: {
levelthree: "test"
}
}
}
*/
Related
I'm trying to create a JS object dynamically providing a key and a value. The key is in dot notation, so if a string like car.model.color is provided the generated object would be:
{
car: {
model: {
color: value;
}
}
}
The problem has a trivial solution if the key provided is a simple property, but i'm struggling to make it work for composed keys.
My code:
function (key, value) {
var object = {};
var arr = key.split('.');
for(var i = 0; i < arr.length; i++) {
object = object[arr[i]] = {};
}
object[arr[arr.length-1]] = value;
return object;
}
your slightly modified code
function f(key, value) {
var result = object = {};
var arr = key.split('.');
for(var i = 0; i < arr.length-1; i++) {
object = object[arr[i]] = {};
}
object[arr[arr.length-1]] = value;
return result;
}
In the loop you should set all of the props but the last one.
Next set the final property and all set.
If you're using lodash you could use _.set(object, path, value)
const obj = {}
_.set(obj, "car.model.color", "my value")
console.log(obj)
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.15/lodash.min.js"></script>
Use namespace pattern, like the one Addy Osmani shows: http://addyosmani.com/blog/essential-js-namespacing/
Here's the code, pasted for convenience, all credit goes to Addy:
// top-level namespace being assigned an object literal
var myApp = myApp || {};
// a convenience function for parsing string namespaces and
// automatically generating nested namespaces
function extend( ns, ns_string ) {
var parts = ns_string.split('.'),
parent = ns,
pl, i;
if (parts[0] == "myApp") {
parts = parts.slice(1);
}
pl = parts.length;
for (i = 0; i < pl; i++) {
//create a property if it doesnt exist
if (typeof parent[parts[i]] == 'undefined') {
parent[parts[i]] = {};
}
parent = parent[parts[i]];
}
return parent;
}
// sample usage:
// extend myApp with a deeply nested namespace
var mod = extend(myApp, 'myApp.modules.module2');
function strToObj(str, val) {
var i, obj = {}, strarr = str.split(".");
var x = obj;
for(i=0;i<strarr.length-1;i++) {
x = x[strarr[i]] = {};
}
x[strarr[i]] = val;
return obj;
}
usage: console.log(strToObj("car.model.color","value"));
I would use a recursive method.
var createObject = function(key, value) {
var obj = {};
var parts = key.split('.');
if(parts.length == 1) {
obj[parts[0]] = value;
} else if(parts.length > 1) {
// concat all but the first part of the key
var remainingParts = parts.slice(1,parts.length).join('.');
obj[parts[0]] = createObject(remainingParts, value);
}
return obj;
};
var simple = createObject('simple', 'value1');
var complex = createObject('more.complex.test', 'value2');
console.log(simple);
console.log(complex);
(check the console for the output)
Here's a recursive approach to the problem:
const strToObj = (parts, val) => {
if (!Array.isArray(parts)) {
parts = parts.split(".");
}
if (!parts.length) {
return val;
}
return {
[parts.shift()]: strToObj(parts, val)
};
}
The function below is intended to return the values from a (potentially nested) object as an array - with the list parameter being any object. If I move my break statement to after the for loop, I don't get any errors, but of course then my function doesn't behave as needed. What's wrong with the way I'm using break?
function listToArray(list) {
var objectArray = [];
function objectPeeler() {
let peel = Object.getOwnPropertyNames(list);
for(var i = 0; i < peel.length; i++) {
list[peel[i]] && typeof list[peel[i]] != 'object' ?
objectArray.push(list[peel[i]]):
list[peel[i]] ?
(list = list[peel[i]], objectPeeler()) :
break;
}
return objectArray;
}
objectPeeler();
}
In case anyone else has this issue: ternary operators only work with value expressions, not statements (like break) and aren't meant to be used in these cases.
This works:
function listToArray(list) {
var objectArray = [];
function objectPeeler() {
let peel = Object.getOwnPropertyNames(list);
for(var i = 0; i < peel.length; i++) {
list[peel[i]] != null && typeof list[peel[i]] != 'object' ?
objectArray.push(list[peel[i]]):
list[peel[i]] ?
(list = list[peel[i]], objectPeeler()): null;
}
}
objectPeeler();
return objectArray;
}
But using the jquery .next method allows a better solution:
function listToArray(list) {
var array = [];
for (var obj = list; obj; obj = obj.next)
array.push(obj.value);
return array;
}
why not writing something like this :
var obj = { 0: "a", 1: "b", 2: "c"}; //test target
var objectArray = [];
var keyArray = Object.getOwnPropertyNames(obj);
for (var i = 0; i < keyArray.length; i++) objectArray.push(obj[keyArray[i]]);
console.log(objectArray); // test result
This question already has answers here:
Access object child properties using a dot notation string [duplicate]
(13 answers)
Closed 8 years ago.
I'm trying to access a property of an object dynamically with a string.
For example:
".id.public" -> anyObject["id"]["public"]
The problem - I don't know how many arguments I have (for example ".id" or ".id.public" or ".id.public.whatever".
I made a little workaround:
var currentSplit = anyObject;
var splitted = "id.public".split("\.");
splitted.forEach(function(s) { currentSplit = currentSplit[s]; });
When I try now to override the object property I will override the reference and not the object property.
currentSplit = "test";
I tried already stuff like anyObject["id.public"] = "test"; but it didn't work.
The deep-get-set library does what you want:
function get (obj, path) {
var keys = path.split('.');
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (!obj || !hasOwnProperty.call(obj, key)) {
obj = undefined;
break;
}
obj = obj[key];
}
return obj;
}
function set (obj, path, value) {
var keys = path.split('.');
for (var i = 0; i < keys.length - 1; i++) {
var key = keys[i];
if (deep.p && !hasOwnProperty.call(obj, key)) obj[key] = {};
obj = obj[key];
}
obj[keys[i]] = value;
return value;
}
Yet another way for setting value
function setVal(obj, path, val){
var paths = path.split('.'),
curProp = obj;
for(var i=0;i<paths.length-1;i++){
curProp = curProp[paths[i]];
}
curProp[paths[i]] = val;
}
and use it like
setVal(anyObj, "id.public", 'newValue');
You can't do that without the help of a little code like this:
var mapToProperty = function(obj, path, value) {
if (!path) return obj;
var parts = path.split("."),
p = parts[0],
v = (typeof obj[p] === "function") ? obj[p](value) : (parts.length !==1 || !value) ? obj[p] : (obj[p] = value), value ;
if (parts.length == 1) return v;
return mapToProperty(v, parts.slice(1).join("."), value);
}
// use it like this
var myvalue = mapToProperty(myObj, "address.street")
// you can map into your objects as far as you want. obj1.obj2.obj3.prop
// you can set as well :-)
mapToProperty(myObj, "address.street", "This is great!")
For example if I have something like so:
var Constants = {
scope:{
namespaceA: { A_X: "TEST_AX" , A_Y: "TEST_AY" },
namespaceN: { N_X: "TEST_NX" , N_Y: "TEST_NY" }
}
_mapping: [],
getMapping: function(){...}
}
var flattenList = flatten(Constants.scope); //returns ["TEST_AX","TEST_AY","TEST_NX","TEST_NY"]
var anotherWayFlattened = flatten(Constants.scope.namespaceA,Constants.scope.namespaceB); //returns same result as above
EDIT: one way would be to iterate over the scope via for-each loop but I was looking for something more elegent?
DOUBLE EDIT: ok I just whipped something up like so:
var flattenedList = (function(list){
var flatList = []
$.each(list,function(i,items){
for(var p in items) flatList.push(items[p]);
})
return flatList;
})([Constants.scope.namespaceA,Constants.scope.namespaceB]);
but was wondering if we can avoid passing in the particular property and just pass in Constants and search for the list of namespaces
[Constants.scope.namespaceA,Constants.scope.namespaceB]
I'm wondering why you pass the sub-objects explicitly in an array. Why not just pass the whole Constants.scope object?
var flattenedList = (function(obj){
var flatList = []
for (var prop in obj) {
var items = obj[prop];
for (var p in items)
flatList.push(items[p]);
}
return flatList;
})(Constants.scope);
From your comment it looks like you wanted this:
var flattenedList = (function(obj, test){
var flatList = []
for (var prop in obj) {
if (!test(prop))
continue;
var items = obj[prop];
for (var p in items)
flatList.push(items[p]);
}
return flatList;
})(Constants, function(name) {
return name.substr(0, 9) == "namespace";
// or maybe
return /^namespace[A-Z]$/.test(name);
});
if you wanted to recurse to any (non cyclical!) depth, you could do this :
function flattenList(list, accumulator){
accumulator = accumulator || [];
for(var p in list){
if(list.hasOwnProperty(p)) {
if(typeof list[p] === "string") {
accumulator.push(list[p]);
} else if(typeof list[p] === "object") { // this is not a reliable test!
flattenList(list[p], accumulator);
}
}
}
return accumulator;
}
This code makes a number of assumptions - we only have strings at the end of our objects etc. Alternatively, if you know the depth in advance, your current solution can be optimized by using concat :
var flattenedList = (function(list){
return Array.prototype.concat.apply([], list);
})([Constants.scope.namespaceA,Constants.scope.namespaceB]);
Here's an approach that allows for deeper nesting. I know that wasn't part of the goals, but I found it a more interesting problem. :-)
var flatten = (function() {
var toString = Object.prototype.toString, slice = Array.prototype.slice;
var flatten = function(input, output) {
var value;
output = (toString.call(output) == "[object Array]") ? output : [];
for (name in input) {if (input.hasOwnProperty(name)) {
value = input[name];
if (toString.call(value) == "[object Object]") {
flatten(value, output);
} else {
output.push(value);
}
}};
return output;
};
var merge = function(first, second) {
return first.concat(second);
}
return function() {
return slice.call(arguments).map(flatten).reduce(merge);
};
}());
This allows either approach:
flatten(Constants.scope);
flatten(Constants.scope.namespaceA, Constants.scope.namespaceN);
You can pass in as many separate arguments as you like, or one argument. They'll all be searched to arbitrary depths.
For some environments, you might have to shim Array.prototype functions map and reduce.
Is there anyway, either natively or through a library, to use autovivification on Javascript objects?
IE, assuming foo is an object with no properties, being able to just do foo.bar.baz = 5 rather than needing foo.bar = {}; foo.bar.baz = 5.
You can't do it exactly with the syntax you want. But as usual, in JS you can write your own function:
function set (obj,keys,val) {
for (var i=0;i<keys.length;i++) {
var k = keys[i];
if (typeof obj[k] == 'undefined') {
obj[k] = {};
}
obj = obj[k];
}
obj = val;
}
so now you can do this:
// as per you example:
set(foo,['bar','baz'],5);
without worrying if bar or baz are defined. If you don't like the [..] in the function call you can always iterate over the arguments object.
Purely natively, I don't think so. undefined isn't extensible or changeable and that's about the only way I could imagine doing it without passing it through a function.
I had a desire to do this, so I wrote a package to handle it.
% npm install autovivify
% node
> Av = require('autovivify')
> foo = new Av()
{}
> foo.bar.baz = 5
5
> foo
{ bar: { baz: 5 } }
>
It'll even do arrays with numeric subscripts:
> foo = new Av()
> foo.bar.baz[0] = 'hum'
> foo
{ bar: { baz: [ 'hum' ] } }
Or you can use an eval-based solution. It's ugly, not recommended.
function av(xpr) {
var res = "";
var pos = 0;
while (true) {
var pos = xpr.indexOf("[",pos);
if (pos == -1) break;
var frag = xpr.substr(0,pos);
pos++;
res += "if (typeof(" + frag + ") != 'object') " + frag + " = {};\n";
} // while
return res + xpr;
} // av()
function main() {
var a = {};
a["keep"] = "yep";
eval(av('a[1][1]["xx"] = "a11xx"; '));
eval(av('a[1][2]["xx"] = "a12xx"; '));
console.log(a);
} // main()
#slebetman's code doesn't seem to work. The last key should not be assigned an empty object, but rather the val. This code worked:
function autoviv(obj,keys,val) {
for (var i=0; i < keys.length; i++) {
var k = keys[i];
if (typeof obj[k] === 'undefined') {
if(i === keys.length-1) {
obj[k] = val;
return;
}
obj[k] = {};
}
obj = obj[k];
}
}
foo = {}
autoviv(foo,['bar','baz'],5);
console.log(foo.bar.baz);
5