Meshbeyn / JavaScript

Конспект по JavaScript

5 Объекты

Объекты в JavaScript серьезно отличаются от своих собратьев в большинстве языков. Поскольку JavaScript позволяет динамически добавлять и удалять поля из объектов, все главные черты ООП не имеют никакого смысла. Фактически, каждый объект - это словарь из пар ключ-значение. Ключом может быть любая строка или число, а значением - любое значение, в том числе и функция. В JavaScript имеется возможность несколько ограничивать доступ к элементам при помощи атрибутов и объявлять общие поля при помощи прототипов. Работа с объектами в JavaScript даже запутаннее, чем во многих языках со сложной структурой.

Создание объекта

Объекты можно создавать разными способами. Поскольку четкая структура в объектах не гарантируется, нет и классов. Самый простой способ создать объект - краткое объектное выражение:

var point = { x : 100, 
              y : 200, 
              show : function() { alert("(" + x + ", " + y + ")"); } 
            };

Здесь мы создали объект с полями x и y, назначили им значения и присвоили этот объект переменной point. Также объект имеет метод show, выводящий его координаты. Фигурные скобки означают создание нового объекта. Следующая запись равнозначна предыдущей:

var point = new Object();
point.x = 100;
point.y = 200;
point.show = function() { alert("(" + x + ", " + y + ")"); };

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

function Point(x, y)
{
  this.x = x;
  this.y = y;
  this.show = function() 
              { 
                 alert("(" + x + ", " + y + ")"); 
              } 
}        

var P = new Point(100, 200);
P.show();

Определение свойств объекта

У каждого свойства объекта есть собственные атрибуты. Свойства бывают двух типов: именованные данные и именованные аксессоры. Именованные данные - это обычные поля (переменные). Именованные аксессоры внешне выглядят как обычные поля, но в определении объекта реализованы двумя функциями: get и set. Это позволяет выполнять некоторые вычисления в момент обращения к свойству. Обычно это используется для проверки правильности аргументов и обновления связанных объектов в функции set и для вычисления сложного значения в функции get.

Атрибуты свойств

Атрибуты именованных данных
Value
Непосредственное значение поля.
Writable
Указывает, может ли значение изменяться после создания. Если равно false, поле будет доступно только для чтения.
Атрибуты именованных аксессоров
get
Функция, вызываемая при чтении значения свойства. В этой функции обычно вычисляется возвращаемое значение в зависимости от состояния этого и связанных с ним объектов.
set
Функция, вызываемая при назначении свойству нового значения. В этой функции обычно проверяют корректность нового значения и автоматически обновляют значения других свойств и объектов при необходимости.
Атрибуты для всех свойств
Enumerable
Атрибут участия в перечислениях. Если равен false, то это поле не будет видимо в некоторых операциях. Например, не будет перечисляться в цикле for in.
Configurable
Атрибут настраиваемости. Если равен false, то изменять атрибуты поля больше нельзя (кроме значения).

Автоматическое определение свойства

При присваивании свойству объекта нового значения, система ищет в объекте определение свойства с указанным именем. Если данное свойство не найдено, то создается новое свойство данных в объекте и ему  назначается данное значение. Остальные атрибуты устанавливаются на максимальную доступность (Configurable, Writable и Enumerable равны true. Set и Get для поля данных не определяются).

Пример создания поля с заданными свойствами

function Rectangle(w, h)
{
    this.w = w;
    this.h = h;
}

var R = new Rectangle(10, 15);

Object.defineProperty(R, "area", 
                        {
                            get : function() { return this.w * this.h ; },
                            enumerable : false
                        }
);

//Area of R: 150
document.write("<br />Area of R: " + R.area);

В этом примере к объекту Rectangle R добавляется вычисляемое свойство только для чтения "area". Свойство не должно быть перечисляемым в цикле for in. Функция Object.defineProperty требует 3 параметра: дополняемый объект, название свойства и объект с атрибутами нового поля. Несколько полей за один вызов можно добавить функцией Object.defineProperties. Также можно создать объект сразу с указанием прототипа и массива определителей полей через функцию Object.create.

Прототип объекта

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

Этот механизм позволяет строить конструкции, которые похожи на механизмы наследования в ООП. Но необходимо помнить, что их возможности не равны. С прототипами можно делать вещи, которые нельзя сделать напрямую в ООП. Также в ООП есть вещи, которые нельзя сделать с помощью прототипов. Поэтому, прототипы не лучше и не хуже, чем ООП - они просто разные. Программисты с опытом работы с ООП ожидают от прототипов полной эмуляции. Получается, что они не могут получить от прототипов всего желаемого и при этом не понимают, какие дополнительные возможности у них появляются. Это основная причина недовольства прототипами.

Присвоение и получение прототипа объекта

Получение и назначение свойств и методов объекта с учетом прототипов

При получении (чтении свойства или вызове метода) элемента, его имя ищется среди элементов объекта. Если объект не содержит определение такого имени, оно ищется в прототипе. Поскольку прототип - это тоже объект и он тоже может ссылаться на свой прототип, там происходит то же самое. Если цепочка прототипов содержит определение такого имени, то соответствующее свойство или метод используется. Иначе получается значение undefined, что обычно приводит к ошибке.

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

Если нужно изменить именно элемент прототипа, то сначала нужно получить этот прототип как объект, а затем назначить его элементу нужное значение.

Пример работы с прототипом

function Point(x, y)
{
    this.x = x;
    this.y = y;
    this.show = function()
                {
                    return "(" + this.x + ", " + this.y + ")";
                }
}

var P = new Point(3, 5);
// Point P: (3, 5)
document.write("<br />Point P: " + P.show());

function Rectangle(w, h)
{
    this.w = w;
    this.h = h;
}
Rectangle.prototype = P;

var R = new Rectangle(10, 15);
// Rectangle R: (3, 5)
document.write("<br />Rectangle R: " + R.show());

R.x = 1;
//Rectangle R after x changed: (1, 5)
document.write("<br />Rectangle R after x changed: " + R.show());
//Point P after R.x changed: (3, 5)
document.write("<br />Point P after R.x changed: " + P.show());


R.show =  function()
          { 
              var p = Object.getPrototypeOf(this).show();
              return "{" + p + ", " + this.w + ", " + this.h + "}";
          };

R.show2 = function()
          {
              return "{(" + this.x + ", " + this.y + "), " + this.w + ", " + this.h + "}";
          };

delete R.x;
// R with own function: {(3, 5), 10, 15}
document.write("<br />R with own function: " + R.show());

Object.getPrototypeOf(R).y = 10;
// R after changes in prototype: {(3, 10), 10, 15}
document.write("<br />R after changes in prototype: " + R.show());

R.x = 10;
// R after overriding x using show from prototype (still takes values from prototype): {(3, 10), 10, 15}
document.write("<br />R after overriding x using show from prototype (still takes values from prototype): " + R.show());

// P.x is not changed: (3, 10)
document.write("<br />P.x is not changed: " + P.show());

// R reading all variables from this object (takes from prototype only y): {(10, 10), 10, 15}
document.write("<br />R reading all variables from this object (takes from prototype only y): " + R.show2());

В данном примере происходит следующее:

  1. Сначала объявляется конструктор Point и создается объект через этот конструктор.
  2. Затем объявляется конструктор Rectangle, его прототипом назначается созданный ранее объект Point и создается новый прямоугольник.
  3. Поля и методы из объекта Point доступны и в объекте Rectangle. Когда мы присваиваем полю x новое значение, оно создается в объекте Rectangle. Метод show показывает, что новое значение имеет только объект Rectangle.
  4. К объекту Rectangle добавляются поля методы show и show2. Метод show теперь перекрывает одноименный метод из прототипа, при этом он вызывает его при своей работе. Мы удаляем поле x из объекта Rectangle, чтобы проверить наследование с новыми функциями.
  5. Меняется значение поля y в прототипе. Метод show показывает изменения.
  6. Меняется значение поля x в самом объекте. При этом в объекте Rectagle создается поле x. Метод show не видит это изменение, так как он читает поля x и y из прототипа. В объекте P поле x по прежнему равно 3. А вот метод show2 теперь берет все значения кроме y из своего объекта.