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.

+3


source to share


4 answers


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 })

      

+5


source


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 ...

0


source


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 ...

http://jsfiddle.net/s4w2pn5a/

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);

      

0


source


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

.

Learn more about typing duck from JavaScript Drops.

0


source







All Articles