JavaScript constructor templates
I'm looking for a sane solution to a problem with only one JavaScript constructor. So, let's say we have a class Point
and we want to allow the creation of an object from coordinates.
I will ignore type checking in all of these examples.
function Point(x, y) {
this.x = x;
this.y = y;
}
Easy. How about creating points from other points?
function Point(x, y) {
if (!y /* && x instanceof Point */) {
y = x.y;
x = x.x;
}
this.x = x;
this.y = y;
}
This quickly turns into a nightmare. So what I want is a design pattern that separates these two constructors (or separates one into two as well). Objective-C has a good pattern for this. ObjC people create objects with something.
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.withPoint = function(point) {
return new Point(point.x, point.y);
};
I really like it, so far. But now we have two different syntaxes.
var a = new Point(4, 2);
var b = Point.withPoint(a);
Okay, that's easy enough, no? Just add Point.withCoordinates
. But what about a constructor? Hide? I dont know. I guess this is where you come in.
And here's what I decided to go with:
var Point = {
withCoordinates: function(x, y) {
if (typeof x == 'number' && typeof y == 'number') {
this.x = x;
this.y = y;
return this;
}
throw TypeError('expected two numbers');
},
withPoint: function(point) {
if (typeof point.x == 'number' && typeof point.y == 'number') {
this.withCoordinates(point.x, point.y);
return this;
}
throw TypeError('expected a point');
}
};
var a = Object.create(Point).withCoordinates(0, 0);
var b = Object.create(Point).withPoint(a);
Pros:
- No template
- Descriptive syntax / API
- Scales well
- Functional
- Ease of testing
Minuses:
- Instances don't know if they are initialized or not.
- Can't add properties to class (compare
Number.MAX_SAFE_INTEGER
)
Note the type checks in Point.withPoint
. It allows you to type points like click events.
function onClick(event) {
var position = Object.create(Point).withPoint(event);
}
Also note the lack of zero initialization in some way by default ctor. Glasses are actually a really good example of why this isn't always a good idea.
source to share
As with ObjC, you can have separate "alloc" and "init" entries, for example:
function Point() {}
Point.prototype.withCoordinates = function(x, y) {
this.x = x;
this.y = y;
return this;
}
Point.prototype.withOffsetFromPoint = function(p, delta) {
this.x = p.x + delta;
this.y = p.y + delta;
return this;
}
p = new Point().withOffsetFromPoint(
new Point().withCoordinates(5, 6),
10);
console.log(p) // 15, 16
where the mock constructor is basically the "alloc" object.
The same is more modern, without new
:
Point = {
withCoordinates: function(x, y) {
this.x = x;
this.y = y;
return this;
},
withOffsetFromPoint: function(p, delta) {
this.x = p.x + delta;
this.y = p.y + delta;
return this;
}
}
p = Object.create(Point).withOffsetFromPoint(
Object.create(Point).withCoordinates(5, 6),
10);
console.log(p)
Another (and perhaps most idiomatic) option is for the constructor to accept named arguments (via the "options" object):
p = new Point({ x:1, y:2 })
p = new Point({ point: someOtherPoint })
source to share
Activation and customization are not the responsibility of the classes. You have to add factory, builder, DI container, etc. to make this work. I suggest you read more about creation design patterns .
For example:
var PointProvider = function (){};
PointProvider.prototype = {
fromCoords: function (x,y){
return new Point(x,y);
},
clonePoint: function (p){
return new Point(p.x, p.y);
}
};
var pointProvider = new PointProvider();
var p1 = pointProvider.fromCoords(x,y);
var p2 = pointProvider.fromPoint(p1);
You can also use multiple setters:
var Point = function (){
if (arguments.length)
this.setCoords.apply(this, arguments);
};
Point.prototype = {
setCoords: function (x,y){
this.x = x;
this.y = y;
return this;
},
setCoordsFromPoint: function (p){
this.x = p.x;
this.y = p.y;
return this;
}
};
var p1 = new Point(x,y);
var p2 = new Point().setCoordsFromPoint(p1);
or with a facade
var p = function (){
var point = new Point();
if (arguments.length == 2)
point.setCoords.apply(point, arguments);
else if (arguments.length == 1)
point.setCoordsFromPoint.apply(point, arguments);
return point;
}
var p1 = p(x,y);
var p2 = p(p1);
So, to summarize the counting of arguments, etc., it belongs to a higher level of abstraction.
The Btw method overload is part of other languages ββlike java, so there you can simply define 2 constructors with different types of arguments, for example:
class Point {
private int x;
private int y;
Point(int x, int y){
this.x = x;
this.y = y;
}
Point(Point p){
this.x = p.x;
this.y = p.y;
}
}
Unfortunately this function is not part of javascript ...
source to share
Ok, maybe a silly way to do this, but you can add a property to indicate that the object is "POINT". Then check this property on the constructor.
It's not ideal, but if it suits your needs ...
function Point(x, y) {
this.type = "POINT";
if (!y && x.type == "POINT") {
y = x.y;
x = x.x;
}
this.x = x;
this.y = y;
}
var p1 = new Point(10, 20);
var p2 = new Point(p1);
alert(p2.x);
source to share
You can use a template instanceof
, but instead move the initialization of the instance variables to another function.
function Point (point) {
if (point instanceof Point) {
this.init(point.x, point.y);
} else {
this.init.apply(this, arguments);
}
}
Point.prototype.init = function (x, y) {
this.x = x;
this.y = y;
};
Duck seal
The best option would be to use a duck pattern , where the designer Point
will always accept the duck printedPoint
function Point (point) {
this.init(point.x, point.y);
}
Point.prototype.init = function (x, y) {
this.x = x;
this.y = y;
};
var point = new Point({
x: 1,
y: 1
});
var point2 = new Point(point);
This makes the constructor invocation easier to read and allows users to Point
pass anything using the x
and keys y
.
source to share