Waldo: Search the JavaScript Object Model in under 1 KB

Here’s a tiny util that you can save as a bookmarklet and use to crawl the JavaScript object model of any web site. Waldo (thanks to @shinypb for the name!) lets you find and inspect instances by name, type or value, and it can be easily customized to add additional tests. It runs in the console on Chrome, Firefox, Safari and IE>8. It's sourced on github. Feel free to fork it if you want to add more search methods or a spiffy UI.

(Update: Check out this alternate version by @jdalton)



The Basics

Loading the Waldo script will create a global object called find which invokes a set of utility methods: byName, byNameContains, byType, byValue, byValueCoerced and custom. Each method will crawl the entire runtime JavaScript model from window down (unless otherwise specified) and report every object that matches the search term argument. There is also an optional options argument for specifying a) the root object for the search and b) the root of the object path that will display in the search results (the latter option is purely cosmetic).

find.util( searchTerm [, options ] )

examples:

//util = 'byValueCoerced', searchTerm = 'false' 
find.byValueCoerced(false); 

//util = 'byType', searchTerm = Date, rootObject = jQuery, rootPath = 'jQuery' 
find.byType(Date, {obj: jQuery, path: 'jQuery'}); 

 
The Built-in Utilities

Waldo comes with five built-in utilities. Here they are (I’m only showing the mandatory params):

1. find.byName(<string>)

Waldo returns all instances whose property name matches the supplied string. For example we might want to look for where Flash is defined in a site…

(moma.org)

…or references to map in the jQuery runtime…

(twitter.com)

 
2. find.byNameContains(<string>)

This is similar to find.byName but the search term need only match a substring of the property name:

(dropbox.com)

3. find.byType(<Function>)

Waldo returns all objects that are an instanceof the given constructor.

One use case is tracking down all date instances in a site – perhaps to narrow in on a timezone offset bug:

(bbc.com)

Here’s a report of all arrays used in the Huffington Post’s slider utility:

(huffingtonpost.com)

 
4. find.byValue(<any type>)

Waldo will perform a strict equality search (===) against every object in the runtime model. I’ve found this one useful for locating configuration values.

(bbc.com)


 
5. find.byValueCoerced(<any type>)

Similar to find.byValue except this time the equality check allows coercion (==) – useful for locating falsey values. It’s also convenient when you’re not sure what type you’re searching for – for example the Huffington Post has a “Recent Blog Posts” section with a pagination control showing a maximum value of “4”. I’m looking for supporting code for this control but I’m not sure whether to look for a number or a string. No problem:

(huffingtonpost.com)

6. find.custom(<Function>)

You can use this method to apply any custom function to the search. The function you provide is the one used to match each property of each object found in the runtime model. Here’s the signature…

function(searchTerm, obj, prop) {}

…and here’s an example that finds every truthey value whose property name is ‘_blog’:

(wordpress.com)

 
Extending Waldo

You can easily add your own utilities to Waldo. Most of the code is generic – you just need to extend the public interface…

window.find={
  byName: function(searchTerm, options) {dealWithIt('name', 'string', searchTerm, options);},
  byNameContains: function(searchTerm, options) {dealWithIt('nameContains', 'string', searchTerm, options);},
  byType: function(searchTerm, options) {dealWithIt('type', 'function', searchTerm, options);},
  byValue: function(searchTerm, options) {dealWithIt('value', null, searchTerm, options);},
  byValueCoerced: function(searchTerm, options) {dealWithIt('valueCoerced', null, searchTerm, options);},
  custom: function(fn, options) {traverse(fn, null, options);}
}

 
…and then define your custom function here…

var tests = {
  'name': function(searchTerm, obj, prop) {return searchTerm == prop},
  'nameContains': function(searchTerm, obj, prop) {return prop.indexOf(searchTerm)>-1},
  'type': function(searchTerm, obj, prop) {return obj[prop] instanceof searchTerm},
  'value': function(searchTerm, obj, prop) {return obj[prop] === searchTerm},
  'valueCoerced': function(searchTerm, obj, prop) {return obj[prop] == searchTerm}
}

 
Wrap Up

Here’s the full source code…

(function(){
  var traverse = function(util, searchTerm, options) {
    var options = options || {};
    var obj = options.obj || window;
    var path = options.path || ((obj==window) ? "window" : "");
    var props = Object.keys(obj);
    props.forEach(function(prop) {
      if ((tests[util] || util)(searchTerm, obj, prop)){
        console.log([path, ".", prop].join(""), "->",["(", typeof obj[prop], ")"].join(""), obj[prop]);
      }
      if(Object.prototype.toString.call(obj[prop])=="[object Object]" &&
          (obj[prop] != obj) && path.split(".").indexOf(prop) == -1) {
        traverse(util, searchTerm, {obj: obj[prop], path: [path,prop].join(".")});
      }
    });
  }

  var dealWithIt = function(util, expected, searchTerm, options) {
    (!expected || typeof searchTerm == expected) ?
      traverse(util, searchTerm, options) :
      console.error([searchTerm, 'must be', expected].join(' '));
  }

  var tests = {
    'name': function(searchTerm, obj, prop) {return searchTerm == prop},
    'nameContains': function(searchTerm, obj, prop) {return prop.indexOf(searchTerm)>-1},
    'type': function(searchTerm, obj, prop) {return obj[prop] instanceof searchTerm},
    'value': function(searchTerm, obj, prop) {return obj[prop] === searchTerm},
    'valueCoerced': function(searchTerm, obj, prop) {return obj[prop] == searchTerm}
  }

  window.find={
    byName: function(searchTerm, options) {dealWithIt('name', 'string', searchTerm, options);},
    byNameContains: function(searchTerm, options) {dealWithIt('nameContains', 'string', searchTerm, options);},
    byType: function(searchTerm, options) {dealWithIt('type', 'function', searchTerm, options);},
    byValue: function(searchTerm, options) {dealWithIt('value', null, searchTerm, options);},
    byValueCoerced: function(searchTerm, options) {dealWithIt('valueCoerced', null, searchTerm, options);},
    custom: function(fn, options) {traverse(fn, null, options);}
  }
})();

 
…and here’s the minified source should you want to create a bookmarklet

javascript:(function(){var c=function(d,e,f){var f=f||{};var i=f.obj||window;var h=f.path||((i==window)?"window":"");var g=Object.keys(i);g.forEach(function(j){if((b[d]||d)(e,i,j)){console.log([h,".",j].join(""),"->",["(",typeof i[j],")"].join(""),i[j])}if(Object.prototype.toString.call(i[j])=="[object Object]"&&(i[j]!=i)&&h.split(".").indexOf(j)==-1){c(d,e,{obj:i[j],path:[h,j].join(".")})}})};var a=function(d,g,e,f){(!g||typeof e==g)?c(d,e,f):console.error([e,"must be",g].join(" "))};var b={name:function(d,e,f){return d==f},nameContains:function(d,e,f){return f.indexOf(d)>-1},type:function(d,e,f){return e[f] instanceof d},value:function(d,e,f){return e[f]===d},valueCoerced:function(d,e,f){return e[f]==d}};window.find={byName:function(d,e){a("name","string",d,e)},byNameContains:function(d,e){a("nameContains","string",d,e)},byType:function(d,e){a("type","function",d,e)},byValue:function(d,e){a("value",null,d,e)},byValueCoerced:function(d,e){a("valueCoerced",null,d,e)},custom:function(e,d){c(e,null,d)}}})();

 
Both sources are also available on github. I hope you have fun using Waldo and look forwarding to seeing how folks are able to fork it with extra coolness!

15 thoughts on “Waldo: Search the JavaScript Object Model in under 1 KB

  1. I updated the code to also search functions (can have props attached) and prototypes:

    (function(){var f=function(b,c,a){var a=a||{},d=a.obj||window,e=a.path||(d==window?”window”:””),a=Object.keys(d);a.push(“prototype”);a.forEach(function(a){typeof a!==”undefined”&&((g[b]||b)(c,d,a)&&console.log([e,”.”,a].join(“”),”->”,[“(“,typeof d[a],”)”].join(“”),d[a]),((“”+d[a]).indexOf(“function”)===0||””+d[a]==”[object Object]”&&d[a]!=d&&e.split(“.”).indexOf(a)==-1)&&f(b,c,{obj:d[a],path:[e,a].join(“.”)}))})},e=function(b,c,a,d){!c||typeof a==c?f(b,a,d):console.error([a,”must be”,c].join(” “))}, g={name:function(b,c,a){return b==a},nameContains:function(b,c,a){return a.indexOf(b)>-1},type:function(b,c,a){return c[a]instanceof b},value:function(b,c,a){return c[a]===b},valueCoerced:function(b,c,a){return c[a]==b}};window.find={byName:function(b,c){e(“name”,”string”,b,c)},byNameContains:function(b,c){e(“nameContains”,”string”,b,c)},byType:function(b,c){e(“type”,”function”,b,c)},byValue:function(b,c){e(“value”,null,b,c)},byValueCoerced:function(b,c){e(“valueCoerced”,null,b,c)},custom:function(b, c){f(b,null,c)}}})();

    1. thanks – though array’s could have properties too – wanted to draw the line somewhere

      Also if you’re checking for functions too its more efficient to do either

      Object.prototype.toString.call(obj)=="[object Function]"
      

      or simply

      typeof obj =="function"
      

      ((“” + obj).indexOf searches the entire function body)

      1. Good points. I hope it’s apparent that I really appreciate the work you’ve done.

        Also, I’ve added the following extension in case anyone finds it useful:

        var tests = {
        ‘valueContains’: function(searchTerm, obj, prop) {return typeof obj[prop] === ‘string’ && obj[prop].indexOf(searchTerm)>-1}
        }

        window.find={
        byValueContains: function(searchTerm, options) {dealWithIt(‘valueContains’, null, searchTerm, options);},
        }

        So you can do something like finding URLs without having to know the exact URL:

        find.byValueContains(‘javascriptweblog.wordpress.com’);

    2. // this is writ for people, not puters! 🙂 enjoy.

      (function() {
       var f=function(b,c,a) {
        var a=a || {},
        d=a.obj || window,e=a.path || (d==window?”window”:”"),
        a=Object.keys(d);
        a.push(“prototype”);
        a.forEach(
         function(a) {
          typeof a!==”undefined”
           && ((g[b]||b)(c,d,a)
           && console.log([e,".",a].join(“”),”->”,["(",typeof d[a],”)”]
           . join(“”), d[a]),((“”+ d[a])
           . indexOf(“function”) === 0
           || ”" + d[a]==”[object Object]“
           && d[a]!=d
           && e.split(“.”).indexOf(a)==-1)
           && f(b,c,{obj:d[a],path:[e,a].join(“.”)})
          )
         }
        )
       },
       e=function(b,c,a,d) {
        !c || typeof a==c?f (b,a,d) : console.error([a,"must be",c].join(” “))
       },
       g={
        name:function(b,c,a){return b==a},
        nameContains:function(b,c,a){return a.indexOf(b)>-1},
        type:function(b,c,a){return c[a]instanceof b},
        value:function(b,c,a){return c[a]===b},
        valueCoerced:function(b,c,a){return c[a]==b}
       };
       window.find= {
        byName:function(b,c) {e(“name”,”string”,b,c)},
        byNameContains:function(b,c) {e(“nameContains”,”string”,b,c)},
        byType:function(b,c) {e(“type”,”function”,b,c)},
        byValue:function(b,c) {e(“value”,null,b,c)},
        byValueCoerced:function(b,c) {e(“valueCoerced”,null,b,c)},
        custom:function(b, c){f(b,null,c)}
        }
       }
      )();
      
  2. Would have enjoyed a version that doesn’t rely on Object.keys–the reason being that the IATA CUSS spec requires using IE8 (still) and that’s what I’m stuck with. IE8 doesn’t do Object.keys.

      1. Just looked–it’s a little weighty for what I need to do–but–later research shows that the MDN page for Objects.keys has something I can use. I can see using that shim in some other environments for sure, though. Thanks!

Leave a comment