Методы конструктора в javascript влияют на все экземпляры объекта

Я работаю над реализацией тетриса, чтобы научиться javascript, и столкнулся с проблемой. Во-первых, предыстория: у меня есть класс для каждого типа блока тетриса, который наследуется от базового шаблона блока с такими методами, как перемещение и возврат координат. Например,

function BlockTemplate(w, h, c){
    var height = h;
    var width = w;
    var x = 0;
    var y = 0;
    var color = c;

    this.getHeight = function(){return height;};
    this.getWidth = function(){return width;};
    this.getX = function(){return x;};
    this.getY = function(){return y;};
    this.getColor = function(){return color;};
    this.moveTo = function(newX, newY){
        x = newX;
        y = newY;
    };
    this.translate = function(dx, dy){
        x += dx;
        y += dy;
    };
}

function StraightBlock(){
    this.draw = function(){
        ctx.fillStyle = this.getColor();
        ctx.fillRect(this.getX(), this.getY(), 20, 100);
    };
}
StraightBlock.prototype = new BlockTemplate(20, 100, "#00FFE5");

Все блоки, находящиеся в данный момент на экране, хранятся в массиве blockArr, за исключением текущего падающего блока, который хранится в массиве curBlock.

Я использую функцию createRandomBlock(), чтобы создать блок и поместить его в curBlock:

var blockTypeArr = [LBlock, SquareBlock, StraightBlock, SquigBlock];

var createRandomBlock = function(){
    var block = blockTypeArr[Math.floor(Math.random()*blockTypeArr.length)];
    var randomBlock = new block();
    return randomBlock;
};

curBlock = createRandomBlock();

Как только он упал, я помещаю его в массив и создаю новый блок:

blockArr[blockArr.length] = curBlock;
curBlock = createRandomBlock();

Если вновь созданный блок еще не был на экране, то нет проблем. Однако использование методов moveTo и translate нового экземпляра влияет на все экземпляры этого класса (я добавил свойство id, чтобы убедиться, что они действительно разные экземпляры).

Например, используя консоль JavaScript,

>curBlock
  SquigBlock
>blockArr
  [SquareBlock, SquigBlock, SquigBlock]

Как вы можете видеть, 3 SquigBlock упали до сих пор (2 в blockArr, 1 в настоящее время падает). Тем не менее, единственный, который я вижу, это тот, который в настоящее время падает (curBlock), и проверка параметров curBlock.getY(), blockArr[1].getY() и blockArr[2].getY() возвращает одно и то же значение. Другими словами, все они рисуются в одном и том же месте.

Как я могу изменить его так, чтобы старые блоки, независимо от класса, оставались внизу экрана, а вновь созданный блок падал сверху, не заставляя все остальные блоки этого класса двигаться вместе с ним?

Спасибо!


person tytk    schedule 30.01.2014    source источник
comment


Ответы (2)


StraightBlock.prototype = new BlockTemplate(20, 100, "#00FFE5");

Что ж, это это только один BlockTemplate, который используется совместно с StraightBlock экземплярами. Вы можете видеть, что new StraightBlock().moveTo == new StraightBlock().moveTo, т.е. два экземпляра имеют один и тот же метод, который влияет на одну и ту же переменную x. Не используйте new для создания прототипа, а Правильное наследование JavaScript:

function StraightBlock(){
    BlockTemplate.call(this, 20, 100, "#00FFE5");
    this.draw = function(){
        ctx.fillStyle = this.getColor();
        ctx.fillRect(this.getX(), this.getY(), 20, 100);
    };
}
StraightBlock.prototype = Object.create(BlockTemplate.prototype);
person Bergi    schedule 30.01.2014
comment
Великолепно! Создает ли Object.create только копию прототипа BlockTemplate? - person tytk; 30.01.2014
comment
Именно, если при копировании ориентироваться на то, что он там конструктор не вызывает. Однако он не копирует свойства, а создает пустой объект, который наследуется от прототипа BlockTemplate. - person Bergi; 30.01.2014

StraightBlock.prototype = new BlockTemplate(20, 100, "#00FFE5");

Поскольку это вызывается один раз, существует один var x, который закрывается одним getHeight, который используется всеми экземплярами StraightBlock.

Вы должны внести два изменения:

1 Используйте поля экземпляра, хранящиеся на this

function BlockTemplate(w, h, c){
   this.height = h;
   ...
}

BlockTemplate.prototype.getHeight = function(){return this.height;};
...

2 конструктора цепей

Попросите StraightBlock вызвать свой конструктор суперкласса, чтобы добавить новые версии методов к каждому созданному экземпляру.

function StraightBlock(){
  BlockTemplate.apply(this, arguments);
  ...
}

Вы также можете просто выполнить цепочку конструкторов, но это будет означать, что вы создаете новую копию каждого метода для каждого экземпляра. Это может быть дорого, если у вас много недолговечных экземпляров "класса".

Недостаток хранения полей в this заключается в том, что они могут быть непреднамеренно изменены наивным кодом. JavaScript не имеет надежного сокрытия информации, кроме как с помощью закрытых локальных переменных, поэтому, если вам нужна надежность, которую вы получаете с помощью private полей в других языках, тогда ваш текущий дизайн с локальными переменными может быть лучшим.

person Mike Samuel    schedule 30.01.2014
comment
Первое изменение вряд ли необходимо и делает методы получения излишними. - person Bergi; 30.01.2014
comment
@Bergi, если вы используете что-то вроде Closure Compiler, то геттеры и сеттеры агрессивно встраиваются, и вы получаете некоторый уровень защиты от непреднамеренных мутаций. - person Mike Samuel; 30.01.2014
comment
Ааа, теперь я вижу, где моя логика подвела. Делает ли BlockTemplate.apply(this, args) то же самое, что и StraightBlock.prototype = Object.create(BlockTemplate.prototype)? Или есть разница? - person tytk; 30.01.2014
comment
Object.create(...) создает пустой объект без свойств. BlockTemplate.apply(this, args) использует объект, созданный вызовом new StraightBlock, и запускает тело BlockTemplate, чтобы установить для него свойства. - person Mike Samuel; 30.01.2014