Reputation: 187
I've been been learning Javascript over the past few months, and sometimes get the feeling that I'm writing code faster than I actually grasp the nuances of the language, so please bear with me here.
Anyway, my question pertains to the behavior of a certain methodology I've been using. The basic gist is to be able to design objects that can be constructed and used without using the "new" keyword.
Here's an example:
<!DOCTYPE html>
<html>
<head>
<script>
function example()
{
if(!(this instanceof example))
return new example();
}
example.prototype.test = function()
{
var
result = example();
if(result instanceof example)
{
console.log("Type: example");
if(result === this)
console.log("Unexpected: (result === this)");
else
console.log("Expected: (result !== this)");
}
else
console.log("Type: ?");
return result;
}
function run()
{
var
one = new example(),
two = example(),
three = one.test(),
four = two.test();
three.test();
four.test();
}
</script>
</head>
<body onload = "run()">
</body>
</html>
The output I get is:
Type: example
Expected: (result !== this)
Type: example
Expected: (result !== this)
Type: example
Expected: (result !== this)
Type: example
Expected: (result !== this)
...which is exactly what I want to see. My question, specifically, is whether or not it's:
(1) standard behavior
(2) supported by all browsers
(3) prone to any undesirable side-effects
Upvotes: 3
Views: 113
Reputation: 21926
Your pattern
function Foo() {
if (!(this instanceof Foo)) {
return new Foo();
}
}
is pretty common and well known (yes its kosher). But it has some shortcomings, mostly revolving around dealing with passing arguments around. Also its unnecessary, JavaScript has higher order functions, so there's no need to add boilerplate to every single function you write:
let wrapConstructor = fn => (...args) => {
return new (fn.bind.apply(fn, [fn, ...args]));
};
let Foo = function Foo() {
if (!(this instanceof Foo)) {
throw new TypeError('Not a Foo...');
}
};
let wrappedFoo = wrapConstructor(Foo);
let foo = wrappedFoo(); // doesn't throw
This has the added benefit of working with built-in constructors like Date
:
let wrappedDate = wrapConstructor(Date);
let myDate = wrappedDate(2016, 11, 8); // today's date
In terms of browser compatibility, as written obviously its not great but any environment with Function.prototype.bind
and Function.prototype.apply
(IE 9+) can run this after a little babel magic.
As to whether or not its standard behavior, well, if you mean conforms to the spec than this certainly does. If you mean 'is this common practice', well, it depends on what JavaScript crowd you run with. If you tend to use a lot of higher-order functions (like my little wrapConstructor
utility) then new
is something of a pain and something to be avoided.
For a library that includes this sort of functionality, check out ramda.
Note also that you can combine the two patterns easily enough with a slight modification to the helper:
let construct = (fn, args) => {
return new (fn.bind.apply(fn, [fn, ...args]));
};
function Foo() {
if (!(this instanceof Foo)) {
return construct(Foo, Array.from(arguments));
}
}
now Foo can be called with or without new
, and the arguments to the function are automatically passed along without having to know how many there are ahead of time. This method will work with constructors of any arity, including variadic constructors.
Upvotes: 1
Reputation: 7351
It works, from a technical point of view. But you should be careful about changing expected semantics unless you have a compelling reason to, which in this instance (no pun intended), I'd argue you do not. Especially if your code is being consumed by other developers - best to stick with convention, like making the Constructor capitalized, and regular functions not.
Can't you just throw
an error if the function is not called with new
?
Upvotes: 0