Question

I've spent about 12 hours looking through this code, and fiddling with it, trying to find out where there's a recursion problem because I'm getting the, "maximum call stack size exceeded," error, and haven't found it. Someone smarter than me please help me!

so far, all I found was that when I make the object, spot, a circle, object, the problem disappears, but when I make it a, 'pip', I get this stack overflow error. I've gone over the pip class with a friggin' microscope, and still have no idea why this is happening!

var canvas = document.getElementById('myCanvas');

//-------------------------------------------------------------------------------------
// Classes
//-------------------------------------------------------------------------------------
//=====================================================================================
//CLASS - point
function point(x,y){
    this.x = x;
    this.y = y;
}
//=====================================================================================
// CLASS - drawableItem
function drawableItem() {
    var size = 0;
    this.center = new point(0,0);
    this.lineWidth = 1;
    this.dependentDrawableItems = new Array();
}
//returns the size
drawableItem.prototype.getSize = function getSize(){
    return this.size;
}
// changes the size of this item and the relative size of all dependents
drawableItem.prototype.changeSize = function(newSize){
    var relativeItemSizes = new Array;
    relativeItemSizes.length = this.dependentDrawableItems.length;
    // get the relative size of all dependent items
    for (var i = 0; i < this.dependentDrawableItems.length; i++){
        relativeItemSizes[i] = this.dependentDrawableItems[i].getSize() / this.size;
    }
    // change the size
    this.size = newSize;
    // apply the ratio of change back to all dependent items
    for (var i = 0; i < relativeItemSizes.length; i++){
        this.dependentDrawableItems[i].changeSize(relativeItemSizes[i] * newSize);
    }
}
//moves all the vertices and every dependent to an absolute point based on center
drawableItem.prototype.moveTo = function(moveX,moveY){
    //record relative coordinates
    var relativeItems = new Array;
    relativeItems.length = this.dependentDrawableItems.length;
    for (var i = 0; i < relativeItems.length; i++){
        relativeItems[i] = new point;
        relativeItems[i].x = this.dependentDrawableItems[i].center.x - this.center.x;
        relativeItems[i].y = this.dependentDrawableItems[i].center.y - this.center.y;
    }
    //move the center
    this.center.x = moveX;
    this.center.y = moveY;
    //move all the items relative to the center
    for (var i = 0; i < relativeItems.length; i++){
        this.dependentDrawableItems[i].moveItemTo(this.center.x + relativeItems[i].x,
            this.center.y + relativeItems[i].y);
    }
}
// draws every object in dependentDrawableItems
drawableItem.prototype.draw = function(ctx){
    for (var i = 0; i < this.dependentDrawableItems.length; i++) {
        this.dependentDrawableItems[i].draw(ctx);
    }
}

//=====================================================================================
//CLASS - circle
function circle(isFilledCircle){
    drawableItem.call(this);
    this.isFilled = isFilledCircle
}
circle.prototype = new drawableItem();
circle.prototype.parent = drawableItem.prototype;
circle.prototype.constructor = circle;
circle.prototype.draw = function(ctx){
    ctx.moveTo(this.center.x,this.center.y);
    ctx.beginPath();
    ctx.arc(this.center.x, this.center.y, this.size, 0, 2*Math.PI);
    ctx.closePath();
    ctx.lineWidth = this.lineWidth;
    ctx.strokeStyle = this.outlineColor;
    if (this.isFilled === true){
        ctx.fill();
    }else {
        ctx.stroke();
    }
    this.parent.draw.call(this,ctx);
}

//=====================================================================================
//CLASS - pip
function pip(size){
    circle.call(this,true);
}
pip.prototype = new circle(false);
pip.prototype.parent = circle.prototype;
pip.prototype.constructor = pip;

//----------------------------------------------------------------------
// Objects/variables - top layer is last (except drawable area is first)
//----------------------------------------------------------------------
var drawableArea = new drawableItem();

var spot = new pip();
spot.changeSize(20);
drawableArea.dependentDrawableItems[drawableArea.dependentDrawableItems.length] = spot;

//------------------------------------------
// Draw loop
//------------------------------------------
function drawScreen() {
    var context = canvas.getContext('2d');
    context.canvas.width  = window.innerWidth;
    context.canvas.height = window.innerHeight;

    spot.moveTo(context.canvas.width/2, context.canvas.height/2);

    drawableArea.draw(context);
}

window.addEventListener('resize', drawScreen);

Here's the demo: http://jsfiddle.net/DSU8w/

Was it helpful?

Solution

this.parent.draw.call(this,ctx);

is your problem. On a pip object, the parent will be circle.prototype. So when you now call spot.draw(), it will call spot.parent.draw.call(spot), where this.parent is still the circle.prototype

You will need to explicitly invoke drawableItem.prototype.draw.call(this) from circle.prototype.draw. Btw, you should not use new for the prototype chain.

OTHER TIPS

Why would you write code like that? It's so difficult to understand and debug. When I'm creating lots of classes I usually use augment to structure my code. This is how I would rewrite your code:

var Point = Object.augment(function () {
    this.constructor = function (x, y) {
        this.x = x;
        this.y = y;
    };
});

Using augment you can create classes cleanly. For example your drawableItem class could be restructured as follows:

var DrawableItem = Object.augment(function () {
    this.constructor = function () {
        this.size = 0;
        this.lineWidth = 1;
        this.dependencies = [];
        this.center = new Point(0, 0);
    };

    this.changeSize = function (toSize) {
        var fromSize = this.size;
        var ratio = toSize / fromSize;
        this.size = toSize;

        var dependencies = this.dependencies;
        var length = dependencies.length;
        var index = 0;

        while (index < length) {
            var dependency = dependencies[index++];
            dependency.changeSize(dependency.size * ratio);
        }
    };

    this.moveTo = function (x, y) {
        var center = this.center;
        var dx = x - center.x;
        var dy = y - center.y;
        center.x = x;
        center.y = y;

        var dependencies = this.dependencies;
        var length = dependencies.length;
        var index = 0;

        while (index < length) {
            var dependency = dependencies[index++];
            var center = dependency.center;

            dependency.moveTo(center.x + dx, center.y + dy);
        }
    };

    this.draw = function (context) {
        var dependencies = this.dependencies;
        var length = dependencies.length;
        var index = 0;

        while (index < length) dependencies[index++].draw(context);
    };
});

Inheritance is also very simple. For example you can restructure your circle and pip classes as follows:

var Circle = DrawableItem.augment(function (base) {
    this.constructor = function (filled) {
        base.constructor.call(this);
        this.filled = filled;
    };

    this.draw = function (context) {
        var center = this.center;
        var x = center.x;
        var y = center.y;

        context.moveTo(x, y);

        context.beginPath();
        context.arc(x, y, this.size, 0, 2 * Math.PI);
        context.closePath();

        context.lineWidth = this.lineWidth;
        context[this.filled ? "fill" : "stroke"]();
        base.draw.call(this, context);
    };
});

var Pip = Circle.augment(function (base) {
    this.constructor = function () {
        base.constructor.call(this, true);
    };
});

Now that you've created all your classes you can finally get down to the drawing:

window.addEventListener("DOMContentLoaded", function () {
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");
    var drawableArea = new DrawableItem;
    var spot = new Pip;

    spot.changeSize(20);
    drawableArea.dependencies.push(spot);
    window.addEventListener("resize", drawScreen, false);

    drawScreen();

    function drawScreen() {
        var width = canvas.width = window.innerWidth;
        var height = canvas.height = window.innerHeight;
        spot.moveTo(width / 2, height / 2);
        drawableArea.draw(context);
    }
}, false);

We're done. See the demo for yourself: http://jsfiddle.net/b5vNk/

Not only have we made your code more readable, understandable and maintainable but we have also solved your recursion problem.

As Bergi mentioned the problem was with the statement this.parent.draw.call(this,ctx) in the circle.prototype.draw function. Since spot.parent is circle.prototype the this.parent.draw.call(this,ctx) statement is equivalent to circle.prototype.draw.call(this,ctx). As you can see the circle.prototype.draw function now calls itself recursively until it exceeds the maximum recursion depth and throws an error.

The augment library solves this problem elegantly. Instead of having to create a parent property on every prototype when you augment a class augment provides you the prototype of that class as a argument (we call it base):

var DerivedClass = BaseClass.augment(function (base) {
    console.log(base === BaseClass.prototype); // true
});

The base argument should be treated as a constant. Because it's a constant base.draw.call(this, context) in the Circle class above will always be equivalent to DrawableItem.prototype.draw.call(this, context). Hence you will never have unwanted recursion. Unlike this.parent the base argument will alway point to the correct prototype.

Bergi's answer is correct, if you don't want to hard code the parent name multiple times you could use a helper function to set up inheritance:

function inherits(Child,Parent){
  Child.prototype=Object.create(Parent.prototype);
  Child.parent=Parent.prototype;
  Child.prototype.constructor=Child;
};
function DrawableItem() {
  this.name="DrawableItem";
}
DrawableItem.prototype.changeSize = function(newSize){
  console.log("changeSize from DrawableItem");
  console.log("invoking object is:",this.name);
}
function Circle(isFilledCircle){
    Circle.parent.constructor.call(this);
    this.name="Circle";//override name
}
inherits(Circle,DrawableItem);
Circle.prototype.changeSize = function(newSize){
  Circle.parent.changeSize.call(this);
  console.log("and some more from circle");
};
function Pip(size){
    Pip.parent.constructor.call(this,true);
    this.name="Pip";
}
inherits(Pip,Circle);

var spot = new Pip();
spot.changeSize();

For a polyfill on Object.create look here.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top