Конспект JS-course

Тонкости ECMA-262-3. Часть 7.1. ООП: Общая теория

Источник: http://dmitrysoshnikov.com/ecmascript/ru-chapter-7-1-oop-general-theory/

ECMAScript – это объектно-ориентированный язык программирования с прототипной организацией.

Особенности классовой и прототипной организаций

Рассмотрим общую теорию и ключевые моменты этих парадигм.

Статическая классовая организация

В классовой организации присутствует понятие класса (class) и сущности (instance), принадлежащей данной классификации. Сущности класса также часто называют объектами (object) или экземплярами.

Класс представляет собой формальное абстрактное множество обобщённых характеристик сущности (знаний об объектах).

Понятие множество в этом отношении более близко к математике, однако, можно говорить о типе или классификации.

Пример (здесь и ниже – примеры будут псевдокодом):

C = Class {a, b, c} // класс C, с характеристиками a, b, c

К характеристикам сущностей относятся свойства (описание объекта) и методы (активность объекта).

Сами характеристики, также могут быть представлены объектами.

При этом, объекты хранят состояние (т.е. конкретные значения всех свойств, описанных в классе), а классы определяют жёсткую структуру (т.е. наличие тех или иных свойств) и жёсткое поведение (наличие тех или иных методов) своих экземпляров.

C = Class {a, b, c, method1, method2}

c1 = {a: 10, b: 20, c: 30} // объект с1 класса С
c2 = {a: 50, b: 60, c: 70} // объект с2 со своим состоянием, того же класса С

Ключевые моменты классовой модели

Итак, имеем следующие ключевые моменты:

  • чтобы породить объект, нужно перед этим обязательно описать его класс;
  • при этом, объект будет создан по “образу и подобию” (структуре и поведению) своей классификации;
  • разрешение методов осуществляется в жёсткой цепи наследования;
  • классы-потомки (и соответственно, порождаемые от них объекты) содержат все свойства цепи наследования (даже, если какие-то из этих свойств не нужны конкретному унаследованному классу);
  • будучи порождённым, класс не может (в виду статической организации) изменить набор характеристик (ни свойств, ни методов) своих экземпляров;
  • экземпляры (вновь, в виду жёсткой статической организации) не могут обладать ни дополнительным (своим уникальным) поведением, ни дополнительными свойствами, отличными от структуры и поведения своего класса.

Посмотрим, что предлагает альтернативная ООП организация, на базе прототипов.

Прототипная организация

Здесь основным понятием являются динамические изменяемые (мутируемые) объекты (dynamic mutable objects).

Мутации (полная изменяемость: не только значений, но и всех характеристик) непосредственно связаны с динамикой языка.

Такие объекты могут самостоятельно хранить все свои характеристики (свойства, методы) и в классе не нуждаются.

object = {a: 10, b: 20, c: 30, method: fn};
object.a; // 10
object.c; // 30
object.method();

Более того, в виду динамики, они могут свободно изменять (добавлять, удалять, модифицировать) свои характеристики:

object.method5 = function () {...}; // добавили новый метод
object.d = 40; // добавили новое свойство "d"
delete object.c; // удалили свойство "с"
object.a = 100; // модифицировали свойство "а"

// в итоге: object: {a: 100, b: 20, d: 40, method: fn, method5: fn};

То есть, при присвоении, если определённая характеристика не существует в объекте, она создаётся и инициализируется переданным значением; если существует, – производится её модификация.

Повторное использование кода в данном случае достигается не за счёт расширения классов (обратите внимание, ни о каких классах, как о множествах жёстких характеристик, речи не идёт; здесь их вообще нет), а посредством обращения к, так называемому, прототипу.

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

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

Я напомню, мы сейчас ведём разговор об общей теории, мало касаясь реализаций; когда будем разбирать конкретные реализации (и, в частности, ECMAScript), увидим ряд своих особенностей.

Пример (псевдокод):

x = {a: 10, b: 20};
y = {a: 40, c: 50};
y.[[Prototype]] = x; // x – прототип y

y.a; // 40, собственная характеристика
y.c; // 50, тоже собственная
y.b; // 20 – полученная из прототипа: y.b (нет) -> y.[[Prototype]].b (да): 20

delete y.a; // удалили собственную "а"
y.a; // 10 – получена из прототипа

z = {a: 100, e: 50}
y.[[Prototype]] = z; // изменили прототип y на z
y.a; // 100 – получена из прототипа
y.e // 50, тоже – получена из прототипа

z.q = 200 // добавили новое свойство в прототип
y.q // изменения отобразились и на y

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

Этот механизм называется делегацией (delegation), а связанная с ним прототипная модель, – делегирующим прототипированием.

Обращение к характеристикам в данном случае называется посылкой сообщения объекту. Т.е., когда объект не может ответить на сообщение самостоятельно, он обращается к своему прототипу (делегирует ему полномочия за ответ).

Повторное использование кода в данном случае называется делегирующим наследованием (delegation based inheritance) или наследованием, основанным на прототипах (prototype based inheritance).

Поскольку прототипом может быть любой объект, соответственно, и у прототипов, могут быть свои прототипы. Данная комбинация связанных между собой прототипных объектов образует, так называемую, цепь прототипов (prototype chain). Она, так же, как и в статичных классах, иерархична, однако, в виду мутаций может свободно перегруппировываться, изменяя иерархию и состав.

x = {a: 10}

y = {b: 20}
y.[[Prototype]] = x

z = {c: 30}
z.[[Prototype]] = y

z.a // 10

// z.a найдено по цепи прототипов:
// z.a (нет) ->
// z.[[Prototype]].a (нет) ->
// z.[[Prototype]].[[Prototype]].a (да): 10

Касательно ECMAScript, здесь используется именно эта реализация – делегирующее прототипирование. Однако, как мы увидим, на уровне стандарта и реализаций есть и свои особенности.

Ключевые особенности прототипной модели

Итак, выделим ключевые моменты данной организации:

  • основным понятием является объект;
  • объекты полностью динамичны и изменяемы (и в теории, могут полностью мутировать из одного вида в другой);
  • у объектов нет жёстких классов, задающих их структуру и поведение; объекты не нуждаются в классах;
  • однако, не имея классов, объекты могут иметь прототипы, к которым можно делегировать, если сами объекты не в состоянии ответить на посланное им сообщение;
  • прототип объекта может быть изменён в любое время программы;
  • в делегирующей прототипной модели, изменение характеристик прототипа отображается на всех объектах, связанных с этим прототипом;
  • каскадная же прототипная модель, служит прообразом, с которого порождаемые объекты снимают точную копию и дальше становятся полностью самостоятельными; изменение прототипа в данной модели уже не влияет на клонируемые от него объекты;
  • если сообщение обработать не удаётся, можно сигнализировать об этом вызывающей стороне, которая может предпринять дополнительные меры (например, изменить диспетчеризацию);
  • идентификация объектов может производиться не по их иерархии и принадлежности к конкретному типу, а по текущему набору характеристик.

Полиморфизм

Объекты ECMAScript – полиморфны во многих отношениях.

К примеру, одна функция может быть применена к разным объектам, как, если бы, она являлась родной характеристикой объекта (в виду определения this на этапе вызова):

function test() {
  alert([this.a, this.b]);
}

test.call({a: 10, b: 20}); // 10, 20
test.call({a: 100, b: 200}); // 100, 200

var a = 1;
var b = 2;

test(); // 1, 2