Convert a Constructor function to a regular function that can be called without new
In javascript, how can you convert a "constructor function" to a regular function that can be called without the new
keyword?
The implementation
/**
Take a constructor and return a function that can be called without new
but have the same effect as calling the constructor with new.
Useful for passing constructors to _.map, or passing arbitrary
arguments to the constructor using fn.apply(null, arguments)
*/
function ctor_fn(ctor) {
return function() {
var object = Object.create(ctor.prototype);
ctor.apply(object, arguments);
return object;
}
}
Motivation
Suppose you have a constructor function Button
that takes two arguments: text
and callback
. You can create a new button object like this: new Button("Clickme", callback)
.
If you need to create a function that has the same effect as calling Button but without using the new
keyword, you might do something like:
function CreateButton(text, callback) {
return new Button(text, callback);
}
Easy. Now you can pass CreateButton
around as a function that takes arguments and returns an object. You can even call it in this way:
CreateButton.apply(null, ["click me", callback]);
But now, what if you want to do the same for 10 other constructors? How do we generalize this approach?
Javascript's functions have .call
and .apply
, but there's nothing for doing the equivalent of .apply
but with the effect of having the new
keyword in front of it.
Actually the solution is simple: all you need to do is call apply
and pass it as first argument some kind of an "empty" object, so to speak. However, in order to actually emulate the new
keyword properly, this empty object has to have the prototype of the constructor function; otherwise it's not the same as calling the function with the new
keyword.
Example
Open up the dev console of your browser and try the following:
First let's create some constructor function.
function Button(text, callback) {
var self = this;
self.text = text;
var noop = function() {};
self.callback = callback || noop;
self.as_element = function() {
var button = document.createElement("button");
button.innerText = self.text;
button.addEventListener("click", self.callback);
return button;
}
}
Nothing special there. Takes a label and a callback function, and provides a method to create a DOM button element based on that.
Now to test this works, create some instance and see if it behaves as expected:
> b1 = new Button("test1", function() { console.log("Test 1 has been clicked"); })
<- Button {text: "test1", callback: function, as_element: function}
> b1.as_element()
<- <button>test1</button>
> b1.as_element().click()
Test 1 has been clicked
<- undefined
Note that I'm doing this in the Chrome dev console. Lines starting with <-
indicate the value returned from the line entered above it.
Notice also how the Chrome console displays the button object (second line): it says Button { .... }
. This is Chrome telling you which function was used to construct this object.
OK, so it works as expected when using the new keyword. Now let's turn it into a function using our ctor_fn
above:
CreateButton = ctor_fn(Button);
See if it works:
> b2 = CreateButton("test2", function() { console.log("test2 clicked!! it works!!"); })
<- Button {text: "test2", callback: function, as_element: function}
> b2.as_element()
<- <button>test2</button>
> b2.as_element().click()
test2 clicked!! it works!!
<- undefined
Works exactly the same as if it was called with new
. Chrome even recognizes that it was constructed from the Button
function! Why? Because we used Object.create(ctor.prototype)
to create the object. Yea, we didn't use Button
to create the object, but that's its prototype, so it's the same thing.
If you ask for the constructor of the object, you will get this:
> b2.constructor.name
<- "Button"
How about apply
? Can we call CreateButton.apply
to create the object and expect to behave correctly?
> b3 = CreateButton.apply(null, ["test3", function() { console.log("Test3 clicked! Apply also works!"); }])
<- Button {text: "test3", callback: function, as_element: function}
> b3.as_element()
<- <button>test3</button>
> b3.as_element().click()
Test3 clicked! Apply also works!
<- undefined
Yep, it works!
So there you have it.
Written by Hasen el Judy
Related protips
1 Response
you could also add the following code into your constructor
if (!(this instanceof Button)) {
return new Button(text, callback);
}
this automatically creates a new instance if you call Button("test", ...)
without the new keyword