Изучаем JavaScript, ч. 4: Объекты: Объектно-Ориентированный JavaScript

мая 18, 2011  |  Published in ClientSide, JavaScript  |  4 Comments

javascriptJavaScript — объектно-ориентированный язык программирования в котором почти все является объектами. Основным отличием «объектно-ориентированности» JavaScript от, например, Ruby является то, что JavaScript является class-free языком программирования, то есть в JavaScript отсутствует концепция классов. Если в Ruby объект является экземпляром класса, то в JavaScript объект является просто объектом, а передача свойств происходит через прототипы и функции — конструкторы.

Давайте для начала разберемся с тем, что такое объект в JavaScript. Объект — это просто коллекция свойств. Если свойство объекта ссылается на функцию, то оно называется методом. Простой пример:

myObj = new Object();
myObj.property = "Value.";
myObj['second_property'] = 12;
myObj.third_property = function () { alert(this.property); };

alert(myObj.second_property); //12
alert(myObj.property); //Value.
myObj.third_property(); //Value.


Пояснения:
1. Object является функцией — конструктором (или просто конструктором). Конструкторы используются как альтернатива классам, при вызове функции-конструктора с предшествующим выражением new создается новый объект, со свойствами определенными в конструкторе, который присваивается переменной myObj.

2. Свойства объектов есть ни что иное, как локальные переменные, которые снабжены аксессорами для доступа к ним снаружи объекта. .property, ['second_property'] и т.д. являются такими аксессорами, они устанавливают и предоставляют значение соответствующих локальных переменных объекта, которые явно не объявляются, а объявляются при помощи этих самых аксессоров.

3. myObj.property и myObj['property'] — синонимы, они выполняют одну и ту же работу. Отличие между ними заключается в том, что первый вариант более удобочитаемый, а второй позволяет обращаться к свойству через переменную, которая хранит его имя, пример:

myObj.alertValue = function (property) { alert(this[property]); };

myObj.alertValue("property"); // Value.
myObj.alertValue("second_property"); // 12

В JavaScript нет ассоциативных массивов (хэшей), их роль выполняют объекты, ведь зачем создавать дополнительный тип данных, если объект сам по себе является коллекцией пар свойство — значение. Для удобства работы с объектами существует объектная нотация синтаксис которой позволяет создавать объекты без использования конструкторов и в удобном виде представлять их структуру. Объектная нотация вышла за рамки JavaScript и превратилась в формат хранения данных — JSON (JavaScript Object Notation), который используется повсеместно, как очень простой, лаконичный и удобный формат представления данных, он используется, например, в документо-ориентированных базах данных типа MongoDB. Пример создания объектов при помощи JSON:

myObj = {
  property : "value",
  rubydev : "RubyDev.ru",
  method : function () { alert(this.property)}
}

myObj.method(); //value

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

В JavaScript все является объектом, даже функции, о которых мы уже говорили, однако значения примитивных типов, например строки и числа становятся объектами только тогда, когда мы пытаемся обратиться к каким-нибудь их свойствам.


Функции — Конструкторы


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

function myConstructor(name, email){
  this.blog = "RubyDev.ru";
  this.name = name;
  this.email = email;
  this.getInfo = function (){
    alert("I'm " + this.name + ". I read " + this.blog + ". My email is: " + this.email + ".");
  };
}

var obj = new myConstructor("Barak", "black.president@whitehouse.gov");

obj.getInfo();
//I'm Barak. I read RubyDev.ru. My email is: black.president@whitehouse.gov.

myConstructor.getInfo(); // Error: myConstructor.getInfo is not a function

Пояснения:
1. Ссылка this, о которой говорилось в предыдущей части ссылается на создаваемы объект, то есть this.blog = «RubyDev.ru» в данном случае эквивалентно obj.blog = «RubyDev.ru», разница лишь в том, что конструктор автоматизирует добавление свойств объекту.
2. Ошибка «myConstructor.getInfo is not a function» возникла потому, что метод getInfo() объявлен не для самого конструктора, а для порождаемых имобъектов. При необходимости мы можем добавить в конструктор его собственные свойства:

myConstructor.getDefaultInfo = obj.getInfo;

myConstructor.getDefaultInfo(); // I'm myConstructor. I read undefined. My email is: undefined.


Прототипы


Каждый объект в JavaScript имеет прототип — объект из которого текущий объект берет некоторые свойства. Прототипы реализуют простое подобие наследования в JavaScript. Давайте посмотрим на работу с прототипами на примере представленного выше кода с конструктором myConstructor и порожденным объектом obj:

function myConstructor(name, email){
  this.blog = "RubyDev.ru";
  this.name = name;
  this.email = email;
  this.getInfo = function (){
    alert("I'm " + this.name + ". I read " + this.blog + ". My email is: " + this.email + ".");
  };
}

var obj = new myConstructor("Barak", "black.president@whitehouse.gov");

alert(myConstructor.prototype); //[object Object]

alert(obj.__proto__); // [object Object] только в Firefox.
alert(obj.constructor); // function myConstructor ...

alert(typeof obj); //object

alert(obj instanceof myConstructor); //true
/*Чтобы получить реальный "класс" - тип (конструктор) объекта,
необходимо использовать метод name применительно к возвращенному конструктору.*/

alert(obj.constructor.name); // myConstructor - реально полезная информация по типу объекта.

Пояснения:
1. Каждая функция имеет свойство prototype — ссылку на прототип объектов, которые будут созданы при использовании функции как конструктора. Практически все объекты и функции в том числе имеют прототип. В каждом объекте имеется скрытое (внутреннее) свойство доступ к которому нельзя получить, которое в документации обозначается как [[prototype]] и содержит ссылку на прототип объекта. Не смотря на сокрытость свойства [[prototype]], в Firefox имеется специальное свойство для доступа к прототипу объекта __proto__, однако в нем нет реальной нужды, так как доступ к прототипу можно получить из конструктора myConstructor.prototype или obj.constructor.prototype.

2. Свойство constructor объекта, как вы уже, наверное, догадались возвращает функцию — конструктор объекта. На самом деле constructor принадлежит не самому объекту, а его прототипу.

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

4. instanceof - оператор, который возвращет true или false соответственно если объект находящийся слева порожден конструктором справа. Оператор очень полезен, например, когда нам необходимо проверить переданные в функцию данные на принадлежность их определенному, необходимому нам типу.

5. Каждая функция имеет предопределенное свойство name с помощью которого пожно получить имя функции данное при декларативном ее создании.

Конструктор позволяет доопределять методы прототипа, а через него и методы порождаемых объектов. Конструктор создавая новый объект наполняет его его собственными свойствами, которые определены в теле конструктора через ссылку this, эти свойства являются свойствами самого объекта. Также конструктор устанавливает объектам ссылку [[prototype]] на прототип с которым работает. Таким образом объект получает свои собственнуе свойства + свойства из цепочки прототипов. Пример:

var obj1 = new myConstructor("John", "elt.john@stars.com");
var obj2 = new myConstructor("Brad", "brad.pitt@stars.com");

obj1.getInfo(); //I'm John. I read RubyDev.ru. My email is: elt.john@stars.com.
obj2.getInfo(); //I'm Brad. I read RubyDev.ru. My email is: brad.pitt@stars.com.

obj1.getInfo = function () { alert("Hello, I'm " + this.name + "."); };
obj1.getInfo(); //Hello, I'm John. - теперь getInfo - свойство самого объекта, а не его прототипа.

myConstructor.prototype.getName = function () { alert(this.name); };
myConstructor.prototype.getInfo = function () { alert(this.email); };

obj1.getName(); //John
obj2.getName(); //Brad
obj1.getInfo(); //Hello, I'm John.
obj2.getInfo(); //I'm Brad. I read RubyDev.ru. My email is: brad.pitt@stars.com.


obj1.getName = function () { alert("My name is Legion: for we are many."); };
obj1.getName(); //My name is Legion: for we are many.

Пояснения:
1. Создавая объекты при помощи конструкторов мы определяем для них собственные свойства: blog, name, email, getInfo.
2. Для obj1 мы переопределяем его собственное свойство getInfo.
3. При помощи свойства prototype конструктора мы получаем доступ к прототипу создаваемых конструктором объектов и объявляем для него два новых свойства getName и getInfo.
4. obj1 и obj2 не имеют свойства getName, поэтому обращаются к своему прототипу, который это свойство имеет. Давайте взглянем на getName не просто как на свойство, а как на запрос к объекту. Когда объкт не может ответить на запрос, он передает (делегирует) запрос своему прототипу, если прототип также не может его обработать, то он передает его своему прототипу и так далее по всей цепочке прототипов, пока не будет дан ответ на запрос. Это можно назвать делегирующим наследованием, когда обработка запроса передается родительскому объекту.
5. Свойство getInfo изначально принадлежало объектам obj1 и obj2, по этой причине не происходит делегирования к одноименному свойству getInfo в прототипе.
6. Делегирование к прототипу происходит только когда объект не может самостоятельно обработать направленый к нему запрос (обращение к свойству объекта). В конце примера мы объявляем свойство getName для самого объекта obj1, такми образом при обращении к obj1.getName объект сам отвечает на запрос и не обращается за помощью к прототипу.

Ниже приведена схема того, как работает делегирование к цепи прототипов:
javascript_prototype_chain

Рекомендация: Используйте прототип для объявления всех свойств объектов с неуникальными значениями, а конструктор для объявления свойств с уникальными значениями. Например метод getName просто выводит сообщение содержащее свойство name и во всех случаях метод getName выполняет одно и то же действие, при этом свойство name у каждого объекта имеет уникальное значение: Вася, Петя, Брэд, Легион и т.д. В таком случае getName должен принадлежать прототипу, а name самим объектам. Это обеспечит экономию ресурсов компьютера, посколько исключит создание нескольких абсолютно идентичных копий одного объекта (каждое свойство — это также объект), хотя и сделает код несколько менее читабельным.


Object Factory — фабрика объектов


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

function objFactory(name, lastname){
  return {
    name : name,
    lastName : lastname,
    constructor : objFactory
  };
};

var obj1 = objFactory("Вася", "Васильев");
alert(obj1.name); //Вася

var obj2 = objFactory("Jack", "Black");
alert(obj2.lastName); //Black
alert(obj2.constructor); // function objFactory ...

В Firefox мы все-таки можем используя __proto__ в  фабрике объектов присвоить продуцируемым объектам специальный общий для группы прототип. В этом случае код фабрики будет выглядеть так:

function objFactory(name, lastname){
  return {
    name : name,
    lastName : lastname,
    __proto__ : objFactory.prototype
  }
}

var obj3 = objFactory("Владимир", "Мельник");

alert(obj3.constructor.name); //objFactory

Спасибо за внимание, друзья! И ждите новых статей о Ruby, Rails, JavaScript и прочих интересных штуках.

Лучшая благодарность автору — ваши комментарии!

Tags:

Responses

  1. says:

    октября 30, 2011 at 21:32 (#)

    Спасибо.
    Второй раз перечитываю, и только на второй раз нахожу здесь для себя полезное :).

  2. says:

    февраля 13, 2012 at 18:16 (#)

    Спасибо автору за отличное изложение материала!

    Не совсем ясно с Object Factory. В чем именно отличия от создания объектов через обычный конструктор?

  3. Олег says:

    марта 31, 2012 at 20:51 (#)

    В литературе, с которой я сталкивался, эта тема
    аккуратно обходится.
    Кратко и понятно.
    Качество изложения достойно учебника.

  4. admin says:

    апреля 2, 2012 at 00:03 (#)

    Олег, спасибо!

Leave a Response

Для подсветки кода используйте BB - коды: [language]...[/language], где language может быть: ruby, javascript, css, html.