How to handle callbacks in JavaScript within recursive functions? - javascript
Trying to compare two sub trees of bookmarks in Chrome, I ran into troubles with the asynchronous API call to query the children of a bookmarks folder.
function titleComparator (lhs, rhs) {
return lhs.title < rhs.title ? -1 : lhs.title > rhs.title ? 1 : 0;
}
// Return whether two bookmark trees have equal content
function compare(lhs, rhs) {
// Not equal if one is a bookmark and another is a folder
if (('url' in lhs) != ('url' in rhs))
return false;
// If both are bookmarks, compare url and title
if ('url' in lhs && 'url' in rhs)
return lhs.title == rhs.title && lhs.url == rhs.url;
// If both are folders, compare contents
chrome.bookmarks.getChildren(lhs.id, function (lhsChildren) {
chrome.bookmarks.getChildren(rhs.id, function (rhsChildren) {
if (lhsChildren.length != rhsChildren.length)
return false; // Want to return from compare()
lhsChildren.sort(titleComparator);
rhsChildren.sort(titleComparator);
for (var i = 0; i < lhsChildren.length; i++)
if (!compare(lhsChildren[i], rhsChildren[i])
return false; // Same here
return true; // Same here
});
});
}
How to handle callbacks in JavaScript within recursive functions?
as explained in detail here
you will need to refactor your code.
somehow it seems that it is not the correct way to use recursion within a asynchronous (often delayed) function to search a tree-based or hierarchical data model.
I think this should be the way to do it:
Seperatate the logic into several functions
Use "lazy loading" to avoid duplicate call of getChilden()
Use recursion and define a new nested function callback
Refactor the for-loop to recursion as well
See my untested code to show what I mean:
function compare(lhs, rhs, callback, index, lhsChilds, rhsChilds){
// Not equal if one is a bookmark and another is a folder
if (('url' in lhs) != ('url' in rhs)) {
callback(false);
return;
}
// If both are bookmarks, compare url and title
if ('url' in lhs && 'url' in rhs) {
callback(lhs.title == rhs.title && lhs.url == rhs.url);
return;
}
// If both are folders, check parameters and compare contents
//First, check if the list has already been loaded (code is running inside a recursive step)
if(lhsChilds != undefined && rhsChilds != undefined){
compareTwoChilds(lhs, rhs, callback, index, lhsChilds, rhsChilds);
}
else{
index = 0; //first recursion for this tuple (lhs, rhs)
chrome.bookmarks.getChildren(lhs.id, function (lhsChildren) {
chrome.bookmarks.getChildren(rhs.id, function (rhsChildren) {
compareTwoChilds(lhs, rhs, callback, index, lhsChilds, rhsChilds);
});
});
}
}
function compareTwoChilds(lhs, rhs, callback, index, lhsChilds, rhsChilds){
if (index < lhsChildren.length){ //just for the safety
if (lhsChildren.length != rhsChildren.length) {
callback(false);
return;
}
lhsChildren.sort(titleComparator);
rhsChildren.sort(titleComparator);
//compare using recursion, with an emtpy lists of rhs and lhs children
compare(lhsChildren[index], rhsChildren[index], function(compareResult){
if(!compareResult){
callback(false); //if the result is false, no more search needed
}else{ // use recursion again to loop through the next childs using the already loaded childs
if (++index < lhsChildren.length){
compare(lhsChildren[index], rhsChildren[index], callback, index, lhsChilds, rhsChilds)
}else{
callback(true); // the loop has ended,
}
}
});
}else{
callback(false); //this should never happen, so not the same...
}
}
you can call the compare function like that:
compare(lhs,rhs, function(result){
var compareResult = result;
//carry on with your code here
});
//and not here :-)
return will only ever exit the callee
You're gonna have to provide a callback for the answer to be spat out into asynchronously.
Everywhere you've written a return statement intended to be consumed by the callback closure, you should standardize instead on passing it to your callback.
function compareAsync(lhs, rhs, callback) {
//…
callback(false); return;
//…
callback(lhs.title == rhs.title && lhs.url == rhs.url); return;
//…
callback(false); return; // Want to return from compare()
//…
var finished = 0;
var whetherSuccess = true;
lhsChildren.forEach(function(iterand, index) {
compareAsync(iterand, rhsChildren[index], function(result) {
whetherSuccess &= result;
if (++finished === lhsChildren.length) {
callback(whetherSuccess);
}
});
});
}
So: upon finding out what the lhsChildren are, we kick off a bunch of async functions. They'll each increment the finished counter at some point. They each have the power to demote the overall whetherSuccess to false via &=. The consumer who discovers they're the the final function to get an answer, is the person who will report whetherSuccess to the original callback.
First of all, I found that Chrome has a getSubTree() function as well which makes things considerably easier. So if you just want to get it work, use this instead of asynchronously traversing the tree node by node. However, this remains an interesting problem and thanks to a reddit user, I figured out a working solution to this.
compare() is the main function that recursively calls itself. However, because of the asynchronous call inside, it cannot consume the return values of it's recursive calls.
// Pass a boolean to the callback indicating whether the recursive contents of
// both bookmarks folders are equal.
function compare(lhs, rhs, callback) {
// Compare titles except for the top-level folder
if (lhs.parent_ && lhs.title !== rhs.title) {
compare_failure(callback);
return;
}
// Compare urls if at least one of the sides is a bookmark
if ('url' in lhs || 'url' in rhs) {
if ((lhs.url || null) === (rhs.url || null))
compare_respond(lhs.parent_, callback);
else
compare_failure(callback);
return;
}
// For both sides being folders, we have to take a look at the contents
chrome.bookmarks.getChildren(lhs.id, function (lhs_children) {
chrome.bookmarks.getChildren(rhs.id, function (rhs_children) {
// Compare amount of children
if (lhs_children.length != rhs_children.length) {
compare_failure(callback);
return;
}
// Keep track of how many children already reported back
lhs.all_children = lhs_children.length;
lhs.equal_children = 0;
// Let pairs of children compare each other
lhs_children.sort(bookmark_comparator);
rhs_children.sort(bookmark_comparator);
for (var i = 0; i < lhs_children.length; i++) {
var lhs_child = lhs_children[i];
var rhs_child = rhs_children[i];
// Store parent reference so the deeper function can
// asynchronously respond with the results once finished.
lhs_child.parent_ = lhs;
compare(lhs_child, rhs_child, callback);
}
});
});
};
compare_respond() is the counterpart that is used to propagate results of deeper nodes back up. It's used instead of return in the main function above.
// Report comparison results back to the parent node. The parent node waits
// until it collected the results from all its children. Then it reports to
// its parent in turn. At the root node, the user callback is executed.
function compare_respond(node, callback) {
// Collect child results
node.equal_children++;
// Respond upwards if we got results from all
if (node.equal_children == node.all_children) {
if ('parent_' in node)
compare_respond(node.parent_, callback);
else
callback(true);
}
};
compare_failure() is used to abort the whole thing at any point when we found a pair of unequal nodes. We don't have to report upwards in that case.
// Break out of the recursive function and report failure to the user. It's
// safe against being called multiple times so multiple children can report
// failure and the user will only be notified once.
function compare_failure(callback) {
if ('called' in callback)
return;
callback.called = true;
callback(false);
};
bookmark_comparator() is a small helper that is used to sort arrays of child bookmarks. Sorting is needed to compare the contents of two folders since I don't want to rely on the item order.
// Comparator to sort lists of bookmark nodes first by title and second by
// url. Take into that folders have to url.
function bookmark_comparator(lhs, rhs) {
if (lhs.title != rhs.title)
return lhs.title < rhs.title ? -1 : 1;
if (lhs.url || null != rhs.url || null)
return lhs.url || null < rhs.url || null ? -1 : 1;
return 0;
};
Related
hasOwnProperty() is only checking if a certain property exists in a JSON, but doesn't return anything if it doesn't
I keep trying different methods to check if this JSON contains "attributes." In this way I can determine if the given coordinates are outside of wetlands. If they are in wetlands, "attributes" will exist in the JSON. If they aren't in wetlands, 'attributes' won't be in the JSON. When I run this function, I am only getting TRUE - when I type in coordinates that are in a wetland (try 43.088 instead, in the JSON url, which returns true). However I want FALSE for the given url. For some reason when I do console.log("FALSE"), this doesn't appear or return in the console at all if hasOwnProperty('attributes') == false. Am I missing something? function(GetData) { fetch('https://www.fws.gov/wetlandsmapservice/rest/services/Wetlands/MapServer/0/query?where=&text=&objectIds=&time=&geometry=-88.305%2C43.060&geometryType=esriGeometryPoint&inSR=4326&spatialRel=esriSpatialRelWithin&relationParam=&outFields=WETLAND_TYPE&returnGeometry=false&returnTrueCurves=false&maxAllowableOffset=&geometryPrecision=&outSR=&returnIdsOnly=false&returnCountOnly=false&orderByFields=&groupByFieldsForStatistics=&outStatistics=&returnZ=false&returnM=false&gdbVersion=&returnDistinctValues=false&resultOffset=&resultRecordCount=&queryByDistance=&returnExtentsOnly=false&datumTransformation=¶meterValues=&rangeValues=&f=pjson&__ncforminfo=qCOZOO8Kyr4uogGcKvxkzzuK7gmavd4CxwTAkdbAsF2_aT4eeNbB0NpLwCYwiAJSf1ZHqY3CKVZ3osgMevhYGQrqRUQZej5oHaSmnSIaiZZb469Cexv-zqqmgYMuFJAAzrcRxvKXPBz9VnYPnMrM6kBNhO-cz6yK_w5T1mqNu_VXSbjBSihVf4_mlUBSVb9yf4C8scYXWm9Iak2Nfn1dtJACNUHLBHSElLvc1wxFMO2eUWNsD3qpCk3kAcRyYftuFU86n7THyk2IvkIUpxNmDHRxmmbgSYvPLMkl8t41Jzjp_bntkIyOWB0u8cQU2VsfASFUdznRkvrvYrQxgR8eyvsPq5oV_ZoPSksVCew6xev0K_TV2NU-kjojYpowMVXpZtCX9P-Q_7m8ywt2PyLPhEVgQB12ji1S7G5FRzIt6E0SDoXMY1vqQvPtedaNYbBCazXgs05L9DFKdtvrwmQVCeLmpBTduIhF9Sk4kozMnFX6GOANrZJMCI9AssN0DjrhlZkgDVw0l1flF44Zli927CXGTQ-oUpwsn7PPypVkN2iDJf-nz9XNbj82sv1c6B5s5UZVwiOp8VHJfZSDJ8BAYR4z_oONT2JwbVSKKlFKeN72f-Y6EejcB9wPKmn5kYjv7CKkRyIIv4F4cqVWxLK9x33uvEDMTvxX') .then(function(response) { return response.json(); }) .then(function(data) { appendData3(data); }) .catch(function(err) { console.log('error: ' + err); }); function appendData3(data) { for (let obj of data['features']) { if (obj.hasOwnProperty('attributes') == false) { console.log("FALSE"); } else { console.log("TRUE"); } } } };
The issue is that in the response data['features'] is empty. When iterating over an empty array, nothing within the for...of loop is executed. const emptyArray = []; for (const item of emptyArray) { // body is never executed... } If just checking the presence of an item within data['features'] is enough, you could use the length of the array. function appendData3(data) { if (data.features.length > 0) { console.log("TRUE"); } else { console.log("FALSE"); } } To check if one of the elements has the property "attributes" you could use some(): function appendData3(data) { if (data.features.some(item => item.hasOwnProperty("attributes"))) { console.log("TRUE"); } else { console.log("FALSE"); } }
If you're just trying to find out if a specific point is within one of the wetlands polygons, you could let the server to do the hard job and simplify your request. For example, ask for count. See returnCountOnly at https://developers.arcgis.com/rest/services-reference/enterprise/query-feature-service-layer-.htm https://www.fws.gov/wetlandsmapservice/rest/services/Wetlands/MapServer/0/query?geometry=-88.305%2C43.060&geometryType=esriGeometryPoint&inSR=4326&spatialRel=esriSpatialRelWithin&returnCountOnly=true&f=pjson https://www.fws.gov/wetlandsmapservice/rest/services/Wetlands/MapServer/0/query?geometry=-88.305%2C43.088&geometryType=esriGeometryPoint&inSR=4326&spatialRel=esriSpatialRelWithin&returnCountOnly=true&f=pjson
I tested your code and this is the problem. When the coordinates are outside the wetlands, the features array is empty, that means nothing happen in your for loop. So do this instead of checking directly inside of your for loop function appendData3(data) { // Here we check if features is empty by checking it's length if (data['features'].length == 0) { console.log("FALSE") } for (let obj of data['features']) { console.log("TRUE"); } } I also see that your for loop is only getting one object every time so instea of doing a for loop, just do it like this: function appendData3(data) { var obj = data['features'][0] if(obj) { console.log('TRUE') } else { console.log('FALSE') } } As you can see, I did it even easier this time, just by getting the first object of features and checking if it exist. Also, small tip: when you want to check if a condition is false, don't use == false, just put an exclamation mark at the beginning of the if statement. Like that: if(!obj.hasOwnProperty('attributes')) { // Code here will be executed if the condition is false } else { // Code here will be executed if the condition is true } I hope this help you fixing your problem. Have a nice day :)
javascript how to change function from imperative to functional
i am new in functional programing, and developing web app using angularjs. I am try to change some formal function to functional. functional method has one entry and only one return, and should no side effect inside function. but in my function i change the status in for loop, so i guess i have to separate below to several block? imperative return true/false and change status in one circle of for loop what i expected is that change to functional and no additional for loop function checkStatusOfChildTest(childList) { for (var i = 0; i <childList.length;i++) { var keys = Object.keys(childList[i]); if (keys.length === 1 && keys[0] === '$status') { if (childList[i][keys[0]] === 'create') { return true; } else { //just mark the status of child as delete, then remove child when save finally childList[i][keys[0]] = 'delete'; return false; } } } //if no status is new one return true; }
Collect values as I recurse tree in javascript
I understand basic recursion, but this problem has be stumped. I have a tree structure set up in a database, where each node(row) has an id and parent id. I need a function that can run and in the callback return an array of all the descendants of a particular node given its id. I've been able to put together a function that can print out all of the values, but I can't figure out how to capture them and return them in the callback. I know the base case isn't set up correctly, as I'm not sure what it should even be. I'd appreciate any help! Thank you! // My "database" var nodes_collection = [ {id:"id1",name:"name1",parentid:"."}, {id:"id2",name:"name2",parentid:"id1"}, {id:"id3",name:"name3",parentid:"id1"}, {id:"id4",name:"name4",parentid:"id2"}, {id:"id5",name:"name5",parentid:"id3"}, {id:"id6",name:"name6",parentid:"id3"}, {id:"id7",name:"name7",parentid:"id5"}, {id:"id8",name:"name8",parentid:"id7"}, {id:"id9",name:"name9",parentid:"id7"}, {id:"id10",name:"name10",parentid:"id9"}, ]; // This is NOT a real function, it simply performs the function that the real getChildren does when connected to my database!!! function getChildren(parentid, callback){ var children = []; for(var i=0; i < nodes_collection.length; i++){ if(nodes_collection[i].parentid == parentid){ children.push(nodes_collection[i].id); } } callback(children); } function allDescendants(parentid, callback) { getChildren(parentid, function(childNodes) { if (false) { // Only false because I don't know what my base case should be. //console.log("done"); } else { for (var i = 0; i < childNodes.length; i++) { var child = childNodes[i]; allDescendants(child); console.log(child); // Here it prints out all the values. How can I capture them? and return them with my callback? } } }); } allDescendants("id3", function(result){ console.log(result); }); EDIT: Due to some confusion, I've changed the code to a bare bones example of what I'm trying to do that can be run locally !!! getChildren() is NOT a real function, it simply performs the function that the real getChildren does when connected to my database!!! Bottom line: The code in question works to recursively touch all values. Now how can I store all the values that are currently being outputted via console.log()?
Here's one simple way. We create a result object and an intermediary recursive function, keeping allDescendants as a wrapper. When the recursion is complete, we return the result that now has all the descendants. JsvaScript code: // My "database" var nodes_collection = [ {id:"id1",name:"name1",parentid:"."}, {id:"id2",name:"name2",parentid:"id1"}, {id:"id3",name:"name3",parentid:"id1"}, {id:"id4",name:"name4",parentid:"id2"}, {id:"id5",name:"name5",parentid:"id3"}, {id:"id6",name:"name6",parentid:"id3"}, {id:"id7",name:"name7",parentid:"id5"}, {id:"id8",name:"name8",parentid:"id7"}, {id:"id9",name:"name9",parentid:"id7"}, {id:"id10",name:"name10",parentid:"id9"}, ]; // This is NOT a real function, it simply performs the function that the real getChildren does when connected to my database!!! function getChildren(parentid, callback){ var children = []; for(var i=0; i < nodes_collection.length; i++){ if(nodes_collection[i].parentid == parentid){ children.push(nodes_collection[i].id); } } callback(children); } function allDescendants(parentid, callback) { let result = []; let go = function(children){ for (child of children){ result.push(child); getChildren(child, go) } } getChildren(parentid, go); callback(result); } allDescendants("id3", function(result){ console.log('result: ' + JSON.stringify(result)); });
I propose this: let obj = [ {id:"id1",name:"name1",parentid:"."}, {id:"id2",name:"name2",parentid:"id1"}, {id:"id3",name:"name3",parentid:"id1"}, {id:"id4",name:"name4",parentid:"id2"}, {id:"id5",name:"name5",parentid:"id3"}, ] function getChilds(obj, parent_id, callback) { if(obj.length === 0) return; else if (typeof callback !== 'function'){ throw new Error("The callback must be a function "); return; } let childs = obj.filter(function (c) {return c.parentid == parent_id }) if(childs.length > 0 ){ childs = childs.map(function (c) {return c.id}) } callback(childs) } // Test getChilds(obj, "id1", function (childs) {console.log(childs)})
How do I delete and move later function arguments to the left if an object type is excluded?
I apologize if this has already been answered. I don't know a better technical term for this method and I can't seem to find here or Google searches. Basically what I want is to be able to make a function like how jQuery handles Ajax function arguments. Example: $.get(url, callback); and $.post(url, data, callback); Basically if data is excluded from the function it will move callback to the left in its place. Since when the function runs it uses the variable names from the input order. I presuming this method would involve checking the arguments[1] and arguments[2] but I want to make sure if this is the correct way since I like to have my code clean as possible.
instead of checking for arguments[1] and arguments[2] , you should check $.post(url, data, callback) whether url is string, data is object , callback is function
After spending time using the helpful information here. I managed to code up my solution. I'd thought I post my working (simplified edit) result for those that want a good example how to make one. function post(url) { var callback, data, isAsync, // Prepare possible arguments. i, // Loop index. length = arguments.length; // Get arguments length. // Check the argument's types for optional passed parameter options. for (i = 1; i < length && i < 4; i++) { if (typeof arguments[i] === "object") { data = arguments[i]; } if (typeof arguments[i] === "function") { callback = arguments[i]; } if (typeof arguments[i] === "boolean") { isAsync = arguments[i]; } } // Asynchronous is enabled by default. if (isAsync === undefined) { isAsync = true; } // More code here... }
Implementing eachChild for a specefic case
I have a few places in my code that are very similar to this snippet: tag_iter = hold_tags_el.firstChild; do { if (tag_iter === null) { hold_tags_el.appendChild(paragraph_el); break; } if (par_el.innerHTML < tag_iter.innerHTML) { hold_tags_el.insertBefore(paragraph_el, tag_iter); break; } if (tag_iter === hold_tags_el.lastChild) { NS.insertAfter(tag_iter, paragraph_el); break; } tag_iter = tag_iter.nextSibling; } while (tag_iter !== null); This can be abstracted to: tag_iter = ref_el.firstChild; do { // loop logic tag_iter = tag_iter.nextSibling; } while (tag_iter !== null); In a function form this would look like: The Call: eachChild(par_el, function (tag_iter, par_el) { // loop logic }); The Definition: NS.eachChild = function (par_el, func, context) { var iter_el = par_el.firstChild, result; do { result = func.call(context, iter_el, par_el); if (result) { break; } iter_el = iter_el.nextSibling; } while (iter_el !== null); } Is there a library that implements this pattern / idiom? What improvements can be made to eachChild? Are there any errors in eachChild? Applying the idiom we have: Snippet A NS.eachChild(el, function(tag_iter, par_el){ // first if (tag_iter === null) { par_el.appendChild(paragraph_el); return true; } // middle if (par_el.innerHTML < tag_iter.innerHTML) { par_el.insertBefore(paragraph_el, tag_iter); return true; } // last if (tag_iter === hold_tags_el.lastChild) { par_el.appendChild(paragraph_el); return true; } });
What improvements can be made? Many. Your snippet with its do-while loop and the many breaks is overly complicated and hard to understand. It can be simplified to var tag_iter = hold_tags_el.firstChild, search = par_el.innerHTML; while (tag_iter !== null && search >= tag_iter.innerHTML) tag_iter = tag_iter.nextSibling; hold_tags_el.insertBefore(paragraph_el, tag_iter); Notice that insertBefore with null as second argument, insertAfter(lastChild) and appendChild do exactly the same thing. With that simplification, you don't need that eachChild function any more. But maybe a little different one: NS.findChild = function(parent, condition) { var child = parent.firstChild; for (var i=0; child!==null && condition(child, i); i++) child = child.nextSibling; return child; }; // then simply: var el = NS.findChild(hold_tags_el, function(tag_iter) { return tag_iter.innerHTML < par_el.innerHTML; }); hold_tags_el.insertBefore(paragraph_el, el); Is there a library that implements this pattern / idiom? I don't know any. But there are many libs with generic iterator methods (some of them with break functionality) that can easily be applied on childNodes collections. Are there any errors in eachChild? It calls the callback even when there is no firstChild (with null as argument). That's at least unconventional, if not wrong - not what you would expect from an iteration. If you think to need it, this should better be made a separate case (a separate callback); otherwise it requires an extra condition in the callback. However in the given usecase you do not need it, as that is a search - see the findChild function above - where eachChild is inappropriate. What improvements can be made to eachChild? Additionally to parEl maybe a counter argument might be nice - check the signature of the standard forEach Array method.