Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaScript constructor patterns

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.

like image 537
superlukas Avatar asked Feb 03 '26 14:02

superlukas


1 Answers

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 })
like image 108
georg Avatar answered Feb 05 '26 04:02

georg



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!