Javascript. Object oriented programming. Object literal

Как известно, javascript относится к объектно-ориентированному языку, хотя парадигма разрабатываемых структур программ (сценариев) может быть и не объектно - ориентированной. Но рано или поздно приходит осознание, что использование принципов объектно-ориентированного программирования (ООП) более естественно и более продуктивно в ходе разработки. ООП неразрывно связано с понятием классов, которые выступают с одной стороны как универсальные структуры данных (с точки зрения предметного подхода), а с другой стороны - как особый тип данных (с точки зрения архитектуры, "бинарный" подход). Именно в классах проявляются принципы ООП, реализуемые в соответствующих объектах, инстансах. В javascript основной акцент перемещен на менее абстрактный уровень - на уровень объектов. Рассмотрим как создаются объекты в C++:

class CRectangle {
    int width, height;
  public:
    void set_values (int, int);
    int area (void) {return (width * height);}
};

void CRectangle::set_values (int a, int b) {
  width = a;
  height = b;
}
 
 
И в основной точке инстанцируем следующие объекты:

CRectangle a;
CRectangle * b = new CRectangle; 
 
Не забывая при выходе вызвать явно деструктор для объекта b:

delete b;
 
Отсюда видим, что прежде, чем создавать тот или иной объект, нам необходимо заложить общую будущую структуру в виде класса, определив при необходимости конструктор, деструктор, поля с соответствующими уровнями доступа и методы, также имеющие уровни внешнего доступа (private, public и protected).
Теперь рассмотрим как сие происходит в javascript. Есть несколько способов создания объектов: - посредством литерала {}:

var Obj = {};

- либо посредством функции-конструктора:

var Obj = new Object();

Конструктор Object создает пустой объект, как если бы использовался литерал {}, возвращая ссылку на созданный объект. Стоит отметить, что Object относится к базовым встроенным конструкторам, к которым относятся и такие как Array, Date, RegExp и прочие, но помимо этого есть возможность и в определении собственных конструкторов, например вот так:

function Dog( name ) {
    this.name = name || '';
}

var MyDog = new Dog( 'Rex' );

Как видим в javascript шаблоном объекта выступает не класс, а объект ( функции в javascript являются объектами ). Конечно, второй способ, где есть возможность определять свой конструктор дает больше возможностей для дальнейших манипуляций с объектами. Есть немало литературы, где больше освещается использование именно данного способа. Но меня больше заинтересовало использование первого способа - литерала объекта (Object literal), так как в данном случае мы немного стеснены в своих возможностях в силу отсутствия своего конструктора.

Рассмотрим общее использование литерала объектов. Для этого создадим объект Obj с определенными свойствами:

var Obj = {
    // внутреннее свойство
    value: '',
    // установка значения свойства value
    setValue: fuction( value ) {
        this.value = value || '';
    },
    // извлечение значения свойства value
    getValue: function() {
        return this.value;
    }
};

Обращаться к свойству объекта можно через точку (.), напоминает статические объекты в C++:

Obj.setValue('Obj'); 
console.log( Obj.getValue() ); // output: Obj  

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


var Obj = {
    reference: Obj
};
console.log( Obj.reference ); // output: undefined

А если попытаться получить ссылку на себя так:

var Obj = {
    reference: this
};
console.log( Obj.reference ); // output: window

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

var Obj = {
    reference: null
};
Obj.reference = Obj;
console.log( Obj.reference ); // output: Object { reference={...}}

Теперь рассмотрим вопрос с организацией доступа к свойствам объектов. Как было продемонстрировано выше, изначально все свойства доступны извне, это сравнимо с уровнем доступа - public. Но как быть с такими уровнями как - private и protected. Сначала рассмотрим создание частных свойств. Для литералов объектов можно воспользоваться так называемыми - замыканиями (closure):

var A = {};

A.a = ( function(){
    var a = 16;
    A.getA = function(){ return a; };
    A.setA = function( value ){ a = value || 0 };
    return null;
})();

A.b = ( function(){
    var b = 16;
    A.getB = function(){ return b; };
    A.setB = function( value ){ b = value || 0 };
    return null;
})();


// a null   
// b null
// getA function()
// getB function()
// setA function()
// setB function()


В объекте A при обращении к свойствам a или b в попытках что-то прочитать или внести изменения никак не скажутся на сокрытых a или b в областях просмотра анонимных функций. Можно вообще скрыть ненужные свойства во внутренних объектах:

var B = {};

B.a = ( function(){
    var value = 0;
    return {
        getValue: function(){ return value; },
        setValue: function( val ){ value = val; }
    };
})();

B.b = ( function(){
    var value = 0;
    return {
        getValue: function(){ return value; },
        setValue: function( val ){ value = val; }
    };
})();
   
// a Object { getValue=function(), setValue=function()}
// b Object { getValue=function(), setValue=function()}


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

Теперь рассмотрим как можно использовать наследование между литерал объектами. Создание в одном из своих свойств ссылку на нужный объект - это не наследование:

var A = { name: 'A' }; // parent object
var B = { name: 'B', A: A }; // child object
console.log( B.A.name ); // output: A

В этом случае мы просто имеем ссылку на родительский объект ( к ссылочным типам в javascript относятся - Array, Object, Function ). В javascript у каждого конструктора объекта имеется свойство __proto__, в котором хранится ссылка на объект с конструктором (прототип). Это своего рода внутренняя ссылка. Если присвоить этому свойству ссылку на другой объект, то мы получим наследование. Причем в данном случае говорят о прототипном наследовании (prototype inheritance) за счет возможности образования прототипных цепочек (prototype chain):

var A = {
  x: 10,
  calculate: function ( z )
  {
    return this.x + this.y + z;
  }
};

var B = {
  y: 20,
  __proto__: A
};

var C = {
  y: 30,
  __proto__: A
};

// вызов унаследованного метода
console.log( B.calculate(30) ); // 60
console.log( C.calculate(40) ); // 80


Как видно из этого примера, объекты B и C за счет ссылки на  объект A унаследовали метод calculate, это произошло за счет прототипной цепочки: объект B не найдя у себя затребованного метода обращается к свойству __proto__ и по этой ссылке пытается обратиться к другому объекту, наружу, чтобы попытаться найти метод, если и там его не будет найдено, далее также будет обращение к этому свойству. Так и образуются цепочки: own -> proto -> proto -> proto -> null. В классовом же наследовании (абстрактно): own -> class -> superclass -> superclass -> null. В конечном счете окажется, что у некоторого объекта эта ссылка будет замкнутой ( можно в замыкающем объекте определить сие свойство в null значение ) и тогда мы получим либо undfined, либо сообщение об ошибочности нашего обращения. Также стоит обратить внимание на ключевое слово this. Оно всегда ссылается на конечный объект в цепочке прототипов. Обратите внимание на конечный результат таких цепочек в следующем примере.

var Parent = {
    a: 'Parent',
    getName: function()
    {        

          console.log( this );
    }
};

var Child = {
    b: 'Child',
    __proto__: Parent
};


// Object { b="Child", a="Parent", getName=function()}
Child.getName();


В итоге в наследуемом объекте Parent оказываются все свойства и методы наследника Child. Рассмотрим еще пример:

var Man = {
    head:
    {
        hair: 'black',
        eyes: 'black',
        nose: 'direct'
    },
    body:
    {
        hands: 'normal',
        feet: 'normal',
        brawn: 'normal'
    },
    getPattern: function(){
            console.log( 'Man pattern:' );
            console.log( this.head );
            console.log( this.body );
    },
    pattern:{}
};

Man.pattern.__proto__ = Man;


// output: Man pattern
// output: head Object{}
// output: body Object{}
Man.pattern.getPattern();


Вот так просто можно обращаться из вложенных (дочерних) объектов к объекту родителю.
Приведенный выше способ манипуляции с прототипами считается устаревшим, в новом стандарте javascript - ECMAScript 5th Edition ( JavaScript 1.8.5 ) для этих целей есть соответствующие методы: getPrototypeOf() и create(). Рассмотрим их подробней.
Метод create() используется в момент создания нового объекта с возможностью передачи в аргументе объекта, свойства которого необходимо унаследовать во вновь создаваемом объекте.

var A = {
    name: 'A'
};

var B = Object.create( A );

// output: A
console.log( B.name );

Если нет необходимости в унаследовании, то в аргументе передается null, так в прототипе явно уазывается, что он ни на кого не ссылается:

var C = Object.create( null );

Второй метод - getPrototypeOf() - используется для существующих объектов, он возвращает прототип объекта, указываемого в аргументе, в который можно прописать необходимую ссылку для реализации наследования.

var A = {
    name: 'A'
};   

var B = {};

Object.getPrototypeOf( B ).name = A.name;

// output: A
console.log( B.name );

В этом примере мы получаем прототип объекта B и сразу же  в него добавляем новое свойство name, которое ссылается на свойство name объекта A. Имя свойство необязательно должно совпадать с наследуемым свойством, но все же так правильней, логичней и прозрачней. Есть одно но: нельзя передавать непосредственно возвращаемому прототипу ссылки на свойства или объекты, интерпретатор будет ругаться ( связано это с тем, что возвращается объект ):

// либо так
Object.getPrototypeOf( B ) = A;

// либо так
Object.getPrototypeOf( B ) = A.name;

Оба варианта - нерабочие. Нужно обязательно обращаться через свойства.

Ниже приведу дополнительно примеры с этими методами:


//var myObj = {__proto__: {property1_OfProto: 1}}
var myObj = Object.create({property1_OfProto: 1});
 
//myObj.__proto__.property2_OfProto = 2
Object.getPrototypeOf(myObj).property2_OfProto = 2; 


Напоследок стоит сказать, что данные методы в определенных браузерах не поддерживаются, для решения данной проблемки можно воспользоваться библиотекой - github.com/kriskowal/es5-shim, также можно почитать в блоге Джона Резига - http://ejohn.org/blog/objectgetprototypeof/