Reputation: 1659
A nasty gotcha in javascript is forgetting to call new
on a function meant to be instantiated, leading to this
being bound to a different object (usually the global) instead of a fresh one. One workaround I read about is to check for it explicitly in the function-constructor using the following idiom:
function SomeConstructor(x, y, ...) {
// check if `this` is created with new
if ( !(this instanceof arguments.callee) )
return new SomeConstructor(x, y, ...);
// normal initialization code follows
Now new SomeConstructor(...)
and SomeConstructor(...)
are equivalent.
I'd like to simplify this by creating a wrapper function factory(fn)
that does the first two lines and then delegates to the wrapped function fn
. This would be used like:
SomeConstructor = factory(function (x, y, ...) {
// normal initialization code follows
})
My first attempt was:
function factory(fn) {
return function() {
if ( !(this instanceof arguments.callee) ) {
return new arguments.callee.apply(this, arguments);
}
fn.apply(this, arguments);
}
}
but it fails with "Function.prototype.apply called on incompatible [object Object]". The second attempt was:
function factory(fn) {
return function() {
if ( !(this instanceof arguments.callee) ) {
var tmp = new arguments.callee();
arguments.callee.apply(tmp, arguments);
return tmp;
}
fn.apply(this, arguments);
}
}
This sort of works but it may call the wrapped function twice: once with no arguments (to create a new instance) and once with the passed arguments for the actual initialization. Apparently this is fragile and inefficient but I can't figure out a way to do it with a single call. Is this possible ?
EDIT: Based on bobince's approach, here's a similar one that does the trick:
function factory(init) {
var run_init = true;
function constr() {
if ( !(this instanceof constr) ) {
run_init = false;
var tmp = new constr();
run_init = true;
init.apply(tmp, arguments);
return tmp;
}
if (run_init)
init.apply(this, arguments);
}
return constr;
}
As for whether this is something that should be encouraged or not, that's debatable. I come from a Python background and I think of new
as just noise (Java) or wart (Javascript), but I may be missing something.
Upvotes: 2
Views: 1280
Reputation: 433
Here is some broilerplate code I've come up with as a code-template for object factory in AngularjS. I've used a Car/CarFactory as an example to illustrate. Makes for simple implementation code in the controller.
<script>
angular.module('app', [])
.factory('CarFactory', function() {
/**
* BroilerPlate Object Instance Factory Definition / Example
*/
this.Car = function(color) {
// initialize instance properties
angular.extend(this, {
color : null,
numberOfDoors : null,
hasFancyRadio : null,
hasLeatherSeats : null
});
// generic setter (with optional default value)
this.set = function(key, value, defaultValue, allowUndefined) {
// by default,
if (typeof allowUndefined === 'undefined') {
// we don't allow setter to accept "undefined" as a value
allowUndefined = false;
}
// if we do not allow undefined values, and..
if (!allowUndefined) {
// if an undefined value was passed in
if (value === undefined) {
// and a default value was specified
if (defaultValue !== undefined) {
// use the specified default value
value = defaultValue;
} else {
// otherwise use the class.prototype.defaults value
value = this.defaults[key];
} // end if/else
} // end if
} // end if
// update
this[key] = value;
// return reference to this object (fluent)
return this;
}; // end this.set()
}; // end this.Car class definition
// instance properties default values
this.Car.prototype.defaults = {
color: 'yellow',
numberOfDoors: 2,
hasLeatherSeats: null
};
// instance factory method / constructor
this.Car.prototype.instance = function(params) {
return new
this.constructor()
.set('color', params.color)
.set('numberOfDoors', params.numberOfDoors)
.set('hasFancyRadio', params.hasFancyRadio)
.set('hasLeatherSeats', params.hasLeatherSeats)
;
};
return new this.Car();
}) // end Factory Definition
.controller('testCtrl', function($scope, CarFactory) {
window.testCtrl = $scope;
// first car, is red, uses class default for:
// numberOfDoors, and hasLeatherSeats
$scope.car1 = CarFactory
.instance({
color: 'red'
})
;
// second car, is blue, has 3 doors,
// uses class default for hasLeatherSeats
$scope.car2 = CarFactory
.instance({
color: 'blue',
numberOfDoors: 3
})
;
// third car, has 4 doors, uses class default for
// color and hasLeatherSeats
$scope.car3 = CarFactory
.instance({
numberOfDoors: 4
})
;
// sets an undefined variable for 'hasFancyRadio',
// explicitly defines "true" as default when value is undefined
$scope.hasFancyRadio = undefined;
$scope.car3.set('hasFancyRadio', $scope.hasFancyRadio, true);
// fourth car, purple, 4 doors,
// uses class default for hasLeatherSeats
$scope.car4 = CarFactory
.instance({
color: 'purple'
numberOfDoors: 4
});
// and then explicitly sets hasLeatherSeats to undefined
$scope.hasLeatherSeats = undefined;
$scope.car4.hasLeatherSeats.set('hasLeatherSeats', $scope.hasLeatherSeats, undefined, true);
// in console, type window.testCtrl to see the resulting objects
});
</script>
Upvotes: 0
Reputation: 19257
the only thing that worked for me involves dumbing down the implementation. it's ugly but works (both with and without operator new):
var new_ = function (cls)
{
var constructors = [
function ()
{
return new cls();
}
, function ($0)
{
return new cls($0);
}
, function ($0, $1)
{
return new cls($0, $1);
}
, function ($0, $1, $2)
{
return new cls($0, $1, $2);
}
, // up to a chosen limit
];
return function ()
{
return constructors[arguments.length].apply(
this
, arguments
);
}
}
edit to react to comments
I have way-below-average tolerance to repetitive code, and this code hurt to write, but the functions in constructors
need to be separate if arguments.length
is to mean something in the real constructor function. consider a variant with a single new
wrapper:
var new_ = function (cls)
{
// arbitrary limit
var constructor = function ($0, $1, $2)
{
return new cls($0, $1, $2);
};
return function ()
{
return constructor.apply(
this
, arguments
);
}
}
var gen = new_(function ()
{
print(
arguments.length
+ " "
+ Array.prototype.toSource.call(arguments)
);
});
gen("foo") // 3 ["foo", undefined, undefined]
gen("foo", "bar") // 3 ["foo", "bar", undefined]
gen("foo", "bar", "baz") // 3 ["foo", "bar", "baz"]
the parameter list can be arbitrarily wide, but arguments.length
doesn't lie only in the special case.
I've been using this solution with the upper limit of 10 arguments for a few years, and I don't remember ever running into the limit. the risk that it'll ever happen is rather low: everybody knows that functions with many parameters are a no-no, and javascript has a better interface for the desired functionality: packing parameters into objects.
so, the only limit is the width of the parameter list, and this limit seems to be purely theoretical. other than that, it supports completely arbitrary constructors, so I'd say it's very general.
Upvotes: -1
Reputation: 9265
while 'new' is a good thing, and I don't endorse trying to do away with language features, check out this code I played with a while ago: (note, this is not a complete solution for you, but rather something to build into your code)
function proxy(obj)
{
var usingNew = true;
var obj_proxy = function()
{
if (usingNew)
this.constructor_new.apply(this, arguments);
};
obj_proxy.prototype = obj.prototype;
obj_proxy.prototype.constructor_new = obj.prototype.constructor;
obj_proxy.createInstance = function()
{
usingNew = false;
var instance = new obj_proxy();
instance.constructor_new.apply(instance, arguments);
usingNew = true;
return instance;
}
return obj_proxy;
}
to test it out, create a function foo like this:
function foo(a, b) { this.a = a; }
and test it:
var foo1 = proxy(foo);
var test1 = new foo1(1);
alert(test1 instanceof foo);
var test2 = foo1.createInstance(2);
alert(test2 instanceof foo);
EDIT: removed some code not relevant for this.
Upvotes: 1
Reputation: 1
If you are interested in dealing with the inability to use apply with new, one could use
Function.prototype.New = (function () {
var fs = [];
return function () {
var f = fs [arguments.length];
if (f) {
return f.apply (this, arguments);
}
var argStrs = [];
for (var i = 0; i < arguments.length; ++i) {
argStrs.push ("a[" + i + "]");
}
f = new Function ("var a=arguments;return new this(" + argStrs.join () + ");");
if (arguments.length < 100) {
fs [arguments.length] = f;
}
return f.apply (this, arguments);
};
}) ();
Example:
var foo = Foo.New.apply (null, argArray);
Upvotes: 0
Reputation: 536715
You can pass a unique value into the constructor for the first call (with new
) that signifies you don't want the initialiser called yet:
var _NOINIT= {};
function factory(init) {
function constr() {
if (!(this instanceof constr)) {
var inst= new constr(_NOINIT);
init.apply(inst, arguments);
return inst;
}
if (arguments[0]!==_NOINIT)
init.apply(this, arguments);
}
return constr;
}
Note I've used a named inline function for the constructor because arguments.callee
will be going away in ECMAScript Fifth Edition's ‘strict’ mode.
However if you're using a class factory, I suggest making the initialiser function a member of the class, rather than being passed in. That way, you can subclass a base class and have the subclass inherit the initialiser, which is normal behaviour in class-based languages. eg.:
Function.prototype.makeSubclass= function() {
function constr() {
var that= this;
if (!(this instanceof constr))
that= new constr(_NOINIT);
if (arguments[0]!==_NOINIT && '_init' in that)
that._init.apply(that, arguments);
return that;
}
if (this!==Object)
constr.prototype= new this(_NOINIT);
return constr;
};
var Shape= Object.makeSubclass();
Shape.prototype._init= function(x, y) {
this.x= x;
this.y= y;
};
var Point= Shape.makeSubclass();
// inherits initialiser(x, y), as no need for anything else in there
var Circle= Shape.makeSubclass()
Circle.prototype._init= function(x, y, r) {
Shape.prototype._init.call(this, x, y);
this.r= r;
};
Of course you don't have to put that into the Function prototype... it's a matter of taste, really. As is allowing constructors without new
.
Personally I prefer to throw an error rather than silently make it work, to try to discourage bare-constructor-calling, since this is a mistake elsewhere and may make the code slightly less clear.
Upvotes: 4
Reputation: 828002
Your "factory
" function could be written in this way:
function factory(fn, /* arg1, arg2, ..., argn */) {
var obj = new fn(), // Instantiate using 'new' to preserve the prototype chain
args = Array.prototype.slice.call(arguments, 1); // remove fn argument
fn.apply(obj, args); // apply the constructor again, with the right arguments
return obj;
}
// Test usage:
function SomeConstructor (foo, bar) {
this.foo = foo;
this.bar = bar;
}
SomeConstructor.prototype.test = true;
var o = factory(SomeConstructor, 'foo', 'bar');
// will return: Object foo=foo bar=bar test=true, and
o instanceof SomeConstructor; // true
However, the new
operator is not bad, I would not encourage you to avoid it, I would recommend you to stick with a proper naming convention, constructor functions identifiers in PascalCase, all other identifiers in camelCase, and also I highly recommend you to use JsLint it will help you to detect that kind of mistakes early.
Upvotes: 3
Reputation: 35404
This simply encourages a bad-habit shortcut that relies far too heavily on the implementation of the class to "fix" the calling code.
If this is a problem, don't just let it slide, throw an error message.
Upvotes: 9
Reputation: 1
I dislike your mixing of arguments.callee and the function's identifier. Also, you are dumbing down the original problem. You should have used apply to begin with so as not to make the helper (factory) function seem even better than it really is.
What should have been done to begin with:
function SomeConstructor(x, y, ...) {
// check if `this` is created with new
if ( !(this instanceof arguments.callee) )
return new arguments.callee.apply (this, arguments);
// normal initialization code follows
Another issue with factory
is that it defeats the function's length property.
Upvotes: 2