KnigaRead.com/
KnigaRead.com » Компьютеры и Интернет » Программирование » Марейн Хавербеке - Выразительный JavaScript

Марейн Хавербеке - Выразительный JavaScript

На нашем сайте KnigaRead.com Вы можете абсолютно бесплатно читать книгу онлайн Марейн Хавербеке, "Выразительный jаvascript" бесплатно, без регистрации.
Перейти на страницу:

  return vector.x >= 0 && vector.x < this.width &&

         vector.y >= 0 && vector.y < this.height;

};

Grid.prototype.get = function(vector) {

  return this.space[vector.x + this.width * vector.y];

};

Grid.prototype.set = function(vector, value) {

  this.space[vector.x + this.width * vector.y] = value;

};

Элементарный тест:

var grid = new Grid(5, 5);

console.log(grid.get(new Vector(1, 1)));

// → undefined

grid.set(new Vector(1, 1), "X");

console.log(grid.get(new Vector(1, 1)));

// → X

Программный интерфейс существ

Перед тем, как заняться конструктором мира World, нам надо определиться с объектами существ, населяющих его. Я упомянул, что мир будет спрашивать существ, какие они хотят произвести действия. Работать это будет так: у каждого объекта существа есть метод act, который при вызове возвращает действие action. Action – объект типа property, который называет тип действия, которое хочет совершить существо, к примеру “move”. Action может содержать дополнительную информацию – такую, как направление движения.

Существа ужасно близоруки и видят только непосредственно прилегающие к ним клетки. Но и это может пригодиться при выборе действий. При вызове метода act ему даётся объект view, который позволяет существу изучить прилегающую местность. Мы называем восемь соседних клеток их направлениями по компасу: “n” на север, “ne” на северо-восток, и т. п. Вот какой объект будет использоваться для преобразования из названий направлений в смещения по координатам:

var directions = {

  "n":  new Vector( 0, -1),

  "ne": new Vector( 1, -1),

  "e":  new Vector( 1,  0),

  "se": new Vector( 1,  1),

  "s":  new Vector( 0,  1),

  "sw": new Vector(-1,  1),

  "w":  new Vector(-1,  0),

  "nw": new Vector(-1, -1)

};

У объекта view есть метод look, который принимает направление и возвращает символ, к примеру "#", если там стена, или пробел, если там ничего нет. Объект также предоставляет удобные методы find и findAll. Оба принимают один из символов, представляющих вещи на карте, как аргумент. Первый возвращает направление, в котором этот предмет можно найти рядом с существом, или же null, если такого предмета рядом нет. Второй возвращает массив со всеми возможными направлениями, где найден такой предмет. Например, существо слева от стены (на западе) получит [“ne”, “e”, “se”] при вызове findAll с аргументом “#”.

Вот простое тупое существо, которое просто идёт, пока не врезается в препятствие, а затем отскакивает в случайном направлении.

function randomElement(array) {

  return array[Math.floor(Math.random() * array.length)];

}


function BouncingCritter() {

  this.direction = randomElement(Object.keys(directions));

};


BouncingCritter.prototype.act = function(view) {

  if (view.look(this.direction) != " ")

    this.direction = view.find(" ") || "s";

  return {type: "move", direction: this.direction};

};

Вспомогательная функция randomElement просто выбирает случайный элемент массива, используя Math.random и немного арифметики, чтобы получить случайный индекс. Мы и дальше будем использовать случайность, так как она – полезная штука в симуляциях.

Конструктор BouncingCritter вызывает Object.keys. Мы видели эту функцию в предыдущей главе – она возвращает массив со всеми именами свойств объекта. Тут она получает все имена направлений из объекта directions, заданного ранее.

Конструкция || "s" в методе act нужна, чтобы this.direction не получил null, в случае если существо забилось в угол без свободного пространства вокруг – например, окружено другими существами.

Мировой объект

Теперь можно приступать к мировому объекту World. Конструктор принимает план (массив строк, представляющих сетку мира) и объект legend. Это объект, сообщающий, что означает каждый из символов карты. В нём есть конструктор для каждого символа – кроме пробела, который ссылается на null (представляющий пустое пространство).

function elementFromChar(legend, ch) {

  if (ch == " ")

    return null;

  var element = new legend[ch]();

  element.originChar = ch;

  return element;

}


function World(map, legend) {

  var grid = new Grid(map[0].length, map.length);

  this.grid = grid;

  this.legend = legend;


  map.forEach(function(line, y) {

    for (var x = 0; x < line.length; x++)

      grid.set(new Vector(x, y),

               elementFromChar(legend, line[x]));

  });

}

В elementFromChar мы сначала создаём экземпляр нужного типа, находя конструктор символа и применяя к нему new. Потом добавляем свойство originChar, чтобы было просто выяснить, из какого символа элемент был создан изначально.

Нам понадобится это свойство originChar при изготовлении мирового метода toString. Метод строит карту в виде строки из текущего состояния мира, проходя двумерным циклом по клеткам сетки.

function charFromElement(element) {

  if (element == null)

    return " ";

  else

    return element.originChar;

}


World.prototype.toString = function() {

  var output = "";

  for (var y = 0; y < this.grid.height; y++) {

    for (var x = 0; x < this.grid.width; x++) {

      var element = this.grid.get(new Vector(x, y));

      output += charFromElement(element);

    }

    output += "n";

  }

  return output;

};

Стена wall – простой объект. Используется для занятия места и не имеет метода act.

function Wall() {}

Проверяя объект World, создав экземпляр с использованием плана, заданного в начале главы, и затем вызвав его метод toString, мы получим очень похожую на этот план строку.

var world = new World(plan, {"#": Wall, "o": BouncingCritter});

console.log(world.toString());

// → ############################

//   #      #    #      o      ##

//   #                          #

//   #          #####           #

//   ##         #   #    ##     #

//   ###           ##     #     #

//   #           ###      #     #

//   #   ####                   #

//   #   ##       o             #

//   # o  #         o       ### #

//   #    #                     #

//   ############################

this и его область видимости

В конструкторе World есть вызов forEach. Хочу отметить, что внутри функции, передаваемой в forEach, мы уже не находимся непосредственно в области видимости конструктора. Каждый вызов функции получает своё пространство имён, поэтому this внутри неё уже не ссылается на создаваемый объект, на который ссылается this снаружи функции. И вообще, если функция вызывается не как метод, this будет относиться к глобальному объекту.

Значит, мы не можем писать this.grid для доступа к сетке изнутри цикла. Вместо этого внешняя функция создаёт локальную переменную grid, через которую внутренняя функция получает доступ к сетке.

Это промах в дизайне JavaScript. К счастью, в следующей версии есть решение этой проблемы. А пока есть пути обхода. Обычно пишут var self = this и после этого работают с переменной self.

Другое решение – использовать метод bind, который позволяет привязаться к конкретному объекту this.

var test = {

  prop: 10,

  addPropTo: function(array) {

    return array.map(function(elt) {

      return this.prop + elt;

    }.bind(this));

  }

};

console.log(test.addPropTo([5]));

// → [15]

Функция, передаваемая в map – результат привязки вызова, и посему её this привязан к первому аргументу, переданному в bind, то есть переменной this внешней функции (в которой содержится объект test).

Большинство стандартных методов высшего порядка у массивов, таких как forEach и map, принимают необязательный второй аргумент, который тоже можно использовать для передачи this при вызовах итерационной функции. Вы могли бы написать предыдущий пример чуть проще:

var test = {

  prop: 10,

  addPropTo: function(array) {

    return array.map(function(elt) {

      return this.prop + elt;

    }, this); // ← без bind

  }

};

console.log(test.addPropTo([5]));

// → [15]

Это работает только с теми функциями высшего порядка, у которых есть такой контекстный параметр. Если нет – приходится использовать другие упомянутые подходы.

Перейти на страницу:
Прокомментировать
Подтвердите что вы не робот:*