4

I'm looking for a way to construct arbitrary JavaScript objects based on (a) the name of the constructor, and (b) an array containing the arguments. I found this function (by Matthew Crumley ?) in an other thread on stackoverflow:

function construct(constructor, args) {
  function F() { return constructor.apply(this, args); }
  F.prototype = constructor.prototype;
  return new F();
}

This works well with constructors written in JavaScript, but it fails with a TypeError if I try construct(Date, [...]). I don't know yet if there are more native constructors that this function can't handle. My questions are then ...

  • Are there functions in more recent versions of JavaScript (ECMAScript 5) that will solve my problem?
  • If not, is there some way I can check the constructor in question to see if the above function can be used? (If it cannot, I may have to use eval("new "+cname+"("+arglist+")").)

/Jon

Jon K
  • 327
  • 2
  • 9
  • 3
    At least related, if not duplicate: http://stackoverflow.com/questions/3871731/dynamic-object-construction-in-javascript Either way, my answer there may help. – T.J. Crowder Nov 19 '10 at 15:52
  • @TJC: great answer there (+1). @user: TJ's answer in that question should help you get started. Since you specifically mentioned `Date`, you might want to check out Kris Kowal's ECMA 5th shim which overrides the `Date` constructor by properly accounting for the variable number of arguments it can accept. https://github.com/kriskowal/es5-shim/blob/master/es5-shim.js#L501 – Andy E Nov 19 '10 at 15:58

2 Answers2

4

In ES5, you can do it via bind.

function construct(constructor, args) {
  return new (constructor.bind.apply(constructor, [null].concat(args)));
}

which works because bind still uses the [[Construct]] abstract operator when the bound function appears to the right of new per http://es5.github.com/#x15.3.4.5.2 which says

15.3.4.5.2 [[Construct]]

When the [[Construct]] internal method of a function object, F that was created using the bind function is called with a list of arguments ExtraArgs, the following steps are taken:

  1. Let target be the value of F’s [[TargetFunction]] internal property.
  2. If target has no [[Construct]] internal method, a TypeError exception is thrown.
  3. Let boundArgs be the value of F’s [[BoundArgs]] internal property.
  4. Let args be a new list containing the same values as the list boundArgs in the same order followed by the same values as the list ExtraArgs in the same order.
  5. Return the result of calling the [[Construct]] internal method of target providing args as the arguments.

but most implementations of Function.prototype.bind that attempt to back-port the language feature onto ES3 implementations do not correctly handle bound functions used as a constructor, so if you're not sure your code is running on a real ES5 implementation then you have to fall back to the triangle of hackery:

function applyCtor(ctor, args) {
  // Triangle of hackery which handles host object constructors and intrinsics.
  // Warning: The goggles! They do nothing!
  switch (args.length) {
    case 0: return new ctor;
    case 1: return new ctor(args[0]);
    case 2: return new ctor(args[0], args[1]);
    case 3: return new ctor(args[0], args[1], args[2]);
    case 4: return new ctor(args[0], args[1], args[2], args[3]);
    case 5: return new ctor(args[0], args[1], args[2], args[3], args[4]);
    case 6: return new ctor(args[0], args[1], args[2], args[3], args[4], args[5]);
    case 7: return new ctor(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
    case 8: return new ctor(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]);
    case 9: return new ctor(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]);
    case 10: return new ctor(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]);
    case 11: return new ctor(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10]);
    case 12: return new ctor(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11]);
  }
  // End triangle of hackery

  // Create a throwaway subclass of ctor whose constructor does nothing.
  function TemporarySubclass() {}
  TemporarySubclass.prototype = ctor.prototype;
  var instance = new TemporarySubclass();
  instance.constructor = ctor;  // Patch constructor property
  // Run the constructor.  This assumes that [[Call]] internal method is the same as
  // [[Construct]].  It might work with some builtins/host objects where "new` would not.
  var returnValue = ctor.apply(instance, args);
  // If the constructor returned a non-primitive value, return it instead.
  switch (typeof returnValue) {
    case 'object':
      // If ctor is Array, it reaches here so we don't use broken Array subclass.
      // Ditto for Date.
      if (returnValue) { return returnValue; }
      break;
    case 'function':
      return returnValue;
  }
  // Return the constructed instance.
  return instance;
}
Mike Samuel
  • 118,113
  • 30
  • 216
  • 245
  • Thanks to all of you for very quick answers! I've only had the chance to try Mike Samuel's function yet. When I do d = applyCtor(Date, [2000, ...]), I get an object that claims to be a Date, and it has e.g. a getYear function, but when I try d.getYear(), Firefox says "Date.prototype.getYear called on incompatible Object". In Safari I just get a "Type error". – Jon K Nov 19 '10 at 19:55
  • I have now tried both of T.J. Crowder's applyConstruct functions, but when I do my Date tests, I get exactly the same results as with Mike's function (see my previous comment, above). :-( – Jon K Nov 20 '10 at 08:41
  • I think I fixed this with the triangle of hackery in some older code that I can't find anymore. The basic idea was `switch (args.length) { case 0: return new ctor; case 1: return new ctor(args[0]); case 2: return new ctor(args[0], args[1]); ... }` up to 12 with a default as above, which handles all host objects with constructors that I know of. – Mike Samuel Nov 20 '10 at 23:50
  • @Mike Samuel: A small correction: `Date` and other such objects are *built-in objects*. *Host objects* are those which are implementation-defined, such as DOM objects. – casablanca Nov 21 '10 at 00:56
  • @Casablanca: Where I said "intrinsic" in the code above, I meant to include built-in objects. Intrinsics include built-in objects and other constructs specified completely by the language definition like primitive value types. – Mike Samuel Nov 21 '10 at 07:56
  • @Mike Samuel: Thanks a lot! Your triangle-of-hackery version seems to work well. I just had to replace a few semicolons with colons (case 1; through case 12;) ... ;-) I'm not sure what you mean by "Warning: The goggles! They do nothing!". Could you explain? – Jon K Nov 21 '10 at 15:49
  • @Mike Samuel: If I understand your triangle-of-hackery version, the code below the triangle switch is just for cases when args.length > 12. Is that right? – Jon K Nov 22 '10 at 09:24
  • One subject that hasn't been touched yet, is "what's the best way to produce the constructor from the name of the constructor?". I see two ways: eval(ctorName), and window[ctorName]. Any comments? – Jon K Nov 22 '10 at 09:30
  • @JonK Yes, the triangle of hackery is only for the > 12 case. For "The goggles, they do nothing" see http://www.google.com/search?sourceid=chrome&ie=UTF-8&q=site:thedailywtf.com+%22the+goggles%22 – Mike Samuel Nov 24 '10 at 02:15
  • It seems odd to me that this is the only way to handle what I think should be a built-in language feature! – ErikE Oct 23 '12 at 00:00
  • @ErikE, Yeah. It's a hole in the language's reflection. See my edits for a way to do this in ES 5. – Mike Samuel Oct 23 '12 at 01:03
  • @MikeSamuel I ended up using a variation on [this code](http://stackoverflow.com/a/3871769/57611). – ErikE Oct 23 '12 at 01:22
1

I may end up with this simplified version of Mike's triangle-of-hackery:

function applyCtor2(ctor, args) {
  switch (args.length) {
    case 0: return new ctor();
    case 1: return new ctor(args[0]);
    case 2: return new ctor(args[0], args[1]);
    // add more cases if you like
  }
  var jsStr = "new ctor(args[0]";
  for (var i=1; i<ar.length; i++) jsStr += ",args[" + i + "]";
  jsStr += ")";
  return eval(jsStr);
}

I'm not using 'apply' here, but I don't miss it. ;-) Any comments?

Jon K
  • 327
  • 2
  • 9