Объекты в JavaScript

Объект - он и в Африке объект. И JavaScript не отличается особой оригинальностью на этот счет: объект - это конструкция, в которой мирно уживаются свойства (обычные переменные либо другие объекты) и методы (функции). Договоримся называть свойства и методы объекта, вместе взятые, его составляющими.

Доступ к свойствам объекта осуществляется с помощью "точечной" нотации:
    object.property
либо с помощью "массивной" ;) нотации:
    object[ "property"].

Вторая нотация удобна, когда имя свойства, с которым работаем, заранее не известно (и поэтому не указано в коде скрипта), а находится в переменной. К примеру, пусть переменная prop="counter" - задает имя некоторого свойства объекта obj. Тогда "точечная" нотация для обращения к этому свойству выглядит так:
    eval("obj."+prop) - на чтение (и запись для свойств-объектов), и
    eval( "obj."+prop+"="+value) - на запись (для свойств-необъектов).
А "массивная":
    obj[prop] - на чтение и запись.
Преимущества второй нотации очевидны.

Примечание – А.Р.
Раздвоение ситуации с eval связано с тем, что eval возвращает ссылку только для объектов. Для всех других типов данных – возвращается значение.

«Массивная» нотация позволяет рассматривать объекты как ассоциативные массивы (т.к. индексами в таком массиве являются строки).

Все, что сказано о доступе к свойствам объекта, верно и для методов, вот только после обеих нотаций нужно использовать скобки с параметрами вызова или без таковых, т.к. без скобок нотация для метода дает доступ к исходному тексту метода. Этот доступ – как на чтение, так и на запись. Но запомните, что присвоение этому свойству нового исходного текста преобразовывает метод в свойство типа string (это легко проверить с помощью typeof()).

Примеры обращения к методам.
obj.f() <=> obj["f"]()
obj.count(1,2) <=> obj["count"](1,2)

Пример ошибки.
obj.f (или obj["f"]) = "function anonymous() { return 1; }" – в результате метод obj.f становится строкой.

Утверждения для манипулирования объектами.
Итератор составляющих объекта:
    for (name in obj)

В этом цикле переменная name пробегает множество значений имен составляющих объекта obj (не обязательно в лексикографическом порядке. Совсем не обязательно ;))

Пример.
/* Вывод всех пар составляющая-значение для объекта window: */
for ( var i in window)
  document.write(i + " = " + window[i] + "<br>");

Утверждение with («группировщик»):
    with (obj) {
     …
    }

Устанавливает для набора утверждений в фигурных скобках объект по умолчанию. Если какое-либо имя, использованное в этих утверждениях не найдено среди локальных и глобальных имен, тогда поиск выполняется по составляющим объекта. Если таковое найдено, оно применяется.

Пример.
/* считаем, что переменных x и y в охватывающей программе НЕТ */
with (obj) {
  x = 1;
  y = 2;
}
то же, что и
obj.x = 1; obj.y = 2;

Создание новых объектов.
Кроме определенных по умолчанию (Array, Math и др.), JavaScript допускает создание новых типов объектов.
Для создания нового типа объекта вначале необходимо описать его конструктор, который представляется собой обычную функцию, а потом использовать оператор new. Например:

function obj() {

} – становится конструктором при вызове: A = new obj().

В указанном примере объект A не имеет ни свойств, ни методов. Для определения свойств в конструкторах используется ключевое слово this. Например:

function obj( x0) {
  this.x = x0;
} – при вызове A = new obj(5) создается объект со значением свойства x = 5.

Похожим образом определяются методы объектов: this.f = fname, где fname – имя существующей функции (без кавычек). Внутри методов ключевое слово this применяется для доступа к контексту содержащего этот метод объекта. Пример:

function sum() {
  return this.x+this.y;
}
function sumator( x0, y0) {
  this.x = x0;
  this.y = y0;
  this.f = sum;
}
A = new sumator(3,8);
A.f() возвращает 11.

При таком способе определения, как видим, методы находятся в глобальном пространстве имен. Однака, можно полностью скрыть код метода в объекте. Для этого нужно использовать объект Function. Вот пример того, как это делается:

function sumator( x0, y0) {
  this.x = x0;
  this.y = y0;
  this.f = new Function( "return this.x+this.y;");
}
A = new sumator(3,8);
A.f() возвращает 11.

Общий формат вызова конструктора объекта Function таков:
    new Function( "arg1","arg2",…,"argN","code"),

т.е. вначале перечисляются параметры функции, а потом записывается тело функции.
В качестве примера метода с параметрами приведем следующий код (считаем, что этот код записан внутри конструктора sumator):
this.scalar = new Function("xp","yp","return xp*this.x+yp*this.y;");
Тогда A.scalar(-2,1) вернет 2.

Тело функции, разумеется, может состоять не только из одного предложения языка. Если предложений несколько – их можно записать в одну строку либо в несколько строк, пользуясь символом продолжения строки - "\". Пример:

this.x_sqrt = new Function("if (this.x>0) return Math.sqrt(this.x); return 0;");

или, что то же самое:

this.x_sqrt = new Function("\
  if (this.x >0)\
   return Math.sqrt(this.x);\
  return 0;\
");
Второй способ записи, разумеется, удобнее.

Прототипы.
JavaScript – объектно-ориентированный язык, основанный на прототипах (C++, Java – основаны на классах). Каковы основные отличия между прототипными и "классовыми" языками? Приведем их в виде таблицы на примере Java и JavaScript.

JavaJavaScript
Класс и экземпляр нетождественны.Все объекты являются экземплярами.
Класс как таковой объявляется отдельно, экземпляры – порождаются вызовами конструкторов.Объявление и создание объектов – все это выполняет конструктор.
Построение иерархии объектов производится в объявлениях классов.Построение иерархии объектов выполняется установкой одного объекта прототипом другого.
Свойства наследуются согласно цепочке классов.Свойства наследуются согласно цепочке прототипов.
Объявление класса определяет все составляющие всех экземпляров класса. Динамически добавлять новые составляющие нельзя.Конструктор или прототип определяют начальное множество составляющих. Можно добавлять и удалять новые составляющие в отдельных экземплярах или во всех экземплярах данного типа объектов динамически.

Итак, разберем некоторые особенности JavaScript, как прототипного языка.

К любому созданному экземпляру объекта в любое время можно добавить новые составляющие. Делается это с помощью простого присваивания.

Пример.  function obj( x0) {   this.x = x0;  }  A = new obj(1);  A.y = 5;  B = new obj(2);

Объект A имеет два свойства: x и y. Но объект B имеет только свойство x, т.к присваивание A.y=5 создает новое свойство только у объекта A, а не у всех объектов типа obj.

Удаление составляющих производится с помощью оператора delete: delete A.y; - и A состоит только из свойства x.

Чтобы добавлять/удалять составляющие из всех экземпляров данного типа объектов необходимо обратиться к прототипу – свойству prototype.

Пример.  function obj( x0) {   this.x = x0;  }  A = new obj(1);  obj.protopype.y = 5;  B = new obj(2);

Объекты A и B имеют два свойства: x и y. И у обоих значение свойства y равно 5.

С помощью свойства prototype (и никак иначе) строится иерархия объектов. Это свойство является простым значением, т.к. в JavaScript допустимо только простое наследование, т.е. у всякого объекта может быть не больше одного непосредственного предка.

Пример наследования.  function trig() {   this.sin = Math.sin;   this.cos = Math.cos;  }    function trig_hypo() {   this.sh = new Function("x",  "return (Math.exp(x)-Math.exp(-x))/2");   this.ch = new Function("x",  "return (Math.exp(x)+Math.exp(-x))/2");  }  trig_hypo.prototype = new trig;

Теперь объект trig_hypo содержит четыре метода, два из которых пронаследованы от trig.

Ну что же, общий принцип создания иерархии объектов, думаю, понятен. Однако, не все так просто, как может показаться на первый взгляд. В Core JavaScript Guide 1.5описаны некоторые «подводные камни», которые подстерегают разработчика иерархии объектов в самом безобидном коде. Настоятельно рекомендую ознакомиться с разделом Details of the Object Modelиз указанной документации перед тем, как серьезно заниматься объектно-ориентированным программированием на JavaScript.