Книга: JavaScript. Подробное руководство, 6-е издание

9.6.4. Методы сравнения

9.6.4. Методы сравнения

Операторы сравнения в языке JavaScript сравнивают объекты по ссылке, а не по значению. Так, если имеются две ссылки на объекты, то выясняется, ссылаются они на один и тот же объект или нет, но не выясняется, обладают ли разные объекты одинаковыми свойствами с одинаковыми значениями.[17] Часто бывает удобным иметь возможность сравнить объекты на равенство или определить порядок их следования (например, с помощью операторов отношения < и >). Если вы определяете новый класс и хотите иметь возможность сравнивать экземпляры этого класса, вам придется определить соответствующие методы, выполняющие сравнение.

В языке программирования Java сравнение объектов производится с помощью методов, и подобный подход можно с успехом использовать в JavaScript. Чтобы иметь возможность сравнивать экземпляры класса, можно определить метод экземпляра с именем equals(). Этот метод должен принимать единственный аргумент и возвращать true, если аргумент эквивалентен объекту, метод которого был вызван. Разумеется, вам решать, что следует понимать под словом «эквивалентен» в контексте вашего класса. Для простых классов часто достаточно просто сравнить свойства constructor, чтобы убедиться, что оба объекта имеют один и тот-же тип, и затем сравнивать свойства экземпляра двух объектов, чтобы убедиться, что они имеют одинаковые значения. Класс Complex из примера 9.3 как раз обладает таким методом equals(), и для нас не составит труда написать похожий метод для класса Range:

// Класс Range затирает свое свойство constructor. Поэтому восстановим его.
Range.prototype.constructor = Range;
// Объект Range не может быть равен никакому другому объекту, не являющемуся
// диапазоном значений. Два диапазона равны, только если равны значения их границ.
Range.prototype.equals = function(that) {
  if (that == null) return false; // Отвергнуть null и undefined
  if (that.constructor !== Range) return false; // Отвергнуть не диапазоны
  // Вернуть true, если значения границ равны.
  return this.from == that.from && this.to == that.to;
}

Задание метода equals() для нашего класса Set оказывается несколько сложнее. Мы не можем просто сравнить свойства values двух множеств - требуется выполнить глубокое сравнение:

Set.prototype.equals = function(that) {
  // Сокращенная проверка для тривиального случая
  if (this === that) return true;
  // Если объект that не является множеством, он не может быть равен объекту this.
  // Для поддержки подклассов класса Set используется оператор instanceof.
  // Мы могли бы реализовать более либеральную проверку, если бы для нас
  // было желательно использовать прием грубого определения типа.
  // Точно так же можно было бы ужесточить проверку, выполняя сравнение
  // this.constructor == that.constructor.
  // Обратите внимание, что оператор instanceof корректно отвергает попытки
  // сравнения со значениями null и undefined
  if (!(that instanceof Set)) return false;
  // Если два множества имеют разные размеры, они не равны
  if(this.size() ! = that.size()) return false;
  // Теперь требуется убедиться, что каждый элемент в this также присутствует в that.
  // Использовать исключение для прерывания цикла fоreach, если множества не равны,
  try {
    this.foreach(function(v) {if(!that.contains(v)) throw false; });
    return true; // Все элементы совпали: множества равны.
  } catch (х) {
    if (х === false) return false; // Элемент в this отсутствует в that,
    throw x; // Для других исключений: возбудить повторно.
  }
}:

Иногда бывает полезно реализовать операции сравнения, чтобы выяснить порядок следования объектов. Так, для некоторых классов вполне можно сказать, что один экземпляр «меньше» или «больше» другого. Например, объекты Range можно упорядочивать, опираясь на значение нижней границы. Типы-перечисления можно было бы упорядочивать в алфавитном порядке по именам или в числовом порядке - по значениям, присваиваемым именам (предполагается, что именам присваиваются числовые значения). С другой стороны, объекты класса Set не имеют какого-то естественного порядка следования.

При попытке сравнения объектов с помощью операторов отношения, таких как < и <=, интерпретатор сначала вызовет методы valueOf() объектов и, если методы вернут значения простых типов, сравнит эти значения. Типы-перечисления, возвращаемые методом enumeration() из примера 9.7, имеют метод valueOf() и могут сравниваться с помощью операторов отношения. Однако большинство классов не имеют метода valueOf(). Чтобы сравнивать объекты этих типов для выяснения порядка их следования по вашему выбору, необходимо (опять же, следуя соглашениям, принятым в языке программирования Java) реализовать метод с именем compareTo().

Метод compareTo() должен принимать единственный аргумент и сравнивать его с объектом, метод которого был вызван. Если объект this меньше, чем объект, представленный аргументом, метод compareTo() должен возвращать значение меньше нуля. Если объектthis больше, чем объект, представленный аргументом, метод должен возвращать значение больше нуля. И если оба объекта равны, метод должен возвращать ноль. Эти соглашения о возвращаемом значении весьма важны, потому что позволяют выполнять замену операторов отношения следующими выражениями:


Класс Card в примере 9.8 определяет подобный метод compareTo(), и мы можем написать похожий метод для класса Range, чтобы упорядочивать диапазоны по их нижним границам:

Range.prototype.compareTo = function(that) {
  return this.from - that.from;
};

Обратите внимание, что вычитание, выполняемое этим методом, возвращает значение меньше нуля, равное нулю или больше нуля в соответствии с порядком следования двух объектов Range. Поскольку перечисление Card.Rank в примере 9.8 имеет метод valueOf(), мы могли бы использовать тот же прием и в методе сотраreTo() класса Card.

Методы equals(), представленные выше, выполняют проверку типов своих аргументов и возвращают false, как признак неравенства, если аргументы имеют не тот тип. Метод compareTo() не имеет специального возвращаемого значения, с помощью которого можно было бы определить, что «эти два значения не могут сравниваться», поэтому обычно методы compareTo() возбуждают исключение при передаче им аргументов неверного типа.

Примечательно, что метод compareTo() класса Range, представленный выше, возвращает 0, когда два диапазона имеют одинаковые нижние границы. Это означает, что в данной реализации метод сотрагеТо() считает равными любые два диапазона, которые имеют одинаковые нижние границы. Однако такое определение равенства не согласуется с определением, положенным в основу метода equals(), который требует равенства обеих границ. Подобные несоответствия в определениях равенства могут стать причиной опасных ошибок, и было бы лучше привести методы equals() и compareTo() в соответствие друг с другом. Ниже приводится обновленная версия метода compareTo() класса Range. Он соответствует методу equals() и дополнительно возбуждает исключение при передаче ему несопоставимого значения:

// Порядок следования диапазонов определяется их нижними границами
// или верхними границами, если нижние границы равны.Возбуждает исключение,
// если методу передается объект, не являющийся экземпляром класса Range.
// Возвращает 0, только если this.equals(that) возвращает true.
Range.prototype.compareTo = function(that) {
  if (!(that instanceof Range))
    throw new Еrror("Нельзя сравнить Range c " + that);
  var diff = this.from - that.from; // Сравнить нижние границы
  if (diff == 0) diff = this.to - that.to; // Если равны, сравнить верхние
  return diff;
};

Одна из причин, по которым может потребоваться сравнивать экземпляры класса, - обеспечить возможность сортировки массива экземпляров этого класса. Метод Array.sort() может принимать в виде необязательного аргумента функцию сравнения, которая должна следовать тем же соглашениям о возвращаемом значении, что и метод compareTo(). При наличии метода compareTo(), представленного выше, достаточно просто организовать сортировку массива объектов Range, как показано ниже:

ranges.sort(function(a,b) { return a.compareTo(b); });

Сортировка имеет настолько большое значение, что следует рассмотреть возможность реализации статического метода сравнения в любом классе, где определен метод экземпляров compareTo(). Особенно если учесть, что первый может быть легко реализован в терминах второго, например:

Range.byLowerBound = function(a,b) { return a.compareTo(b); };

При наличии этого метода сортировка массива может быть реализована еще проще:

ranges.sort(Range.byLowerBound);

Некоторые классы могут быть упорядочены более чем одним способом. Например, класс Card определяет один метод класса, упорядочивающий карты по масти, а другой - упорядочивающий по значению.

Оглавление книги


Генерация: 0.047. Запросов К БД/Cache: 0 / 2
поделиться
Вверх Вниз