0

Take a look at this implementation of the state pattern in JavaScript:

var ON = {
    switch: function() {
        Object.setPrototypeOf(this, OFF);
        this.state = "OFF";
    }
}

var OFF = {
    switch: function() {
        Object.setPrototypeOf(this, ON);
        this.state = "ON";
    }
}

var lightBulb = Object.create(OFF);
lightBulb.switch(); // lightBulb.state is "ON"
lightBulb.switch(); // lightBulb.state is "OFF"

Two advantages that I like here are that the context (lightBulb) doesn't have to keep a reference to the active state, and that it doesn't have to define a switch() method just to delegate that call to its active state's switch() method. In other words, less boilerplate code.

I'm new to JavaScript, and haven't done any serious development with it. I would like to know if there are any pitfalls I am missing? Particularly, I'm wondering about the fact that I'm changing the prototype of a constructed object. Is this a no-no in JavaScript land?

1

This would work, but it's a very unusual way of accomplishing this task, so I would avoid it unless it's meant to be part of a learning exercise in how prototypes work.

Just to make sure we're on the same page, the "typical way" I have in mind is this:

var Switch = function() {
    this.state = "ON";
};

Switch.prototype.switch = function() {
    if(this.state === "ON") {
        this.state = "OFF";
    } else {
        this.state = "ON";
    }
};

I would prefer this "traditional" version over your "circular prototypes" version because:

  • If I came across the circular prototype version "in the wild", I'd have to spend some time either working out what it was supposed to do, or convincing myself that this really does what it's supposed to do (as I did for a few minutes when I found this question), precisely because it's so unusual. The traditional one I could probably glance at once and immediately move on.

  • Changing prototype chains at runtime is generally frowned upon because it can result in extremely unintuitive behavior. In particular, mistakes in such code can be extremely hard to debug. It's a lot like self-modifying code in lower-level languages. Your example happens to be very simple, but if this was part of a real program, it would no doubt end up with a much more complex interface on each state and/or far more than two states to switch between. Imagine trying to debug code when the object you're watching in the debugger changes its type on every method call, and each of those types has different expectations about its internal state.

  • The circular prototypes version already has a little bit of code duplicated between the two objects. If this was a more complicated object, there would no doubt be much more duplication, which would make it a lot harder to add new features to this object without making a silly mistake somewhere (which we'd then have to debug!). And whenever two of the objects differed in some way, it'd be impossible to tell if that difference was intentional or not.

  • Although optimization and engine internals aren't my specialty, I'm fairly sure Javascript engines are designed to heavily optimize prototype lookups (since every single member access and method call involves a lookup), and it's much easier to do that when the prototype chains remain static. Besides, most code doesn't change the chains at runtime, so most optimizers probably assume that's the norm.


I also don't really understand your alleged advantages.

the context (lightBulb) doesn't have to keep a reference to the active state

Technically you're right, but in practice it just "keeps that reference" in the form of a prototype link to a closure which knows the state (but won't tell anyone what it is), which is a lot less direct and less flexible than simply holding the state as a member variable. I also don't see why you'd want a light switch to not know whether it's turned on or not; that seems like trying to decouple things which cannot and should not be decoupled.

it doesn't have to define a switch() method just to delegate that call to its active state's switch() method

You appear to be comparing your version to an implementation where lightSwitch.switch() would call ON.switch() or OFF.switch(). I would also criticize that implementation for its unnecessary delegation, but since it's very easy to write a traditional version that simply doesn't do that, it's not clear how this is an advantage.

In other words, less boilerplate code.

Our versions seem about equal in length/boilerplate.


Finally, here's a hypothetical scenario that might help to demonstrate some of the abstract points I was making above: Say you want to give your lightSwitch an isOn() method that returns whether or not it's on. How would you do that?

With the traditional version, the solution is a trivial one-liner with no obvious disadvantages. With the circular prototypes version, I can't think of any clean way of adding it (i.e., no code duplication, no direct prototype comparisons) without essentially reimplementing the whole class in a more traditional way.

  • 1
    Thanks for taking the time to answer. I was referring to the state pattern as it appears on wiki: en.wikipedia.org/wiki/State_pattern, which forgoes if/else checks and simply delegates calls to an active state interface. You've basically validated my concerns that messin' with the prototype chains, which is what I wanted to hear. You mentioned a point about optimizations. I ran the above code's switch() method 1/2 a billion times. It took ~85 seconds. I ran it using your version (if/else) and the wiki version, and it took ~3.5 seconds. Looks like chaning the prototype is slow – jrahhali Aug 10 '15 at 11:28
  • @jrahhali Ah, that explains a lot. The state pattern can still be useful in Javascript, but the particular implementation in that article is not how I'd do it in production code. And I'm a bit stunned the performance difference is that huge, but now I'm glad I mentioned the possibility. – Ixrec Aug 10 '15 at 11:32
  • At lower iterations, the time difference between the ratios is not as bad. Running the swich() call 10,000 times, it was ~3.5sec:~1.5sec – jrahhali Aug 10 '15 at 11:38
  • @jrahhali Interesting, it sounds like they even have different asymptotic complexities. That could make a good StackOverflow question. – Ixrec Aug 10 '15 at 11:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.