5
\$\begingroup\$

As an exercise, I wrote a simple function that composes a prototype chain from a list of objects by appending to the prototype chain of the first object, the rest of the objects. This is the code (example usage and tests in this gist):

var assign = Object.assign || require('object.assign');

// Non-enumerable properties are omitted. Property attributes are not respected 
// and getters and setters aren't copied.
function appendToProto(obj, proto)
{
  if (obj === null)
    throw new Error(
      'Prototype chains are assumed to be delimited by Object.prototype.'
    );
  else if (obj === Object.prototype)
    return proto;
  else
    return assign(
      Object.create(
        appendToProto(Object.getPrototypeOf(obj), proto)
      ),
      obj
    );
}

// Takes a list of objects. The first object will be the farthest away from root
// (Object.prototype).
function composeProtoChain(prototypes)
{
  return prototypes.reduce(appendToProto);
}

As noted above, I can identify three issues with this code:

  1. Non-enumerable properties are omitted from the prototype chain.
  2. Property attributes are not respected and getters and setters aren't copied.
  3. The assumption that Object.prototype is the last object before null in the prototype chain of objects.

I don't think there is a solution for the first issue, but workarounds could be implemented for the second and third ones. I preferred to keep things simple here, so I avoided doing that. (Exceeding the maximum recursion depth in appendToProto shouldn't be a problem.)

I have two questions:

  1. Is throwing an error in appendToProto the right way to enforce the assumption from the third issue?
  2. Are there any other caveats that I haven't considered?
\$\endgroup\$
1
\$\begingroup\$

I'll admit to being new to this concept, and can't think of a real-world application for this method. Regardless, here's my thoughts.

Issues #1 and #2 are directly results of your use of Object.Assign which seems an acceptable limitation. Issue #3 is stating a fact of how Object prototypes are set up. The prototype of Object.prototype is by definition null, so #3 is technically a given.

However, your questions make your meaning of #3 more clear.

Is throwing an error in appendToProto the right way to enforce the assumption from the third issue?

I wouldn't think so, as it limits the usefulness of your function. I would simply skip null values:

if (obj === null || obj === Object.prototype)
    return proto;

This has the benefit of treating null input essentially as if it were not provided, which would seem a more expected result. You could still have an error thrown, but I don't see the value.

If you choose to have an error, at least make the issue more obvious. "Prototype chains are assumed to be delimited by Object.prototype." is confusing and unclear what the issue is to me. I would go for something like: "Cannot appendToProto of null."

In general the error message should indicate what is wrong with the input instead of the codes philosophy regarding what its input should be.

\$\endgroup\$
  • \$\begingroup\$ Thank you for your answer! Objects prototypes are not necessarily set up as assumed in issue #3. One can create an object without Object.prototype in its prototype chain by doing this: Object.create(null). I post a different answer with further thoughts as it would be too long to post as a comment. \$\endgroup\$ – Agost Biro Jul 14 '15 at 9:32
  • \$\begingroup\$ I never considered that anyone would do that. I suppose it could have a use if you wanted a truly empty object with no prototype functionality. \$\endgroup\$ – Daniel Jul 14 '15 at 12:44
1
\$\begingroup\$

Issues #1-2

I missed that ES6 will bring a Object.setPrototypeOf method that can be used to mutate the prototype chains of objects. While this comes with performance penalties, it can be used to overcome issues #1-2.

Issue #3

I initially thought that if an object without Object.prototype (O.p) in its prototype chain (PC) was provided as obj, it would alter the expected behaviour of appendToProto. That's why I decided to throw an error if the function encountered null while traversing the PC of obj before encountering O.p. On a second look, however, this does not seem to be a problem at all:

If the PC of obj is delimited by null and the PC of proto is delimited by O.p, then the resulting PC will be delimited by O.p which is the expected behaviour. But if it's the other way around, then there is a problem, because O.p will be missing from the resulting PC, which is not to be expected and it might break objects down the chain. It is in this case that an error should be thrown. (If both obj and proto are delimited by O.p or by null then there is no problem). So the right way to go about this would be to check first the limits of PCs of obj and proto, throw an error if there is a problem and only then dispatch the append procedure and use the base case that was suggested by Daniel Cook:

// Non-enumerable properties are omitted. Property attributes are not respected
// and getters and setters aren't copied. Use 'Object.setPrototypeOf' if these
// are important.
function appendToProto(obj, proto)
{
  if (Object.prototype.isPrototypeOf(obj) && 
      !Object.prototype.isPrototypeOf(proto))
    throw new Error(
      'Object.prototype will be missing from the resulting proto chain.'
    );

  return (function inner(obj, proto)
  {
    if (obj === Object.prototype || obj === null)
      return proto;
    else
      return assign(
        Object.create(
          inner(Object.getPrototypeOf(obj), proto)
        ),
        obj
      );
  })(obj, proto);
}
\$\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.