JS Object nesting function works, but no idea why - javascript

So I was working with a colleague who showed me how i could solve a particular problem: how to get a flat object into a nested object. The object properties are named in such a way that they can be sliced into their relevant key named and then nested. His solution works beautifully, but when I ran through his code myself later I couldn't understand how it works.
I'm essentially taking a excel worksheet and creating json from it but for argument sake i'll remove the excel parts and just add the example structure which comes out of the excel parser:
//Example data
var result = {
frame1.title: "heading",
frame1.division: "first",
frame1.data[0].month: "Jan",
frame1.data[0].value: "54",
}
function deepInsert (o, path, value) {
let next, cur = o
path.forEach((p,i) => {
next = cur[p]
if (i == path.length -1) {
cur[p] = value
} else {
if (!next) {
next = cur[p] = {}
}
cur = next
}
})
}
function processWorkbook () {
const result = json.map(item => {
var o = {foo: "bar"}
Object.keys(item).forEach(prop => {
deepInsert(o, prop.split('.'), item[prop])
console.log(JSON.stringify(o, 0, ' '));
})
return o
})
console.log(JSON.stringify(result, 0, ' '))
}
From what I can tell it looks like hes passing in 'o', which is a blank object, then the loop in the deepInsert function is assigning data to not the param, but the object in the calling function, because everytime through the loop, my console log shows more being added to the object.
I also don't understand this part: next = cur[p] = {}. For some reason a triple assignment throws me an error in the chrome repl but not in that function? Im just so confused, any help would be great!

The function deepInsert recives the following params:
An Object (it will be modified)
The array of path for the value( 'foo.bar.x' needs to become ['foo','bar', 'x'] )
The value to be inserted
and does this:
Iterates on the path Array
if the current path iteration isn't the last, it will Initialize a
new Object on it.
if the current path IS the last one, the passed value is set to it.
The function processWorkbook just iterates on the object keys to send the parameters to the deepInsert function. This could be done directly on the deepInsert.
And that's it. The problem is the function has unused variables and complicated code. A more simple and documented function:
function unnestObject(obj = {}) {
let newObject = {}, //The object to return
current; // the object position holder
for (var key in obj) { // iterate on recived object keys
current = newObject // Makes the current object the new Object root
let path = key.split('.') // Split the current key
path.forEach((p, i) => { // iterate on the key paths
if ((path.length - 1) !== i) //if the path isn't the last one
current[p] = current[p] || {} // initialize a new object on that path (if a object was previously initialized, it is preserved)
else //if the path is the last one
current[p] = obj[key] // sets the value of the initial object key
current = current[p] // Updates the current to the next node
})
}
return newObject; //returns the new object
}
//Example data [DOESNT WORK WITH ARRAYS]
var data = {
"frame1.title": "heading",
"frame1.division": "first",
"frame1.data.month": "Jan",
"frame1.data.value": "54",
}
console.log(unnestObject(data))
// Prints
// {
// "frame1": {
// "title": "heading",
// "division": "first",
// "data": {
// "month": "Jan",
// "value": "54"
// }
// }
// }
Note: Both functions doesn't support arrays, if you pass something like {"foo.bar[0].value": 42}, foo.bar will be a object. You can detect the array [] keys and make it initialize an array instead of an object on the iteration
About the next = cur[p] = {}, you can assign one value to multiple variables at once. you can do foo = bar = 42, both will have 42.
You can also do foo = bar = {}. both will have pointers to the same object, if you change a value on one, another will already have the change.
This is very userful for get and initialize global values for instance
var foo = window.foo = window.foo || {bar: 42};
This line will make foo and window.foo recive the object on window.foo . if window.foo wasn't initialized yet, it will recive the new object.

Related

Get an object just by a property value

Let´s assume I have an object property which is passed into a function. In this case 'name' is filled with 'myObject.name' (which has the value 'Tom') - so basically 'Tom' gets passed into the function as the 'name'
function(name) {
do something //non-essential for my question
}
Is it possible to get the object, where 'Tom' is the property of, just by having the information 'Tom'? Basically I´m looking to get myObject.
Thanks :)
No, that's not possible.
All that the function knows is that one of its parameters was pointed to the string "Tom", not what else points to that string somewhere else in memory.
You can store objects within an array, filter the array to match property name of object to parameter passed to function using for..of loop, Object.entries(), which returns an array of property, values of an object.
const data = Array();
const setObjectPropertyName = _name => {
data.push({[_name]:_name});
return data
}
const getObjectByPropertyName = prop => {
let res = `${prop} property not found in data`;
for (let obj of data) {
for (let [key] of Object.entries(obj)) {
if(key === prop) return obj;
}
}
return res;
}
let s = setObjectPropertyName("Tom");
let g = getObjectByPropertyName("Tom");
let not = getObjectByPropertyName("Tome");
console.log(s,"\n", g, "\n", not);
Disclaimer: you absolutely should not do this. I'm only posting this because it is in fact possible (with some caveats), just really not advisable.
Going on the assumption that this is running in the browser and it's all running in the global scope (like in a script tag), you could technically iterate over the window object, check any objects in window for a name property and determine if their name property matches the name passed to your function.
var myObject = {
name: 'Tom',
thisIs: 'so awful',
imSorry: true,
};
function doSomethingWithName(name) {
for (var obj in window) {
var tmp = window[obj];
if (Object(tmp) === tmp && tmp.name === name) {
return tmp;
}
}
}
console.log(doSomethingWithName(myObject.name));

Identify all non-native variables in the window namespace? [duplicate]

Is there a way to find out all user defined window properties and variables (global variables) in javascript?
I tried console.log(window) but the list is endless.
You could also compare the window against a clean version of the window instead of trying to snapshot during runtime to compare against. I ran this in console but, you could turn it into a function.
// make sure it doesn't count my own properties
(function () {
var results, currentWindow,
// create an iframe and append to body to load a clean window object
iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
// get the current list of properties on window
currentWindow = Object.getOwnPropertyNames(window);
// filter the list against the properties that exist in the clean window
results = currentWindow.filter(function(prop) {
return !iframe.contentWindow.hasOwnProperty(prop);
});
// log an array of properties that are different
console.log(results);
document.body.removeChild(iframe);
}());
This is in the same spirit as #jungy 's answer but we can do it in 3 lines:
document.body.appendChild(document.createElement('div')).innerHTML='<iframe id="temoin" style="display:none"></iframe>';
for (a in window) if (!(a in window.frames[window.frames.length-1])) console.log(a, window[a])
document.body.removeChild($$('#temoin')[0].parentNode);
First we add a hidden iframe; then we test existing variables against the standard JavaScript API in the iframe; then we remove the iframe.
To work more conveniently, it could be useful to sort the results in alphabetical order, and it's still possible in a 3 lines version:
document.body.appendChild(document.createElement('div')).innerHTML='<iframe id="temoin" style="display:none"></iframe>';
Object.keys(window).filter(a => !(a in window.frames[window.frames.length-1])).sort().forEach((a,i) => console.log(i, a, window[a]));
document.body.removeChild($$('#temoin')[0].parentNode);
And it can be packed into a bookmark:
javascript:document.body.appendChild(document.createElement('div')).innerHTML='<iframe%20id="temoin"%20style="display:none"></iframe>';Object.keys(window).filter(a=>!(a%20in%20window.frames[window.frames.length-1])).sort().forEach((a,i)=>console.log(i,a,window[a]));document.body.removeChild(document.querySelectorAll('#temoin')[0].parentNode);throw 'done';
You would need to do the work for yourself. Read in all properties, on the first possible time you can. From that point on, you can compare the property list with your static one.
var globalProps = [ ];
function readGlobalProps() {
globalProps = Object.getOwnPropertyNames( window );
}
function findNewEntries() {
var currentPropList = Object.getOwnPropertyNames( window );
return currentPropList.filter( findDuplicate );
function findDuplicate( propName ) {
return globalProps.indexOf( propName ) === -1;
}
}
So now, we could go like
// on init
readGlobalProps(); // store current properties on global object
and later
window.foobar = 42;
findNewEntries(); // returns an array of new properties, in this case ['foobar']
Of course, the caveat here is, that you can only "freeze" the global property list at the time where your script is able to call it the earliest time.
I ran this in the console in ChromeDev tool and it copied all of the user defined proper
const getUserDefinedKeys = () => {
const globalKeys = [ 'postMessage','blur','focus','close','parent','opener','top','length','frames','closed','location','self','window','document','name','customElements','history','locationbar','menubar','personalbar','scrollbars','statusbar','toolbar','status','frameElement','navigator','origin','external','screen','innerWidth','innerHeight','scrollX','pageXOffset','scrollY','pageYOffset','visualViewport','screenX','screenY','outerWidth','outerHeight','devicePixelRatio','clientInformation','screenLeft','screenTop','defaultStatus','defaultstatus','styleMedia','onanimationend','onanimationiteration','onanimationstart','onsearch','ontransitionend','onwebkitanimationend','onwebkitanimationiteration','onwebkitanimationstart','onwebkittransitionend','isSecureContext','onabort','onblur','oncancel','oncanplay','oncanplaythrough','onchange','onclick','onclose','oncontextmenu','oncuechange','ondblclick','ondrag','ondragend','ondragenter','ondragleave','ondragover','ondragstart','ondrop','ondurationchange','onemptied','onended','onerror','onfocus','oninput','oninvalid','onkeydown','onkeypress','onkeyup','onload','onloadeddata','onloadedmetadata','onloadstart','onmousedown','onmouseenter','onmouseleave','onmousemove','onmouseout','onmouseover','onmouseup','onmousewheel','onpause','onplay','onplaying','onprogress','onratechange','onreset','onresize','onscroll','onseeked','onseeking','onselect','onstalled','onsubmit','onsuspend','ontimeupdate','ontoggle','onvolumechange','onwaiting','onwheel','onauxclick','ongotpointercapture','onlostpointercapture','onpointerdown','onpointermove','onpointerup','onpointercancel','onpointerover','onpointerout','onpointerenter','onpointerleave','onselectstart','onselectionchange','onafterprint','onbeforeprint','onbeforeunload','onhashchange','onlanguagechange','onmessage','onmessageerror','onoffline','ononline','onpagehide','onpageshow','onpopstate','onrejectionhandled','onstorage','onunhandledrejection','onunload','performance','stop','open','alert','confirm','prompt','print','queueMicrotask','requestAnimationFrame','cancelAnimationFrame','captureEvents','releaseEvents','requestIdleCallback','cancelIdleCallback','getComputedStyle','matchMedia','moveTo','moveBy','resizeTo','resizeBy','scroll','scrollTo','scrollBy','getSelection','find','webkitRequestAnimationFrame','webkitCancelAnimationFrame','fetch','btoa','atob','setTimeout','clearTimeout','setInterval','clearInterval','createImageBitmap','onappinstalled','onbeforeinstallprompt','crypto','indexedDB','webkitStorageInfo','sessionStorage','localStorage','chrome','onformdata','onpointerrawupdate','speechSynthesis','webkitRequestFileSystem','webkitResolveLocalFileSystemURL','openDatabase','applicationCache','caches','ondevicemotion','ondeviceorientation','ondeviceorientationabsolute','WebUIListener','cr','assert','assertNotReached','assertInstanceof','$','getSVGElement','getDeepActiveElement','findAncestorByClass','findAncestor','disableTextSelectAndDrag','isRTL','getRequiredElement','queryRequiredElement','appendParam','createElementWithClassName','ensureTransitionEndEvent','scrollTopForDocument','setScrollTopForDocument','scrollLeftForDocument','setScrollLeftForDocument','HTMLEscape','elide','quoteString','listenOnce','hasKeyModifiers','isTextInputElement' ];
return Object.fromEntries(Object.entries(window).filter(([ key ]) => !globalKeys.includes(key)));
};
const getCircularReplacer = () => {
const seen = new WeakSet();
return (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
};
copy(JSON.stringify(getUserDefinedKeys(), getCircularReplacer()));
The properties of window object are in chronological order. So, create some variable with unique name at the beginning of your first script included in your webpage and get the index of this property:
var abcdefghijklmnopqrstuvwxyz = true;
var firstOwnPropertyFound = Object.keys(window).indexOf('abcdefghijklmnopqrstuvwxyz');
Then anywhere you want to get array of all user defined properties use:
let myProp = Object.keys(window).slice(firstOwnPropertyFound);
or if you want to skip the first two variables:
let myProp = Object.keys(window).slice(firstOwnPropertyFound + 2);
The myProp variable is array contains all property names you created. In my test webpage for example:
Array(7) [
0: "abcdefghijklmnopqrstuvwxyz"
1: "firstOwnPropertyFound"
2: "st"
3: "clog"
4: "tstfnc"
5: "tstFNC1"
6: "obj"
length: 7
]
Then access all variables:
myProp.forEach(item => {
console.log(window[item]);
}
I use this and it works. (Sorry for my bad English)
Maybe this?:
for (var property in window)
{
if (window.hasOwnProperty(property))
console.log(property)
}

Initialize a JavaScript object "tree" to any depth, nested objects

Essentially my I am trying to initialize a JavaScript object and have it contain empty objects with a single key. For example:
getOject('one.two.three')
Would result in the object:
{one:{two:{three:''}}}
As far as I can tell, you can't initialize with dynamic key names unless you use array notation
root[dynamicKey] = 'some variable';
so I need to loop through and based on the number of args initialize each one then assign it's value but the syntax doesn't seem to let me do this in any way that I know of.
So, if it were not a loop it would be like this:
jsonifiedForm[rootKey] = {};
jsonifiedForm[rootKey][childKeys[0]] = {};
jsonifiedForm[rootKey][childKeys[0]][childKeys[1]] = $input.val();
I can't think of a way to do this, I am not typically a JS guy so it might be something simple but I couldn't find anything on Google or Stack Overflow
Thank you in advance!
This function should be what you're looking for.
function getOject(str) {
// this turns the string into an array = 'one.two.three' becomes ['one', 'two', 'three']
var arr = str.split('.');
// this will be our final object
var obj = {};
// this is the current level of the object - in the first iteration we will add the "one" object here
var curobj = obj;
var i = 0;
// we loop until the next-to-last element because we want the last element ("three") to contain an empty string instead of an empty object
while (i < (arr.length-1)) {
// add a new level to the object and set the curobj to the new level
curobj[arr[i]] = {};
curobj = curobj[arr[i++]];
}
// finally, we append the empty string to the final object
curobj[arr[i]] = '';
return obj;
}
Because JavaScript references values in variables instead of copying them "into" variables, we can make our initial value, then make a reference to it which we'll move around as we delve down in:
var getOject = function (k, s) {
// initialize our value for return
var o = {},
// get a reference to that object
r = o,
i;
// we'll allow for a string or an array to be passed as keys,
//and an optional sepeartor which we'll default to `.` if not given
if (typeof k === 'string') {
k = k.split(s || '.');
}
// do we have an array now?
if (k && k.length) {
//iterate it
for (i = 0; i < k.length; i += 1) {
// set a property on the referenced object
r[k[i]] = {};
// point the reference to the new level
r = r[k[i]];
}
}
// send back the object
return o;
}
console.log(getOject('one.two.three'));
console.log(getOject('four|five|six', '|'));
r points to the same thing that o does, initially, and as we move the reference (r) deeper into o and write to it, we're building out o as we go.
The two console.log() calls at the end output the following:
Also notice I let you pass in an array to start with if you feel like it, and made the separator a parameter so that you're not stuck with .

Walk and set value to complex Javascript object

I note the following similarity to this post:
Dynamic deep setting for a JavaScript object
However, the above post is based upon a known structure and depth to the javascript object and not truly dynamic. Truly dynamic would suggest that you did not have any precursor knowledge of the structure, just a path and a value to replace it with. I have created a fairly good use case over on JSFiddle here:
http://jsfiddle.net/kstubs/nJrLp/1/
function Message(message) {
$('result').insert('<div>' + message + '</div>');
}
var obj = {
"array": [1, 2, 3],
"boolean": true,
"null": null,
"number": 123,
"object": {
"a": "b",
"c": "d",
"e": "f",
"complex_array1": [{
"g": "h"
}, {
"bingo": "bongo"
}, {
"x": {
"complex_array2": [{
"h": "i"
}, {
"j": "k"
}, {
"bingo": "bongo"
}, {
"bango": "jango"
}]
}
}]
},
"string": "Hello World"
};
var list = [{
"h": "i"
}, {
"j": "k"
}];
function walk(path,value) {
var a = path.split('.');
var context = obj;
for (i = 0; i < a.size(); i++) {
context = context[a[i]];
}
}
The use case:
Find complex_array2
Update its list to a new list (a new array)
The new array is the array list which should replace the list for complex_array2. The javascript function walk does just that, walks the javascript object until the path criteria is met and then sets the value to whatever value is passed to the walk function, however the new value does not stick.
I know why it doesn't stick, because as you walk over an object of type array you lose the pointer to the original object. So, the challenge is to walk the javascript object and not lose context of the original object.
Thanks for any assistance.
Karl..
Just loop over all but the last element in the path. Then the final element is used for assignment after the loop.
var i = 0;
for (; i < a.size() - 1; i++) {
context = context[a[i]];
}
context[a[i]] = value;
Technically you can leave the declaration of i inside the for. I just find this clearer.
http://jsfiddle.net/nJrLp/2/
The reason your code doesn't work as it's written is because rather than changing a property on an object you have a reference to, you're actually changing which object your local variable points to.
context = context[a[i]];
context is a pointer to an object, and it's a local variable. When you assign to it, you're assigning a new pointer value, which loses the reference to the previous object. If you want to replace it, you'll have to refer to it from its parent object. Assume parent is one such object; once you locate your target object's key name (let's say you've put it in variable key), you could overwrite the existing value as such:
parent[key] = new_value;
This will dereference parent, find the property named by key, and replace its value (which is a pointer) with the memory address of new_value. What you have currently works something like this:
var context = parent[key];
context = new_value;
In this case you're simply changing the pointer value of the local variable context, not the object that parent[key] points to.
I used a helper function for reading complex json objects. (http://jsfiddle.net/JBBAJ/)
var object = {
data: {
users: [
{
firstName: "White"
},
{
firstName: "Black"
}
]
}
}
var read = function(path, obj) {
var path = path.split(".");
var item = path.shift();
if(item.indexOf("]") == item.length-1) {
// array
item = item.split("[");
var arrayName = item.shift();
var arrayIndex = parseInt(item.shift().replace("]", ""));
var arr = obj[arrayName || ""];
if(arr && arr[arrayIndex]) {
return read(path.join("."), arr[arrayIndex]);
} else {
return null;
}
} else {
// object
if(obj[item]) {
if(path.length === 0) {
return obj[item];
} else {
return read(path.join("."), obj[item]);
}
} else {
return null;
}
}
}
console.log(read("data.users[0].firstName", object)); // White
console.log(read("data.users[1].firstName", object)); // Black
console.log(read("data.test.users[0]", object)); // null
The function read accepts a path and an object.

Dynamically create sub-objects and arrays when needed

I have an object created from JSON via AJAX from the server. The object has several sub-objects in an array, e.g.:
obj.subObj1[0].value="abc";
obj.subObj1[1].value="abc";
obj.subObj2[0].value="abc";
Now I want to set some values in this object but I dont know if they already exist.
obj.subObj1[0].value="new Value"; // No Problem
obj.subObj2[1].value="new Value2"; // Problem because obj.subObj2[1] is no Object.
I would need to do obj.subObj2[1]={} first.
Because I have this problem very often I am looking for method to automate this. A method or class which does automatically create the needed object (or array if I use an integer).
It should be able to handle an infinite depth of such sub-objects. Like this:
var obj = TheObject();
obj.sub1.sub2[10].sub3[1].sub4='value';
Now automatically all needed sub-objects and arrays should be created.
Cannot really guarantee anything about cross-browser compatibility, but how about trying this on for size (works in Chrome):
// Safely sets value property of index of an array of an object.
function setObj(obj, subObjName, index, val) {
// Ensure the object exists
if (typeof obj == 'undefined') {
obj = new Object();
}
// Ensure the array property exists
if (typeof obj[subObjName] == 'undefined') {
obj[subObjName] = new Array();
}
// Ensure the array properties index exists
if (typeof obj[subObjName][index] == 'undefined') {
obj[subObjName][index] = {};
}
// Set the value
obj[subObjName][index].value = val;
// Return the object
return obj;
}
Example use:
<script type="text/javascript">
var obj;
obj = setObj(obj, "something", 1, "val");
setObj(obj, "something", 0, "someValue");
alert(obj.something[1].value);
alert(obj.something[0].value);
</script>
If you can assume that the referenced item in the array will be either undefined or an object it simplifies things. Of course the simple (non-automatic) way would be something like this:
if (!obj.subObj2[1]) obj.subObj2[1] = {};
obj.subObj2[1].value = "new Value2";
A not-very generic function to do it for you would be:
function setArrayObjectProp(arr, index, prop, val) {
if (!arr[index])
arr[index] = {};
arr[index][prop] = val;
}
// called as
setArrayObjectProp(obj.subObj2, 1, "value", "new Value2");
heloo
try testing the type of the array item first if its not object then equal it to the new object format {value:"new Value2"}
if(typeof(obj.subObj2[1])!='object')
{
obj.subObj2[1] = {value:"new Value2"};
}
else
{
obj.subObj2[1].value = "new Value2";
}

Categories