Is setting element.id as a side effect bad practice? - javascript

I'm writing a little cached function in a plugin / library. It takes a HTMLElement and returns a Decorator.
return function _cache(elem) {
if (elem.id === "") {
elem.id = PLUGIN_NAME + "_" + uid++;
}
if (cache[elem.id] === void 0) {
cache[elem.id] = _factory(elem);
}
return cache[elem.id];
}
Here I'm storing some expensive operation in a cache by the id of the HTMLElement. This is a O(1) lookup but it uses the "bad practice" of setting elem.id and having a side effect.
The alternative would be O(N) lookup on the cache
return function _cache(elem) {
for (var i = 0, ii = cache.length; i++) {
var o = cache[i];
if (o.elem == elem) return o.data;
}
var ret = _factory(elem);
cache.push({ elem: elem, data: ret });
return ret;
}
But this means that my cached expensive method doesn't have any side effects on the HTMLElement.
Question:
Is this "side effect" innocent and is it worth doing for the optimization on my decorator?
Real Code:
Gist of plugin template where I use this snippet
Edit:
I'm clearly too tired and forgot data-foo exists. Here's how it should be implemented
var attr = "data-" + PLUGIN_NAME + "-cache";
return function _cache(elem) {
var val = elem.getAttribute(attr);
if (val === null || val === "") {
val = PLUGIN_NAME + "_" + uid++;
elem.setAttribute(attr, val);
}
if (cache[val] === undefined) {
cache[val] = _factory(elem);
}
return cache[val];
}

Instead of using the id, use data-x - that's what it was created for.
id has a specific meaning, is confusing to see it automagically generated (even if properly documented, which is nearly never.) You're also risking a slight chance of override.

Is this "side effect" innocent
No, clearly. Is it going to interact well with other scripts on the page? Don't know... that depends what it's for and what other kinds of scripts you expect it to be combined with. You can never make a ‘plugin’ that won't ever fail when interacting with other plugins and scripts, but by keeping the side-effects to a minimum you can at least try to minimise it.
Note that id is not a unique identifier. Although there should be only one element with a given ID in a document at one particular time, (a) multiple elements might be created with the same ID and inserted into the document sequentially (perhaps one element replacing another with the same ID), and (b) people still do use duplicate IDs even though it's wrong. Either would cause your cache to collect old, no-longer-used elements and return them inappropriately.
It is unfortunate that there is no JavaScript function to get a scalar/hashable unique identifier for an arbitrary object; the only way to obtain object identity is to ===-compare against other objects.
Another common way forward is to add an arbitrary new property to the node (‘expando’ in IE terms), with a randomised really-unique ID. Expandos aren't guaranteed by standard to work, but it has worked in all browsers back to day one and is commonly used.
This is how for example jQuery identifies elements uniquely, and if you are writing a plugin for jQuery you might try taking advantage of that—jQuery.expando holds the name of the arbitrary expando property being used for this purpose... or, sticking within the documented featureset, data() could be used to add your own metadata to the element including another unique ID of your own.
Expandos do have some unpleasant side-effects including accidentally treating them as attributes in IE<9 (which can't tell the difference between properties and attribute), but if you're using jQuery anyway you probably don't have anything to lose.
is it worth doing for the optimization on my decorator?
Depends how many you're expecting to have on a page. Comparing each item to each other item is an O(n²) operation; tolerable (and probably preferable given the side-effects) if n is low, but quickly getting unmanageable as n grows.

Related

getcomputedstyle only the changes from default

The code I currently have gets the whole CSS, even the default one. What I want is to get only the CSS changed from default.
function baba() {
addEventListener("mouseover", function() {
var elem = document.getElementById("ex");
cssObj = window.getComputedStyle(elem, null)
var txt = "";
for (i = 0; i < cssObj.length; i++) {
cssObjProp = cssObj.item(i)
txt += cssObjProp + " = " + cssObj.getPropertyValue(cssObjProp) + "<br>";
document.getElementById("empty").innerHTML = txt;
}
})
}
<p id="ex" onclick="baba()">Hello World</p>
<h1>Hello World</h1>
<p id="empty"></p>
Okay, so here's how I'd tackle it. Note: I just slammed this out in the console in 5 minutes; I'm sure there are more efficient ways to handle it, but for PoC this should get you going.
Requirements Analysis
Really (barring a more specific edge-case application, anyway), what you're asking for is "How does Element <XXX>'s current computed style differ from a vanilla object of the same type in the same context?" It's nonsensical to ask how it differs from "default" because "default", perforce, is going to be influenced by said context (don't agree? Wait for it; I'll 'splain).
Because of this, really what we need to be examining is a <XXX> that lacks the effects applied to your target object (consequences of its DOM position, class, id, attributes, predecessors, etc.). The good news is: we can totally fake it! Check it out:
The Setup
First thing's first, let's get hold of our object. This would be better executed as a function, I know, but for illustrative purposes, work with me here. Let's pick an object you can see the results on right away. Let's see... how about the Search bar at the top of this very page? Hit f12 to pop your console, and you'll see it's name is 'q'. That'll work.
// Get the StackOverflow Search field from the top of this page.
var targetDOMElement = document.querySelector('[name="q"]');
// Now, let's get its current style snapshot.
var targetObjsStyles = window.getComputedStyle(targetDOMElement);
// ... and vomit it out to our console, just so we know what we're dealing with.
console.log('ORIGINAL SET (' + Object.keys(targetObjsStyles).length + ' rules):',targetObjsStyles);
Capital! Now we have our source object (our "<XXX>", if you will).
The Control
Next, we need something to compare it against. Now, being the obedient little boy who was raised Orthodox Scientist that I am, in my mind that's a control. Fortunately, we know plenty about our source object, so let's manufacture one:
// Create a new element of the same type (e.g. tagName) as our target
var tempCopyOfTarget = document.createElement(targetDOMElement.tagName);
// Insert it into the context AT THE BEGINNING of the page. Both bits here are important:
// if we create it within a documentFragment (try it) literally every property will
// be flagged as unique. I suspect this has to do with the client's default
// renderer, vs. the purity of a abstracted prototype, but I won't lie: I'm guessing.
// It MUST be at the start of the body to avoid silliness like
// body > .first-element ~ xxx { display:none; }
// CSS still won't let us target predecessors/ancestors, alas.
document.body.insertAdjacentElement('afterBegin', tempCopyOfTarget);
// Now our new object shares our target's context, get ITS snapshot.
var basicElementsCSS = window.getComputedStyle(tempCopyOfTarget);
console.log('BASELINE (DUMMY OBJECT) SET (' + Object.keys(basicElementsCSS).length + ' rules):',basicElementsCSS);
The Grunt Work
While I'm certain most folks see where I'm going at this point, let's finish her off. Given a testable quantity, and a control, check for deltas.
// Create an empty object to store any changes in.
var cleanSetOfStyles = {};
// Objectify our target's style snapshot, and iterate.
Object.entries(targetObjsStyles).forEach(p=>{
// If a key-value pair exists that matches our control, ignore it. Otherwise,
// tack it onto our clean object for later perusal.
if(basicElementsCSS[p[0]] !== p[1]){
cleanSetOfStyles[p[0]] = p[1];
}
});
Awesome! Nice work!
Conclusion
Now, assuming my hypothesis is correct, we should see within our clean object a set of properties and their corresponding values. The length of this list should be both non-zero, and different than the count contained within the raw sets above (which, the more observant of you will have noticed, WERE the same, in that the browser assigns ALL possible styles' values to an object when a getComputedStyles collection is requested.
// Display our deltas
console.log('CLEAN SET (' + Object.keys(cleanSetOfStyles).length + ' rules):',cleanSetOfStyles);
// Oh, and always remember to clean up after you make a mess in the lab.
tempCopyOfTarget.remove()
What's this!? VICTORY! At least in my environment (which has to factor my browser make, version, active plug-ins, supported features, operating system, etc., etc.; your mileage may vary), I count 116 rules that remain and are acting on our target object. These are the rules that differ from our vanilla, first-line-of-code object we summoned into being for the picoseconds it took the browser to take a gander at it.
CAVEATS
There's always a catch, isn't there?
This is NOT a foolproof system. I can think of a half dozen ways this will fail off the top of my head (:empty modifiers, depending on the scope you're in... [name="q"] ~ [name="q"] rules, the insertion of our dummy object now making apply to our target... :first-of-type no longer being applicable... all kinds of 'whoopsies'). BUT, I'm prepared to assert all the ones I can think of are both edge cases, and themselves manageable, given proper forethought.
TLDR
Here's the whole code, copy+pasteable directly into console, if you're so inclined, and sans comments:
var targetDOMElement = document.querySelector('[name="q"]');
var targetObjsStyles = window.getComputedStyle(targetDOMElement);
console.log('ORIGINAL SET (' + Object.keys(targetObjsStyles).length + ' rules):',targetObjsStyles)
var tempCopyOfTarget = document.createElement(targetDOMElement.tagName);
document.body.insertAdjacentElement('afterBegin', tempCopyOfTarget);
var basicElementsCSS = window.getComputedStyle(tempCopyOfTarget);
console.log('BASELINE (DUMMY OBJECT) SET (' + Object.keys(basicElementsCSS).length + ' rules):',basicElementsCSS)
var cleanSetOfStyles = {};
Object.entries(targetObjsStyles).forEach(p=>{
if(basicElementsCSS[p[0]] !== p[1]){
cleanSetOfStyles[p[0]] = p[1];
}
});
console.log('CLEAN SET (' + Object.keys(cleanSetOfStyles).length + ' rules):',cleanSetOfStyles);
tempCopyOfTarget.remove()
Final note: I know this question is a couple months old, but nobody really answered it outside of "Nope! You're screwed!"
On the off chance #angels7 still needs the fix, here ya go. Otherwise, "Hi, all you far-out future folk!"

Maybe monad in JavaScript

In the examples for monads.maybe on npm we have:
function find(collection, predicate) {
for (var i = 0; i < collection.length; ++i) {
var item = collection[i]
if (predicate(item)) return Maybe.Just(item)
}
return Maybe.Nothing()
}
Can someone explain what Maybe.Just(item); and Maybe.Nothing() are actually doing?
Put another way; are monads essentially objects used as return values that implement specific interfaces that enable the definition of a sequence of function invocations?
Maybe is used to represent an operation that might fail.
In the case of this function, you return Just(the element) if an element fulfills the predicate, else, you return Nothing to show that it had "failed" (in this case, none of the elements fulfill the predicate).
It's preferred to just returning a null because the return type explicitly shows that it can fail, and the answer can be pattern matched against.
Monads are abstract containers with an API to operate on the data contained within. In the instance of the Option monad I think of it as a giftbox that either has a gift or is empty. Wrapping your data in a Maybe.Just() signifies that this container does infact contain data, while at the same time it maintains the returned value as a Maybe. The caller of your find() method can then do this:
var userPredicate = function(user) { return user.name === 'billy bob'; };
var users = collections.getUsersCollection();
var maybeData = find(users, userPredicate);
if(maybeData.isJust()) {
// there was data...do something with it
} else {
// no data...do something else
}
On the other hand, Maybe.Nothing() indicates the absence of data (the else part in the example above). Ideally, you would wrap your data within like so: var maybeData = Maybe(data) and then operate on this, pass it around etc. This is a signal to anyone receiving this object that they need to handle the case of missing data consciously.
Disclosure: I'm working on a similar library called Giftbox that has a richer API. Take a look at the readme there for some more explanations to help you understand what the Option monad is and how to use it effectively.
Here's an article describing Monads, Applicatives and Functors that might be useful to you.

Efficiently tracking and updating an array of DOM elements with Javascript / jQuery?

Inside of a module I'm writing (its kind of a slider / timeline interface component) I've got a method that updates the controls which are a set of clickable elemetns along the bottom that are updated on click and when the user scrolls.
I'm doing the following to attach classes to the items up until the active one. While the approach I'm using works, its feels very inefficient as I'm looping over a set of DOM elements each time.
updateTimeLine : function(pos, cb) {
var p = pos;
var timeline = $('.timer').toArray();
if (p > 15)
p = 15;
$.each(timeline, function(index,value) {
var that = $(this);
if (index >= p) {
if (that.children('span').hasClass('active'))
that.children('span').removeClass('active');
} else {
that.children('span').addClass('active');
}
});
if (cb && typeof(cb) === "function") {
cb();
}
return this;
},
Is there a better way to do this? If so, how?
Is this a good use case for something like the observer pattern? which I don't fully get, having not spent any time with it yet, so if it is, I'd really like to know how to apply this pattern properly.
Observer patterns notify subscribed objects by looping through and invoking listeners on each subscriber when a relevant change occurs. Because of that, you'd probably end up using $.each anyways. I think what you have is equally efficient.
If you feel bad about iterating over the dom each time, consider this: there exists no such algorithm that can update each dom element without iterating through them. Caching the DOM array theoretically would improve performance, but my money says the browser's already doing that. Try it yourself on this jsperf...

Form handling and validation in pure JavaScript

My intention is to get your thoughts and criticism about the script below, as regards the algorithm's design, performance and cross-browser compatibility.
I have just started getting into JavaScript having missed out on its awesomeness for quite a while. My background and experience is in developing C/C++/PHP based RESTful backends.
In order to understand the language and the right way of using it, I decided to do something which I am sure has been done many times before. But learning to use a new language and paradigm often entails pain anyway.
This is my attempt to create a normal form processing and validation script/ function.
In order to reduce complexity and keep code simple/clean, I decided to use HTML5 Custom Data Attributes (data-*) to assign metadata for each element in the form:
Data-Required: True or False. If set to true, this parameter makes the form-field required and so it cannot be empty. A value set to false indicates that the field is optional. Default is false.>
Data-Type: Type of validation to be performed. Examples include 'email', 'password', 'numbers' or any other 'regexp'.
A fairy simple example of such a form would be:
<form action="postlistings" id="postlistings" enctype='multipart/form-data' method="post" class="postlistings">
<ul class="login-li">
<li>
<input class="title" name="title" type="title" id="title" data-required="true" data-type="title"></a>
</li>
<li>
<textarea name="body" id="elm1" class="elm1" name="elm1" data-type="body" data-required="true" >
</textarea>
</li>
<li>
<span class="nav-btn-question">Add Listing</span>
</li>
</ul>
</form>
Reminder: This is my first piece of JavaScript code.
The idea is to call Form while passing the form name to retrieve and validate all the field values in one loop for performance. The validation involves two steps as can be guessed from the Data-* attributes described above:
i. Check for required form fields.
In case the values fail to meet step 1 requirement, an error message from configuration is pulled for the specific form value. Thus, for all values that fail to meet this requirement, an array of error messages are collected and passed on to the View.
ii. Perform respective validations.
Validations are only performed if all the values passed step 1. Otherwise, they follow the same steps as indicated in 1 above.
function Form(){
var args = Array.prototype.slice.call(arguments),
formName = args[0],
callback = args.pop(),
userError = [{type: {}, param: {}}],
requiredDataParam = 'required',
typeDataParam = 'type',
form = document.forms[formName],
formLength = form.length || null,
formElement = {id: {}, name: {}, value: {}, required: {}, type: {}};
function getFormElements(){
var num = 0;
var emptyContent = false;
for (var i = 0; i < formLength; i += 1) {
var formField = form[i];
formElement.id[i] = inArray('id', formField) ? formField.id : null;
formElement.name[i] = inArray('name', formField) ? formField.name : null;
formElement.value[i] = inArray('value', formField) ? formField.value : null;
formElement.required[i] = getDataAttribute(formField, requiredDataParam);
formElement.type[i] = getDataAttribute(formField, typeDataParam);
if (formElement.required[i] === true){
if(!formElement.type[i]) {
error('Validation rule not defined!');
}
else if (!formElement.value[i]) {
userError[num++] = {'type': 'required', 'param': form[i]};
emptyContent = true;
}
}
if (emptyContent === false) {
// Perform validations only if no empty but required form values were found.
// This is so that we can collect all the empty
// inputs and their corresponding error messages.
}
}
if (userError) {
// Return empty form errors and their corresponding error messages.
}
return formElement;
};
// Removed the getFormParam function that was not used at all.
return {
getFormElements: getFormElements
}
};
Two outside functions that are used in the JS script above (from JQuery source):
var inArray = function(elem, array){
if (array.indexOf){
return array.indexOf(elem);
}
for (var i = 0, length = array.length; i < length; i++){
if (array[i] === elem){
return i;
}
}
return -1;
}
// This is a cross-platform way to retrieve HTML5 custom attributes.
// Source: JQuery
var getDataAttribute = function(elem, key, data) {
if (data === undefined && elem.nodeType === 1) {
data = elem.getAttribute("data-" + key);
if (typeof data === "string") {
data = data === "true" ? true :
data === "false" ? false :
data === "null" ? null :
!CheckType.isNaN ? parseFloat(data) :
CheckType.rbrace.test(data) ? parseJSON(data) :
data;
}
else {
data = undefined;
}
}
return data;
}
An example of Config Error messages can be set as follows:
var errorMsgs = {
ERROR_email: "Please enter a valid email address.",
ERROR_password: "Your password must be at least 6 characters long. Please try another",
ERROR_user_exists: "The requested email address already exists. Please try again."
};
As I post this for your review, please ignore any styling conventions that I might not have followed. My intention is to get your expert reviews on anything I should be doing different or could do better concerning the code itself, and the algorithm.
Besides the styling conventions, all criticism and questions are welcome.
First I'd like to clear up a common misconception. Forgive me if you already understand this clearly; maybe it will be helpful for someone else.
Learning and using jQuery or a similar library does not preclude or conflict with learning the JavaScript language. jQuery is simply a DOM manipulation library which takes away many of the pain points of using the DOM. There's plenty of room to learn and use JavaScript, the language, even if you use a library to abstract away some of the DOM details.
In fact, I would argue that using the DOM directly is likely to teach bad JavaScript coding habits, because the DOM is very much not a "JavaScript-ish" API. It was designed to work identically in JavaScript and Java and potentially other languages, and so it completely fails to make good use of the features of the JavaScript language.
Of course as you said, you're using this as a learning exercise; I just don't want you to fall into the trap that I've seen many people fall into of thinking, "I don't want to learn jQuery, because I want to learn JavaScript instead!" That's a false dichotomy: you have to learn JavaScript in either case, and using jQuery for the DOM doesn't interfere with that at all.
Now some details...
While it's OK to quote property names in an object literal and when you reference the properties, it's customary - and more readable - not to quote them when they are valid JavaScript names. e.g. in your formElement object
formElement = { id: {}, name: {}, value: {}, required: {}, type: {} };
(there was a missing semicolon at the end there too)
and where you use the names you can do:
formElement.id[i] = ...
formElement.name[i] = ...
etc.
Don't run your loops backwards unless the program logic requires it. It doesn't make the code faster except possibly in the case of an extremely tight loop, and it makes it unclear whether you're just prematurely optimizing or actually need the backwards loop.
Speaking of optimization, that loop has several inArray() calls. Since each of those loops through an array, that could be more of a performance impact than the outer loop. I imagine these arrays are probably pretty short? So performance wouldn't matter at all anyway, but this is something to think about in cases where you have longer arrays and objects. In some cases you can use an object with property names and values for a faster lookup - but I didn't look closely enough at what you're doing to suggest anything.
In any case, you're using inArray() wrong! But not your fault, that is a ridiculously named function in jQuery. The name clearly suggests a boolean return value, but the function returns the zero-based array index or -1 if the value is not found. I strongly recommend renaming this function as indexOf() to match the native Array method, or arrayIndex(), or some such.
That same loop has form[i] repeated numerous times. You could do this at the top of the loop:
var field = form[i];
and then use field throughout, e.g. field.id instead of form[i].id. This is generally faster, if it matters (which it probably doesn't here), but more importantly it's easier to read.
Do not use strict boolean comparisons like if( foo === true ) and if( bar === false) unless you really need to - and those cases are rare. The code sends a signal to the reader that there is something going on that's different from the usual boolean test. The only time these particular tests should be used is when you have a variable that may contain a boolean value or may contain some other type of value, and you need to distinguish which is which.
A good example of a case where you should use tests like these is an optional parameter that defaults to true:
// Do stuff unless 'really' is explicitly set to false, e.g.
// stuff(1) will do stuff with 1, but stuff(1,false) won't.
function stuff( value, really ) {
if( really === false ) {
// don't do stuff
}
else {
// do stuff
}
}
That specific example doesn't make a lot of sense, but it should give you the idea.
Similarly, an === true test could be used in a case where need to distinguish an actual boolean true value from some other "truthy" value. Indeed, it looks like this line is a valid case for that:
if (formElement['required'][i] === true){
given that if (formElement['required'][i] comes from the getDataAttribute() function which may return a boolean or other type.
If you are just testing for truthiness, though - and this should be most of the time - simply use if( foo ) or if( ! foo ). Or similarly in a conditional expression: foo ? x : y or !foo ? x : y.
The above was a long-winded way of saying that you should change this:
if (empty_content === false) {
to:
if (!empty_content) {
Your getFormParam() function goes to some work to convert an undefined result to null. There is usually no reason to do this. I don't see any place where that function is called, so I can't advise specifically, but in general you'd be testing for truthiness on something like this, so null and undefined would both be treated as false. Or in cases where you do need to distinguish null/undefined from other values (say, an explicit false), you can easily do it with != null or == null. This is one case where the "looser" comparison performed by == and != is very useful: both null and undefined evaluate the same with these operators.
You asked to ignore coding style, but one little suggestion here: You have a mix of camelCaseNames and names_with_underscores. In JavaScript, camelCaseNames are more idiomatic for function and variable names, with PascalCaseNames for constructor functions. Of course feel free to use underscores where they make more sense, for example if you're writing code that works with database columns in that format you may want your variable names to match the column names.
Hope that helps! Keep up the good work.
Update for your new code
I'm having a bit of trouble following the logic in the code, and I think I know part of the reason. It's a combination of naming conventions and inside-out objects.
First, the name formElement is really confusing. When I see element in JavaScript, I think of either a DOM element (HTMLElement) or an array element. I'm not sure if this formElement represents one or the other or neither.
So I look at the code to figure out what it's doing, and I see it has id:{}, name:{}, ... properties, but the code later treats each of those as an Array and not an Object:
formElement.id[i] = ...
formElement.name[i] = ...
formElement.value[i] = ...
formElement.required[i] = ...
formElement.type[i] = ...
(where i is an integer index)
If that code is right, those should be arrays instead: id:[], name:[], ....
But this is a red flag. When you see yourself creating parallel arrays in JavaScript, you're probably doing it wrong. In most cases you're better off replacing the parallel arrays with a single array of objects. Each of the objects in that array represents a single slice through all your parallel arrays, with a property for each of the previous arrays.
So, this object (where I've made the correction from {} to [] to match its current use):
formElement = { id: [], name: [], value: [], required: [], type: [] };
should be:
formInfo = [];
and then where you have the code that goes:
formElement.id[i] = ...;
formElement.name[i] = ...;
formElement.value[i] = ...;
formElement.required[i] = ...;
formElement.type[i] = ...;
It should be:
var info = {
id: ...,
name: ...,
value: ...,
required: ...,
type: ...
};
formInfo.push( info );
and adjust the rest of the code to suit. For example:
formElement.required[i]
would be:
formInfo[i].required
or even simpler since it's in the same function:
info.required
And note: I'm not saying info and formInfo are great names :-) they are just placeholders so you can think of a better name. The main idea is to create an array of objects instead of a set of parallel arrays.
One last thing and then I'm out of time for now.
That getDataAttribute() function is a complicated little piece of work. You don't need it! It would be simpler would just call the underlying function directly where you need it:
var info = {
...
required: formField.getAttribute('data-required') === 'true',
type: formField.getAttribute('data-type')
};
This also gives you full control of how the attributes are interpreted - as in the === 'true' test above. (This gives you a proper boolean value, so when you test the value later you don't have to use === true on it.)
On a stylistic note, yes, I did hard code the two 'data-xxxx' names right there, and I think that's a better and more clear way to do it.. Don't let your C experience throw you off here. There's no advantage to defining a string "constant" in this particular case, unless it's something that you want to make configurable, which this isn't.
Also, even if you do make a string constant, there's a minor advantage to having the complete 'data-whatever' string instead of just 'whatever'. The reason is that when somebody reads your HTML code, they may see a string in it and search the JS code for that string. But when they search for data-whatever they won't find it if the data- prefix is automagically prepended in the JS code.
Oh, I forgot one last thing. This code:
function Form(){
var args = Array.prototype.slice.call(arguments),
formName = args[0],
callback = args.pop(),
is working way too hard! Just do this instead:
function Form( formName, callback ) {
(and keep the var for the remaining variable declarations of course)
I cannot add comments yet so here is a little tip. I would separate the getFormElements() into smaller private functions. And I would add the errorMsgs to the Form function.
But for a first script in JavaScript, it is very impressive. This is actually the real reason I respond. I think it deserves more upvotes, and I would be very interested in a JS ninja responding to this question.
Good luck!

Algorithms for Directed Cyclic Graph Traversal (JavaScript)

I have a connected, directed, cyclic graph. The task is to discover every single node in the graph without falling into an infinite loop, as a regular tree traversal algorithm will do.
You can assume that I already know what node to start at so as to reach all points in the directed graph, and that for each node I have a function that will return the nodes it directs to. Is there a known algorithm for finding all nodes?
The main issue is really avoiding cycles, and I would love it if there was a way to do that without keeping track of every single node and comparing it with a list of nodes that has already been traversed.
If you need more details, the actual task is to get a list of every named function in JavaScript, including functions that are properties of other objects. So I tried something like the following, as I thought the JS objects' references to each other made a tree (but of course it doesn't):
function __findFunctions(obj){
for (var f in obj){
// for special case of edge with self
if (obj === obj[f]){
continue
}
if (typeof obj[f] === 'function' &&
obj.hasOwnProperty(f) &&
// exclude native functions, we only want user-defined ones
!(/\[(native\scode|object\sfunction)\]/i).test(obj[f].toString()) &&
// exclude functions with __ prefix
!(/^\s*function\s*__/).test(obj[f].toString())
){
document.write(f + "<br/>" + obj[f].toString() + "<hr/>");
}
//alert(typeof obj[f] + "\n" + obj + "\n" + obj[f] + "\n" + f)
__findFunctions(obj[f]);
}
}
__findFunctions(window);
The problem with this code is that it gets stuck in cycles.
I would love it if there was a way to do that without keeping track of every single node and comparing it with a list of nodes that has already been traversed.
It may not be as bad as checking a list of already-traversed nodes. You could, instead, give each node a unique ID of some sort:
// psuedo
id=0;
for each node
node.id = id++;
etc.
Then you can add each node's ID to a hash while you traverse:
var alreadyTraversed = {};
// Traversing a node:
alreadyTraversed[node.id] = true;
And later on, when you need to check whether or not its already been traversed:
if (node.id in alreadyTraversed) // It's already been traversed...
Or, for a really rudimentary solution, simply set some property on each object that you traverse:
node._traversed = true;
// Later:
if (someNode._traversed) // already traversed.
You would need to maintain a list of already visited nodes if you want to avoid cycles.
E.g.
__findFunctions(obj, visited) as your signature
at start do an "in array" test for current obj and return if so.
at start add obj to the visited for subsequent traversals.

Categories

Resources