Some options for you, all using Object.create
:
Option 1:
function User(first, last){
var rv;
if ( !(this instanceof User) ) {
// They called us without `new`: Create an object backed by `User.prototype`:
rv = Object.create(User.prototype);
// Now, call this function applying the arguments
User.apply(rv, arguments);
// Return the object
return rv;
}
// Normal constructor stuff
this.name = first + " " + last;
}
Of course, all of that logic doesn't have to be repeated for every constructor function you create, you can use a helper function:
function constructWith(obj, ctor, args) {
if (obj instanceof ctor) {
return null;
}
obj = Object.create(ctor.prototype);
ctor.apply(obj, args);
return obj;
}
then
function User(first, last){
var rv;
if ((rv = constructWith(this, User, arguments)) != null) {
return rv;
}
// Normal constructor stuff
this.name = first + " " + last;
}
Option 2: Don't use this
much:
function User(first, last){
var rv;
if (this instanceof User) {
// They (probably) used `new`, all is good, use `this`
rv = this;
} else {
// They didn't use `new`, create an object backed by `User.prototype`
rv = Object.create(User.prototype);
}
// ...use `rv`, not `this`, from here on
rv.name = first + " " + last;
// This is important for the case where they didn't use `new`, and harmless
// in the case where they did.
return rv;
}
As you can see, this is a lot simpler, but if you really like your syntax highlighting (seriously, I have a client to whom it really matters that this
jumps out), etc...
And of course, you can wrap that up in a helper:
function useOrConstruct(obj, ctor) {
return obj instanceof ctor ? obj : Object.create(ctor.prototype);
}
Then
function User(first, last){
var rv = useOrConstruct(this, User);
// ...use `rv`, not `this`, from here on
rv.name = first + " " + last;
// This is important for the case where they didn't use `new`, and harmless
// in the case where they did.
return rv;
}
Option 3: constructOMatic
Of course, if we're going to define helpers, maybe we should go whole-hog:
function User() {
return constructOMatic(this, User, arguments, function(first, last) {
this.name = first + " " + last;
});
}
...where constructOMatic
is:
function constructOMatic(obj, ctor, args, callback) {
var rv;
if (!(obj instanceof ctor)) {
obj = Object.create(ctor.prototype);
}
rv = callback.apply(obj, args);
return rv !== null && typeof rv === "object" ? rv : obj;
}
Now, you can use this
to your heart's content within the callback. That fiddling with rv
vs. obj
in the return
at the end is to emulate the behavior of new
(the result of a new
expression is the object created by the new
operator unless the constructor function returns a non-null
object reference, in which case that takes precedence).
Object.create
is an ES5 feature found on all modern browsers, but the single-argument version of it used above can be shimmed for out-of-date browsers:
if (!Object.create) {
Object.create = function(proto, props) {
if (typeof props !== "undefined") {
throw "The two-argument version of Object.create cannot be shimmed.";
}
function ctor() { }
ctor.prototype = proto;
return new ctor; // Yes, you really don't need () (but put them on if you prefer)
};
}