dynamic object construction in javascript?

11,738

Solution 1

There's no simple, straightforward way to do this with a constructor function. This is because special things happen when you use the new keyword to call a constructor function, and so if you're not going to do that, you have to emulate all of those special things. They are:

  1. Creating a new object instance (you're doing that).
  2. Setting that object's internal prototype to constructor function's prototype property.
  3. Setting that object's constructor property.
  4. Calling the constructor function with that object instance as the this value (you're doing that).
  5. Handling the special return value from the constructor function.

I think that's about it, but worth double-checking in the spec.

So if you can avoid it and just use the constructor function directly, I'd do that. :-) If you can't, though, you can still do it, it's just awkward and involves workarounds. (See also this related answer here on StackOverflow, although I cover all of the ground here [and then some] as well.)

Your biggest issue is #2 above: Setting the internal prototype of the object. For a long time, there was no standard way to do this. Some browsers supported a __proto__ property that did it, so you can use that if it's there. The good news is that ECMAScript 5 introduces a way to do this explicitly: Object.create. So cutting-edge browsers like Chrome will have that. But if you're dealing with a browser that has neither Object.create nor __proto__, it gets a bit ugly:

1) Define a custom constructor function.

2) Set its prototype property to the prototype property of the real constructor function

3) Use it to create a blank object instance.

That handles the prototype for you. Then you continue with:

4) Replace the constructor property on that instance with the real constructor function.

5) Call the real constructor function via apply.

6) If the return value of the real constructor function is an object, use it instead of the one you created; otherwise, use the one you created.

Something like this (live example):

function applyConstruct(ctor, params) {
    var obj, newobj;

    // Use a fake constructor function with the target constructor's
    // `prototype` property to create the object with the right prototype
    function fakeCtor() {
    }
    fakeCtor.prototype = ctor.prototype;
    obj = new fakeCtor();

    // Set the object's `constructor`
    obj.constructor = ctor;

    // Call the constructor function
    newobj = ctor.apply(obj, params);

    // Use the returned object if there is one.
    // Note that we handle the funky edge case of the `Function` constructor,
    // thanks to Mike's comment below. Double-checked the spec, that should be
    // the lot.
    if (newobj !== null
        && (typeof newobj === "object" || typeof newobj === "function")
       ) {
        obj = newobj;
    }

    // Done
    return obj;
}

You could take it a step further and only use the fake constructor if necessary, looking to see if Object.create or __proto__ are supported first, like this (live example):

function applyConstruct(ctor, params) {
    var obj, newobj;

    // Create the object with the desired prototype
    if (typeof Object.create === "function") {
        // ECMAScript 5 
        obj = Object.create(ctor.prototype);
    }
    else if ({}.__proto__) {
        // Non-standard __proto__, supported by some browsers
        obj = {};
        obj.__proto__ = ctor.prototype;
        if (obj.__proto__ !== ctor.prototype) {
            // Setting it didn't work
            obj = makeObjectWithFakeCtor();
        }
    }
    else {
        // Fallback
        obj = makeObjectWithFakeCtor();
    }

    // Set the object's constructor
    obj.constructor = ctor;

    // Apply the constructor function
    newobj = ctor.apply(obj, params);

    // If a constructor function returns an object, that
    // becomes the return value of `new`, so we handle
    // that here.
    if (typeof newobj === "object") {
        obj = newobj;
    }

    // Done!
    return obj;

    // Subroutine for building objects with specific prototypes
    function makeObjectWithFakeCtor() {
        function fakeCtor() {
        }
        fakeCtor.prototype = ctor.prototype;
        return new fakeCtor();
    }
}

On Chrome 6, the above uses Object.create; on Firefox 3.6 and Opera, it uses __proto__. On IE8, it uses the fake constructor function.

The above is fairly off-the-cuff, but it mostly handles the issues I'm aware of in this area.

Solution 2

From developer.mozilla: Bound functions are automatically suitable for use with the new operator to construct new instances created by the target function. When a bound function is used to construct a value, the provided this is ignored. However, provided arguments are still prepended to the constructor call.

That said, we still need to use apply to get the arguments out of an array and into the bind call. Further, we need to also reset bind's function as the this argument of the apply function. This gives us a very succinct one-liner that does exactly as needed.

function constructorApply(ctor, args){
    return new (ctor.bind.apply(ctor, [null].concat(args)))();
};
Share:
11,738
einarmagnus
Author by

einarmagnus

Updated on July 06, 2022

Comments

  • einarmagnus
    einarmagnus almost 2 years

    When I want to call a function in javascript with arguments supplied from elsewhere I can use the apply method of the function like:

    array = ["arg1", 5, "arg3"] 
    ...
    someFunc.apply(null, array);
    

    but what if I need to call a constructor in a similar fashion? This does not seem to work:

    array = ["arg1", 5, "arg3"] 
    ...
    someConstructor.apply({}, array);
    

    at least not as I am attempting:

    template = ['string1', string2, 'etc'];
    var resultTpl = Ext.XTemplate.apply({}, template);
    

    this does not work wither:

    Ext.XTemplate.prototype.constructor.apply({}, template);
    

    Any way to make that one work? (In this particular case I found that new Ext.XTemplate(template) will work, but I am interested in the general case)

    similar question but specific to built-in types and without an answer I can use: Instantiating a JavaScript object by calling prototype.constructor.apply

    Thank you.

    Edit:

    Time has passed and ES6 and transpilers are now a thing. In ES6 it is trivial to do what I wanted: new someConstructor(...array). Babel will turn that into ES5 new (Function.prototype.bind.apply(someConstructor, [null].concat(array)))(); which is explained in How to construct JavaScript object (using 'apply')?.

  • T.J. Crowder
    T.J. Crowder over 13 years
    In fact, it does -- in his question, he said "(In this particular case I found that new Ext.XTemplate(template) will work, but I am interested in the general case)"
  • philgiese
    philgiese over 13 years
    I thought by general he meant something like I described. Sorry if I have misunderstood the question.
  • einarmagnus
    einarmagnus over 13 years
    That sure was a bit more involved than I expected. Thank you though, for an excellent explanation!
  • T.J. Crowder
    T.J. Crowder over 13 years
    @ormuriauga: :-) Glad that helped. The explanation makes it seem a bit more complicated than it is (as explanations tend to do). As you can see from the first code snippet, it can actually be quite a brief bit of code.
  • einarmagnus
    einarmagnus over 13 years
    even one-liners are often quite involved ;) the finer details of the javascript Object model still evade me, but I bought "the good parts" now and will read it soon. Is there any good reason to use your latter code snippet when the shorter, and less complicated one, works? Would I gain any speed or compatibility or something else by using the newer features?
  • T.J. Crowder
    T.J. Crowder over 13 years
    @ormuriauga: Funny, I was thinking about that too. I'm not sure you do, frankly, I'm not immediately seeing much benefit to detecting and using Object.create or __proto__ when there's a universal and straightforward way to do it instead. I expect they're slightly faster, but you'd only need to worry about speed if you were doing it a lot. Re JavaScript's object model, the disarming thing about it is how incredibly simple it is. We have objects, and an object can be backed by a prototype object, which can in turn be backed by another prototype object, ad infinitum. (cont'd)
  • T.J. Crowder
    T.J. Crowder over 13 years
    (continuing) There are no packages or classes (though we frequently call constructor functions classes). There are no methods, only functions (though when they're hooked to properties of objects we frequently call them methods). The language considers almost everything to be an object (including the context created when you call a function, which is how closures work). Etc. If you're interested, I've written up some disjointed thoughts on JavaScript over the years (including about classes, methods, this and how it differs from other languages, closures): blog.niftysnippets.org
  • Mike Samuel
    Mike Samuel over 13 years
    I don't think this works for the Function constructor because the typeof check there returns "function", and it fails for constructors that return null since typeof null is "object".
  • ErikE
    ErikE over 11 years
    Thank you for this. I made a version where the first call returns a function with a closure on the to-be-created object, which in turn takes normal function arguments rather than an array, using arguments with apply. Example: var newMyObject = objectCreator(MyObject); var objinst = newMyObject(arg1, arg2, arg3);. Of course, you can just chain them: objectCreator(MyObject)(arg1, arg2, arg3). This pattern is now supremely useful for letting mixed VBScript/Javascript in an ASP Classic web page create new JS objects from VBScript without a special constructor function for each object type.
  • ErikE
    ErikE over 11 years
    (continued) In VB you can then use exactly the same syntax, objectCreator(MyObject)(arg1, arg2, arg3) to simulate new MyObject(arg1, arg2, arg3).
  • Mike Samuel
    Mike Samuel over 11 years
    In ES5, Function.prototype.bind lets you separate the concerns of currying and constructing. See stackoverflow.com/a/4226801/20394
  • drkstr101
    drkstr101 over 7 years
    Also from the relevant section in the linked source: "Warning: This section demonstrates JavaScript capabilities and documents some edge cases of the bind() method. The methods shown below are not the best way to do things and probably should not be used in any production environment."
  • herringtown
    herringtown over 6 years
    If you wanted to make this args parameter agnostic between an actual array of arguments or an Arguments object, you could do something like ...... // ... function constructorApply(ctor, args){ return new (ctor.bind.apply(ctor, [null].concat([].slice.call(args))))(); };