3

I'm looking for a sane solution to JavaScript's only-one-constructor problem. So let's say we have a class Point and we want to allow object creation from coordinates.

I'm gonna ignore type-checking in all 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 turns into a nightmare quickly. So what I want is a design pattern that decouples these two constructors (or splits the one into two, rather). Objective-C has a nice 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 like this a lot, so far. But now we have two different syntaxes.

var a = new Point(4, 2);
var b = Point.withPoint(a);

Alright that's easy enough, no? Just add Point.withCoordinates. But what about the constructor then? Hide it? I don't know. I guess this is where you come in.


And here's what I've 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 boilerplate
  • Descriptive syntax/API
  • Scales well
  • Functional
  • Easy to test

Cons:

  • Instances don't know whether they're initialized or not
  • Can't just add properties to a class (compare Number.MAX_SAFE_INTEGER)

Notice the type-checks in Point.withPoint. It allows duck-typed points like click events.

function onClick(event) {
  var position = Object.create(Point).withPoint(event);
}

Also notice the lack of zero-initialization in some sort of default ctor. Points are actually a really good example for why that's not always a good idea.

4 Answers 4

5

Just like on 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 dummy constructor is basically the "alloc" thing.

The same in a more modern way, 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)

Yet another (and perhaps the most idiomatic) option would be to make the constructor accept named arguments (via the "options" object):

p = new Point({ x:1, y:2 })
p = new Point({ point: someOtherPoint })
Sign up to request clarification or add additional context in comments.

1 Comment

Didn't draw the line between new and ObjC's alloc. But yeah, this is probably about as good as it gets. I'm still hoping for a miracle though.
0

Okay, maybe a silly way of doing this, but you could add a property to specify that the object is a "POINT". Then, check for this property on the constructor.

It's not ideal, but if it fits 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);

Comments

0

You could use the instanceof pattern, 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 Typing

A better option would be to use a duck typing pattern, where the Point constructor would always accept a duck typed Point

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 provides for easier to read constructor calling, and allows consumers of Point to pass in anything with x and y keys.

More information on duck typing from "A Drip of JavaScript".

Comments

0

Instantiation and configuration is not the responsibility of classes. You have to add a factory, builder, DI container etc... to do the job. I suggest you to read more about creational 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 use multiple setters as well:

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 argument counting, etc... belongs to a higher abstraction level.

Btw method overloading is part of other languages, for example java, so in there you could simply define 2 constructors with different type 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;
    }
}

Sadly this feature is not part of javascript...

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.