1
\$\begingroup\$

I'm fairly new at JavaScript, and I'm finding that JS is a language with what seems like an unusually high number of gotchas for the inexperienced, so some criticisms of this code would be particularly appreciated.

The function getProperties below is a relatively simple function that returns an array of the names (not the values) of all the properties that an object has (either "owned" or "inherited"). Here's the code, followed by some description of what it does:

function getProperties(o) {
  var seenobj = new Set();
  var seenprop = new Set();
  function _proto(obj) {
    return obj instanceof Object ?
           Object.getPrototypeOf(obj) :
           obj.constructor.prototype;
  }
  function _properties(obj) {
    var ret = [];
    if (obj === null || seenobj.has(obj)) { return ret; }
    seenobj.add(obj);
    if (obj instanceof Object) {
      var ps = Object.getOwnPropertyNames(obj);
      for (var i = 0; i < ps.length; ++i) {
        if (!seenprop.has(ps[i])) {
          ret.push(ps[i]);
          seenprop.add(ps[i]);
        }
      }
    }
    return ret.concat(_properties(_proto(obj)));
  }
  return _properties(o);
}

This function has four local variables: _proto, _properties, seenobj, seenprop. The first one, _proto is a helper function to streamline computing an object's prototype. The second one, _properties, is a recursive function, and it actually does all the work. The third one, seenobj, is a (ECMAScript 6) Set object used to keep track of the objects already encountered in the recursion (and therefore avoid infinite recursion). The fourth one, seenprop, also a Set object, similarly keeps track of the names of properties already seen, and thus ensures that property names are not repeated in the returned value. (NOTE: Set objects are available only in a few browsers. An obvious improvement would be to replace these objects with a more portable alternative.)

If the argument obj of _properties is null, or if it is already present in seenobj, _properties returns an empty array. (These are the conditions that terminates the recursion.)

Otherwise, if obj is an instance of Object, _properties gets the list of its properties with Object.getOwnPropertyNames. _properties then returns as its result the array consisting of those properties of obj (if any) that are not already in the seen variable concatenated with the array returned by a recursive call with Object.getPrototypeOf(obj) as argument.

Here's getProperties applied to some standard items (the interaction shown is from the Firefox/Firebug console, though I've added line breaks in the output to avoid long lines):

>>> getProperties({})
[]
>>> getProperties(0)
["constructor", "toSource", "toString", "toLocalString", "toFixed", "toExponential",
"toPrecision"]
>>> getProperties("")
["length", "constructor", "quote", "toSource", "toString", "valueOf", "substring",
"toLowerCase", "toUpperCase", "charAt", "charCodeAt", "contains", "indexOf", "lastIndexOf",
"startsWith", "endsWith", "trim", "trimLeft", "trimRight", "toLocaleLowerCase",
"toLocaleUpperCase", "localeCompare", "match", "search", "replace", "split", "substr",
"concat", "slice", "bold", "italics", "fixed", "fontsize", "fontcolor", "link", "anchor",
"strike", "small", "big", "blink", "sup", "sub", "iterator"]
>>> getProperties([])
["length", "constructor", "toSource", "toString", "toLocaleString", "join", "reverse",
"sort", "push", "pop", "shift", "unshift", "splice", "concat", "slice", "indexOf",
"lastIndexOf", "forEach", "map", "reduce", "reduceRight", "filter", "some", "every",
"iterator"]
>>> getProperties(function () {})
["prototype", "length", "name", "arguments", "caller", "constructor", "toSource",
"toString", "apply", "call", "bind", "isGenerator"]

Thanks for reading this. I look forward to your comments.


EDIT: The code shown above incorporates fixes to various problems that I noticed after posting the original version. This original version is shown below, FWIW.

function getProperties(o) {
  var seen = {};
  function _properties(obj) {
    var ret = [];
    if (obj === null) {
      return ret;
    }
    try {
      var ps = Object.getOwnPropertyNames(obj);
    }
    catch (e if e instanceof TypeError &&
                e.message === "obj is not an object") {
      return _properties(obj.constructor.prototype);
    }
    for (var i = 0; i < ps.length; ++i) {
      if (typeof seen[ps[i]] === "undefined") {
        ret.push(ps[i]);
        seen[ps[i]] = true;
      }
    }
    return ret.concat(_properties(Object.getPrototypeOf(obj)));
  }
  return _properties(o);
}
\$\endgroup\$
2
\$\begingroup\$

As you said, I have a little issue with the use of a try/catch statement. I tried to write it again using, imo, neater constructs:

function getProperties(o) {
    var results = []; 
    function properties(obj) {
        var props, i;
        if (obj == null) {
            return results;
        }   
        if (typeof obj !== 'object' && typeof obj !== 'function') {
            return properties(obj.constructor.prototype);
        }   
        props = Object.getOwnPropertyNames(obj);
        i = props.length;
        while (i--) {
            if (!~results.indexOf(props[i])) {
                results.push(props[i]);
            }   
        }   
        return properties(Object.getPrototypeOf(obj));
    }   
    return properties(o);
}

There is one big difference between my code and your code though: you remove all the properties of object on the line typeof seen[ps[i]] === "undefined". I don't know if it was intended behaviour or not; just pointing out!

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.