Skip to content

Instantly share code, notes, and snippets.

@stracker-phil
Last active March 14, 2024 19:10
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save stracker-phil/e5b3bbd5d5eb4ffb2acdcda90d8bd04f to your computer and use it in GitHub Desktop.
Save stracker-phil/e5b3bbd5d5eb4ffb2acdcda90d8bd04f to your computer and use it in GitHub Desktop.
Recursively searches the entire object tree for a given value
/**
* Recursively searches the startObject for the given value.
*
* All matches are displayed in the browser console and stored in the global variable "gsResults"
* The function tries to simplify DOM element names by using their ID, when possible.
*
* Usage samples:
*
* globalSearch( document, 'someValue' ); // Search entire DOM document for the string value.
* globalSearch( document, '^start' ); // Simple regex search (function recognizes prefix/suffix patterns: "^..." or "...$").
* globalSearch( document, new Regex('[a|b]') ); // Advanced Regex search.
* globalSearch( 'myObj', 'value', 'key' ); // Searches all keys with the name "value" inside the object window.myObj
* globalSearch( window, 'value', 'key', 3 ); // Ends the search after 3 results were found.
* globalSearch( window, 'value', 'all', 3 ); // Finds the first three occurances of "value" in either a object key or value.
*
* globalSearch( document, 'some_value', 'key', 20 )
* Output:
*
* 1. Match: KEY
* Value: [string] on
* Address: window.gsResults[1]
* document.getElementById("the-id").childNodes[1].__reactInternalInstance$v9o4el5z24e.alternate..memoizedProps._owner.alternate.memoizedState.attrs.some_value
*
* @param {string|object} startObject The object to search. Either an object, or the global object name as string.
* @param {mixed} value The value to find. Can be any type, or a Regex string.
* @param {string} searchField Either of [value|key|all].
* @param {int} limit Max results to return. Default is -1, which means "unlimited".
*/
function globalSearch(startObject, value, searchField = 'value', limit = -1) {
var startName = '';
if ('string' === typeof startObject) {
startName = startObject;
startObject = eval(startObject);
} else if (window === startObject) {
startName = 'window';
} else if (document === startObject) {
startName = 'document';
}
var stack = [[startObject, startName, startName]];
var searched = [];
var found = 0;
var count = 1;
var isRegex = 'string' === typeof value && (-1 !== value.indexOf('*') || '^' === value[0] || '$' === value[value.length-1]);
window.gsResults = [];
if (isRegex) {
value = new RegExp(value);
} else if ('object' === typeof value && value instanceof RegExp) {
isRegex = true;
}
if (!searchField) {
searchField = 'value';
}
if (-1 === ['value', 'key', 'all'].indexOf(searchField)) {
console.error('The "searchField" parameter must be either of [value|key|all]. Found:', searchField);
return;
}
function isArray(test) {
var type = Object.prototype.toString.call(test);
return '[object Array]' === type || '[object NodeList]' === type;
}
function isElement(o){
return (
typeof HTMLElement === "object" ? o instanceof HTMLElement : //DOM2
o && typeof o === "object" && o !== null && o.nodeType === 1 && typeof o.nodeName==="string"
);
}
function isMatch(item) {
if (isRegex) {
return value.test(item);
} else {
return item === value;
}
}
function result(type, address, shortAddr, value) {
var msg = [];
found++;
window.gsResults[found] = {
match: type,
value: value,
pathOrig: address,
pathShort: shortAddr
};
msg.push(found + ". Match: \t" + type.toUpperCase()),
msg.push(" Value: \t[" + (typeof value ) + '] ' + value);
msg.push(" Address: \twindow.gsResults[" + found + ']');
msg.push('%c' + shortAddr);
console.log(msg.join("\n"), 'background:Highlight;color:HighlightText;margin-left:12px');
}
function skip(obj, key) {
var traversing = [
'firstChild',
'previousSibling',
'nextSibling',
'lastChild',
'previousElementSibling',
'nextElementSibling',
'firstEffect',
'nextEffect',
'lastEffect'
];
var scopeChange = [
'ownerDocument',
];
var deprecatedDOM = [
'webkitStorageInfo',
];
if (-1 !== traversing.indexOf(key)) { return true; }
if (-1 !== scopeChange.indexOf(key)) { return true; }
if (-1 !== deprecatedDOM.indexOf(key)) { return true; }
var isInvalid = false;
try {
obj[key]
} catch(ex) {
isInvalid = true;
}
return isInvalid;
}
while (stack.length) {
if (limit > 0 && found >= limit) { break; }
var fromStack = stack.pop();
var obj = fromStack[0];
var address = fromStack[1];
var display = fromStack[2];
if ('key' !== searchField && isMatch(obj)) {
result( 'value', address, display, obj);
if (limit > 0 && found >= limit) { break; }
}
if (obj && typeof obj == 'object' && -1 === searched.indexOf(obj)) {
var objIsArray = isArray(obj);
if ( isElement(obj) && obj.id ) {
display = 'document.getElementById("' + obj.id + '")';
}
for (i in obj) {
if (skip(obj, i)) { continue; }
var subAddr = (objIsArray || 'number' === typeof i) ? '[' + i + ']' : '.' + i;
var addr = address + subAddr;
var displayAddr = display + subAddr;
stack.push([obj[i], addr, displayAddr]);
count++;
if ('value' !== searchField && isMatch(i)) {
result( 'key', address, displayAddr, obj[i]);
if (limit > 0 && found >= limit) { break; }
}
}
searched.push(obj);
}
}
searched = null;
console.log('-----');
console.log('All Done!');
console.log('Searched', count.toLocaleString(), 'items');
console.log('Found', found.toLocaleString(), 'results');
return found;
}
@nevercast
Copy link

nevercast commented Aug 3, 2020

Thanks heaps for the script Phil, and to Tom for the original StackOverflow answer I found this Gist from! https://stackoverflow.com/questions/12102425/recursively-search-for-a-value-in-global-variables-and-its-properties/12103127#12103127

Couple issues I had:

  1. The skip function should check if obj === window.gsResults and mark that as isInvalid = true; because obviously we are going to see our search string in the global results.
  2. isElement may throw a DOMException: Blocked a frame with origin if this script tries to inspect iframe objects that it does not have access to. Simplist solution for this is just add a try{}catch and return false; to the isElement function. It's possible that this could still be an issue elsewhere in the code, I've not looked hard enough.

Further thoughts:
It would be nice if the results could be clicked to immediately inspect that object. I know Chrome supplies some cool console logging tools but I've not investigated further.

I see searched prevents multiple visits to the same parent, and display shows a shorter address via a document.getElementById. But the address from startObject is lost. It would be nice to keep the shortest path to the same parent by keeping a track of the path each time an already searched element is visited again and updating the addr. This way, when the results are displayed you get both the document.getEleme... and the shortest full address found from the startObject.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment