Евгений Калоша рассказывает о том, как выглядит объектно-ориентированное программирование в JavaScript. Его видение сформировано также под влиянием многолетнего опыта разработки на Flex, PHP и Java.
Запись доклада:
http://flash-ripper.com/oop-in-javascript
2. Table of contents
Introduction
1. JavaScript data types
1.1. String
1.2. Number
1.3. Boolean
1.4. Object
1.5. Array
1.6. null
1.7. undefined
1.8. Преобразование типов
3. Table of contents
2. OOP in JavaScript
2.1. Object
2.1.1. Свойства объектов
2.1.2. Геттеры и сеттеры
2.1.3. Методы объекта
2.1.4. Что такое this? Магия в действии
2.1.5. Литеральный синтаксис определения объекта
2.1.6. Наследование объектов
2.2. Class
2.2.1. Классы. Наследование
2.2.2. Классы и прототипы. Наследование
4. Introduction
Изначально JavaScript позиционируется как объектно-
ориентированный язык. Это утверждение вызывает споры среди
разработчиков. Основные контраргументы:
• Где классы?
• Где наследование?
• И вообще WTF?
5. Внешне JavaScript (ECMAScript) выглядит как Java, но фактически это
совершенно другой язык, прототипом которого является язык Self
(http://selflanguage.org/). Это обстоятельство делает язык очень
простым и сравнительно мощным благодаря своим особенностям.
Одна из таких особенностей – это реализация прототипного
наследования. Этот простой концепт является гибким и мощным. Он
позволяет сделать наследование и поведение сущностями первого
класса, также как и функции являются объектами первого класса в
функциональных языках (включая JavaScript).
6. 1. JavaScript data types
В JavaScript есть три основных типа данных, два составных типа
данных и два специальных типа данных. Итого 7 типов данных.
Мощно? – Мощно!
Основные типы данных (базовые):
• String
• Number
• Boolean
7. Составные типы данных (ссылочные):
• Object
• Array
Специальные типы данных:
• null
• Undefined
А где же классы? Пичалька…
8. 1.1 String
Строковое значение представляет собой цепочку, состоящую из нуля
или более знаков Unicode (букв, цифр или знаков пунктуации).
Строковый тип данных используется для представления текста в
JavaScript. Для включения в скрипты строковых литералов, их
необходимо заключить в одинарные или двойные кавычки. В
строках, заключенных в одинарные кавычки, можно использовать
двойные кавычки, а в строках, заключенных в двойные кавычки,
можно использовать одинарные кавычки. Ниже представлены
примеры строк:
"String!"
'"String!" String.'
"12345"
's'
9. Обратите внимание, что JavaScript не имеет типа для представления
одиночного символа. Для представления отдельного символа в
JavaScript используется строка, которая состоит только из одного
символа. Строка, не содержащая знаков ("") называется пустой
строкой (или строкой нулевой длины).
Для представления знаков, которые невозможно ввести без
преобразования, в JavaScript предусмотрены escape-
последовательности, включаемые в строки.
Например, t задает символ табуляции.
10. 1.2 Number
• В JavaScript, нет различия между целым числом и числом с плавающей
запятой; число JavaScript может быть и тем, и тем (по сути, JavaScript
представляет все числа в качестве значения с плавающей запятой).
• Целочисленные значения могут быть положительными целыми числами,
отрицательным целыми числами и 0. В качестве основы представления этих
чисел можно использовать 10 (десятичное представление), 16
(шестнадцатеричное представление) и 8 (восьмеричное представление).
Большинство чисел в JavaScript записываются в десятичном представлении.
• Все шестнадцатеричные ("hex") целые числа содержат префикс "0x" (ноль и
x|X). В их состав могут входить только цифры от 0 до 9 и буквы от A до F (в
верхнем или нижнем регистре). Буквы A-F используются для представления
десятичных чисел от 10 до 15. То есть шестнадцатеричное число 0xF
эквивалентно десятичному числу 15, а 0x10 эквивалентно числу 16.
• Все восьмеричные целые числа содержат префикс "0" (ноль). В их состав
могут входить только цифры от 0 до 7. Число, которое начинается с цифры "0"
и содержит цифры "8" и (или) "9", интерпретируется как десятичное.
11. • Восьмеричные и шестнадцатеричные числа могут быть отрицательными,
однако они не могут содержать дробную часть и их невозможно записать в
научной (экспоненциальной) нотации.
– Кроме того, JavaScript содержит числа со следующими специальными
значениями:
– NaN (не число). Используется при выполнении математической операции
над недопустимыми данными, такими как строки или неопределенное
значение
– Положительная бесконечность. Используется, если положительное число
слишком велико и не может быть представлено в JavaScript
– Отрицательная бесконечность. Используется, если отрицательное число
слишком велико и не может быть представлено в JavaScript
– Положительный и отрицательный 0. JavaScript различает положительный и
отрицательный ноль.
12. 1.3 Boolean
В то время как строковые и числовые типы данных могут принимать
практическое неограниченное число различных значений,
логический тип данных может принимать только два значения. Это
литералы true и false. Логическое значение является значением
истинности: оно определяет, является ли условие истинным или нет.
Результат сравнения в скриптах всегда будет иметь тип логического
значения. Пример: (x == 124)
13. 1.4 Object
• Объекты JavaScript представляют собой коллекции свойств и методов (Аналог
Map в Java). Методом называется функция, являющаяся членом объекта.
Свойство представляет собой значение или набор значений (в виде массива
или объекта), который является членом объекта. JavaScript поддерживает
четыре типа объектов: встроенные объекты, создаваемые объекты,
предоставляемые основным узлом объекты (например, window и document в
браузере) и объекты ActiveX.
• Все объекты в JavaScript поддерживают свойства и методы "expando", а также
свойства, которые могут быть добавлены и удалены во время выполнения.
Эти свойства и методы могут иметь любые имена, включая числа. Если имя
свойства или методы является простым идентификатором, то оно может быть
указано через точку после имени метода, например:
14. var myObj = new Object();
myObj.name = "Fred";
myObj.age = 42;
myObj.getAge =
function () {
return this.age;
};
document.write(myObj.name);
document.write("<br/>");
document.write(myObj.age);
document.write("<br/>");
document.write(myObj.getAge());
// Output:
// Fred
// 42
// 42
Можно ознакомиться с этим в примере example-001.html
15. • Если имя свойства или метода не является простым идентификатором или
неизвестно в момент написания скрипта, то можно использовать выражение
в квадратных скобках для обозначения свойства. Перед добавлением к
объекту имена всех свойств "expando" в JavaScript преобразуются в строки.
var myObj = new Object();
// Add two expando properties that cannot be written in the
// object.property syntax.
// The first contains invalid characters (spaces), so must be
// written inside square brackets.
myObj["not a valid identifier"] = "This is the property value";
// The second expando name is a number, so it also must
// be placed inside square brackets
myObj[100] = "100";
16. Получить доступ к таким свойствам можно только используя их строковые
значения или обход по for each.
Для создания объектов можно использовать также, так называемую JavaScript
Object Notation (JSON).
Например:
var pasta = {grain: "wheat", width: 0.5, shape: "round"};
17. 1.5 Array
• В JavaScript объекты и массивы обрабатываются практически
одинаково, поскольку массивы – это специальный тип объекта.
Они оба могут содержать свойства и методы.
• В отличие от объектов, массивы имеют свойство length. При
присвоении значения к элементу массива, индекс которого
больше длины (например, myArray[100] = "hello"), свойство length
автоматически увеличивается до новой длины. Аналогично, при
уменьшении значения свойства length, любой элемент с индексом
за пределами длины массива удаляется.
18. // An array with three elements
var myArray = new Array(3);
// Add some data
myArray[0] = "Hello";
myArray[1] = 42;
myArray[2] = new Date(2000, 1, 1);
document.write("original length is: " + myArray.length);
document.write("<br/>");
// Add some expand properties
myArray.expando = "JavaScript!";
myArray["another Expando"] = "Windows";
// This will still display 3, since the two expando properties
// don't affect the length.
document.write("new length is : " + myArray.length);
// Output:
// original length is: 3
// new length is : 3
19. 1.6 null
Тип данных null имеет только одно значение в JavaScript: значение
null. Ключевое слово null невозможно использовать в качестве
имени функции или переменной.
Переменная, которая содержит null, не содержит допустимых
Number, String, Boolean, Array или Object. Можно стереть
содержимое переменной (не удаляя переменную), присваивая ей
значение null.
Обратите внимание, что в JavaScript значение null – не то же самое,
что 0 (как в C и C++). Кроме того, оператор typeof в JavaScript
определяет значения null как значения типа Object, а не типа null.
Такое поведение, которое может запутать разработчиков,
используется в целях обратной совместимости.
20. 1.7 undefined
Значение undefined возвращается при использовании свойства
объекта, которое не существует, или переменной, которая была
объявлена, но так и не получила значения.
Можно проверить, существует ли переменная, сравнивая ее с
undefined, однако можно проверить, является ли ее тип undefined,
сравнивая тип переменной со строкой "undefined". В следующем
примере показано, как определить, была ли объявлена переменная
x:
21. var x;
// This method works.
if (x == undefined) {
document.write("comparing x to undefined <br/>");
}
// This method works.
if (x == null) {
document.write("comparing x to null <br/>");
}
// This method doesn't work - you must check for the string "undefined".
if (typeof(x) == undefined) {
document.write("comparing the type of x to undefined <br/>");
}
// This method does work.
if (typeof(x) == "undefined") {
document.write("comparing the type of x to the string 'undefined'");
}
// Output:
// comparing x to undefined
// comparing x to null
// comparing the type of x to the string 'undefined'
undefined довольно странный
зверек, условно говоря, оно
обозначает что "данных нет".
Не null, а данных нет.
Понимайте как хотите.
22. 1.8 Преобразование типов
Преобразование типа можно явным образом сделать через его
название:
var test = Boolean("something") // true
23. 2. OOP in JavaScript. 2.1 Object
Объект в JavaScript – это просто коллекция пар ключ-значение (и немного
внутренней магии). Однако, в JavaScript нет концепции класса. К примеру,
объект с свойствами {name: Eugene, age: 16} не является экземпляром какого-
либо класса или класса Object. И Object, и Eugene являются экземплярами самих
себя. Они определяются непосредственно собственным поведением. Тут нет
слоя мета-данных (т.е. классов), которые говорили бы этим объектам как нужно
себя вести.
Сразу возникает вопрос: «WTF?», особенно если вы пришли из мира
классических объектно-ориентированных языков (таких как Java или C#). «Но
если каждый объект обладает собственным поведением (вместо того чтобы
наследовать его от общего класса), то если у меня 100 объектов, то им
соответствует 100 разных методов? Разве это не опасно? А как мне узнать, что,
например, объект действительно является Array-ем?»
24. Чтобы ответить на все эти вопросы необходимо забыть о классическом ОО-
подходе и начать всё с нуля. Оно того стоит.
Модель прототипного ОО приносит несколько новых динамичных и
экспрессивных путей решения старых проблем. В ней также представлены
мощные модели расширения и повторного использования кода (а это и
интересует людей, которые говорят об объектно-ориентированном
программировании). Однако, эта модель даёт меньше гарантий. Например,
нельзя полагаться, что объект x всегда будет иметь один и тот же набор свойств.
Объект в JavaScript создаётся с помощью функции Object.create. Эта функция из
родителя и опционального набора свойств создаёт новую сущность.
Так как объекты — это просто пары уникальных ключей с соответствующими
значениями – такие пары называются свойства. К примеру, вы хотите описать
несколько аспектов своего старого друга (назовём его Юджин, он же Eugene),
таких как возраст, имя и пол. Итак, академический способ:
25. var Eugene = Object.create(null)
Классика ООП:
var Eugene = new Object()
Упрощенная нотация:
var Eugene = {}
26. 2.1.1 Свойства объектов
Свойства в JavaScript являются динамическими. Это означает, что мы их можем
создавать или удалять в любое время. Свойства уникальны в том смысле, что
ключ свойства внутри объекта соответствует ровно одному значению.
В Javascript есть функция, позволяющая создат свойство объякта, правильно:
Object.defineProperty. В качестве аргументов она использует объект, имя
свойства для создания и дескриптор, описывающий семантику свойства.
Например:
Object.defineProperty(Eugene, 'name', { value: 'Eugene' , writable: true ,
configurable: true , enumerable: true })
Или упрощенно:
Eugene.name = 'Eugene'
Eugene['name'] = 'Eugene'
27. Для удаления свойства из объекта в JavaSCript предусмотрен оператор delete. К
примеру, если вы хотите удалить свойство gender из нашего объекта mikhail:
delete Eugene [‘name’]
// => true
Eugene [‘name’]
// => undefined
28. 2.1.2 Геттеры и сеттеры
Getter-ы и setter-ы обычно используются в классических объектно-ориентированных языках для
обеспечения инкапсуляции. Геттеры и сеттеры в JavaScript позволяет обеспечить proxy для
запросов на чтение и запись свойств. Поскольку понятие области видимости в JavaScript нет, то и
задач инкапсуляции геттеры/сеттеры не решают. Пример классического определения
Геттера/Сеттера:
Object.defineProperty(Illya, 'name', {
configurable: true,
enumerable: true,
get: function () {
return "My Name is: " + this._name;
},
set: function (value) {
this._name = value;
}
});
29. 2.1.3 Методы объекта
Описание действий, которые можно делать с объектом делается в JavaScript очень просто.
Почему? Да, потому, что в JavaScript нет разницы между манипулированием такими вещами,
как Function, Number, Object. Всё делается одинаково. Любой метод является просто свойством
текущего объекта типа Function, которое создается динамически и может быть использовано в
контексте этого объекта.
Пример:
var Eugene = { name: "Eugene" };
Eugene.getName = function () {
return this.name;
};
Eugene.setName = function (value) {
this.name = value;
};
30. 2.1.4 Что такое this? Магия в действии
this – это одна из самых важных переменных в JavaScript, она хранит в себе
ссылку на объект, которому принадлежит исполняющаяся функция. Это не
обязательно означает, что this всегда равно объекту, в котором функция
хранится. Нет, JavaScript не настолько эгоистичен.
Функции являются generic-ами. Т.е. в JavaScript переменная this определяет
динамическую ссылку, которая разрешается в момент исполнения функции.
Процесс динамического разрешения this обеспечивает невероятно мощный
механизм для динамизации объектной ориентированности JavaScript и
компенсирует отсутствие строгого соответствия заданным структурам (т.е.
классам). Это означает, что можно применить функцию к любому объекту,
который отвечает требованиям запуска, независимо от того, как устроен объект
31. Существует четыре различных способа разрешения this в функции, зависящие от
того, как функция вызывается:
• непосредственно, когда функция вызывается сама по себе, без объектного
контекста. Например: someFunction1(). В этом слечае this это глобальный
контекст (в браузере это объект window)
• как метод, например: Eugene.getName(), в таком случае this будет
тождественно объекту Eugene.
• явно применяется. В JavaScript любая функция может быть вызвана в
контексте некоторого объекта используя методы call или apply. Рассмотрим
пример:
32. function getObjectQName(parameter1) {
var result = "My Name is: " + this.name;
if (parameter1 != null) {
result += ", and parameter1 is: " + parameter1;
}
return result;
}
getObjectQName.call(Eugene, "PAR-1"); // this === Eugene
getObjectQName.call(Illya, "PARAMETER-1"); // this === Illya
// Bind example
Eugene.illyaNameFnc = getObjectQName.bind(Illya);
writeLog(Eugene.illyaNameFnc("PAR-1")); // this === Illya
34. 2.1.5 Литеральный синтаксис определения
объекта
Простой способ создать объект заключается в использовании литерального
синтаксиса JavaScript. Литеральный объект определяет новый объект, родитель
которого Object.prototype. Пример этого синтаксиса приведен ниже:
var Eugene = {
_name: "Eugene",
_age: 18,
get name() { return this._name; },
set name(value) { this._name = value; },
getAge: function(){ return this._age; }
};
35. 2.1.6 Наследование объектов?
• Наследование в JavaScript осуществляется через клонирование поведения
объекта и расширение его специализированным поведением. Объект,
поведение которого клонируют, называется прототипом.
• Прототип – это обычный объект, который делится своим поведением с
другими объектами – в этом случае он выступает в качестве родителя.
• Концепт клонирования поведения не означает, что вы будете иметь две
различные копии одной и той же функции или данных. На самом деле
JavaScript реализует наследование через делегирование, т.е. все свойства
хранятся в родителе, а доступ к ним расширен через ребёнка.
• Как упоминалось ранее, родитель (или [[Prototype]]) объекта определяется
вызовом Object.create с первым аргументом, ссылающимся на объект-
родитель. Рассмотрим пример:
37. JavaScript реализует делегирование доступа к свойствам, т.е. свойство ищется
через всех родителей объекта.
Эта цепь родителей определяется скрытым слотом в каждом объекте, который
называется [[Prototype]]. Вы не можете изменить его непосредственно,
существует только один способ задать ему значение – при создании нового
объекта.
Когда свойство запрашивается из объекта, движок сначала пытается получить
свойство из целевого объекта. Если свойство не найдено, то рассматривается
непосредственный родитель объекта, затем родитель родителя и т.д.
Это означает, что мы можем изменить поведение прототипа в середине
программы, то автоматически изменится поведение всех объектов, которые
были от него унаследованы.
38. 2.2 Class. 2.2.1 Классы
Итак, самая большая магия. В JavaScript классы – это функции! WTF?
Любая функция, кроме некоторых встроенных, может быть инициализирована
как объект. Для этого ее нужно вызвать через директиву new. Итак:
RiaShamans.JSExamples.Person = function (name, age, mass) {
this.name = name;
this.age = age;
this.mass = mass;
};
var Eugene = new ("Eugene", 18, 65);
39. Во время работы функции, вызванной директивой new, новосоздаваемый
объект доступен как this, так что можно проставить любые свойства.
Класс объекта определяется функцией, которая его создала. Для проверки
принадлежности классу есть оператор instanceof:
var Eugene = new RiaShamans.JSExamples.Person("Eugene", 18, 65);
writeLog(Eugene instanceof RiaShamans.JSExamples.Person);
40. 2.2.2 Пространства имен и пакеты
Поскольку классы и объекты реализуются в виде переменных (переменных-
функций и переменных-объектов), то пространства имен могут быть
реализованы в виде полей-переменных Объектов. Например:
if (typeof(RiaShamans) == "undefined") RiaShamans = {};
if (typeof(RiaShamans.JSExamples) == "undefined") RiaShamans.JSExamples = {};
Таким образом создан пакет RiaShamans.JSExamples, который может
использоваться для определения классов или объектов. Например:
41. if (typeof(RiaShamans) == "undefined") RiaShamans = {};
if (typeof(RiaShamans.JSExamples) == "undefined") RiaShamans.JSExamples = {};
/**
* Class defines Person Entity
*
* @class {RiaShamans.JSExamples.Person}
* @param name
* @param age
* @param mass
* @returns {RiaShamans.JSExamples.Person}
*/
RiaShamans.JSExamples.Person = function (name, age, mass) {
};
42. 2.2.3 Классы и прототипы. Наследование
В javascript базовое наследование основано не на классах. То есть, нет такого, что
классы наследуют друг от друга, а объект класса-потомка получает общие
свойства. Вместо этого объекты наследуют от объектов без всяких классов.
Наследование на классах можно построить(эмулировать), опираясь на базовое
наследование javascript.
Реализуется наследование через неявную(внутреннюю) ссылку одного объекта
на другой, который называется его прототипом и в спецификации обозначается
[[prototype]]. Это свойство обычно скрыто от программиста. Также существует
свойство с похожим названием prototype (без квадратных скобок) – оно
вспомогательное и указывает, откуда брать прототип при создании объекта.
Когда вы ставите функции Person свойство Person.prototype = XXX – вы этим
декларируете: "все новые объекты класса Person будут иметь прототип XXX".
Рассмотрим пример:
43. if (typeof(RiaShamans) == "undefined") RiaShamans = {};
if (typeof(RiaShamans.JSExamples) == "undefined") RiaShamans.JSExamples = {};
/**
* Class defines Person Entity
*
* @class {RiaShamans.JSExamples.Person}
* @param name
* @param age
* @param mass
* @returns {RiaShamans.JSExamples.Person}
*/
RiaShamans.JSExamples.Person = function (name, age, mass) {
this.initPerson(name, age, mass);
};
RiaShamans.JSExamples.Person.prototype.constructor = RiaShamans.JSExamples.Person;
/**
* Initialize person Data
*
* @param name
* @param age
* @param mass
*/
RiaShamans.JSExamples.Person.prototype.initPerson = function(name, age, mass){
this.name = name;
this.age = age;
this.mass = mass;
};
44. /**
* Returns Person name
*
* @returns {String}
*/
RiaShamans.JSExamples.Person.prototype.getName = function(){
return this.name;
};
/**
* Returns person Age
*
* @returns Number
*/
RiaShamans.JSExamples.Person.prototype.getAge = function(){
return this.age;
};
/**
* Class defines gendered Person Entity
*
* @class {RiaShamans.JSExamples.GenderPerson}
* @augments {RiaShamans.JSExamples.Person}
* @lends {RiaShamans.JSExamples.Person.prototype}
*/
RiaShamans.JSExamples.GenderPerson = function (name, age, mass) {
// Inherit parent constructor
RiaShamans.JSExamples.GenderPerson.superclass.constructor.call(this, name, age, mass); // this is equivalent of
RiaShamans.JSExamples.Person.call(this, name, age, mass);
this.gender = "UNKNOWN";
};
45. // Implement Inheritance
var GenderPersonPrototype = function(){};
GenderPersonPrototype.prototype = RiaShamans.JSExamples.Person.prototype; // Create prototype without parameters. We can't Instantiate Parent
without proper data in some cases.
RiaShamans.JSExamples.GenderPerson.prototype = new GenderPersonPrototype(); // Add all parent prototype data to Child class
RiaShamans.JSExamples.GenderPerson.prototype.constructor = RiaShamans.JSExamples.GenderPerson; // Set constructor
RiaShamans.JSExamples.GenderPerson.superclass = RiaShamans.JSExamples.Person.prototype; // Set superclass for Child
/**
* Returns Gender
*
* @returns {String}
*/
RiaShamans.JSExamples.GenderPerson.prototype.getGender = function(){
return this.gender;
};
/**
* Set Gender for a Person
*
* @param value
*/
RiaShamans.JSExamples.GenderPerson.prototype.setGender = function(value){
this.gender = value;
};
46. 2.2.4 Статические методы
Поскольку классы в JavaScript (они же функции), это в свою очередь объекты, то
для них можно определять поля в виде функций. Такая функция будет доступна
без инициализации объекта класса и в контексте самого класса, являясь
аналогом статического метода в классическом ООП. Рассмотрим пример:
47. /**
* Factory class. Example of static methods
*
* @class {RiaShamans.JSExamples.PersonFactory}
* @lends {RiaShamans.JSExamples.PersonFactory.prototype}
*/
RiaShamans.JSExamples.PersonFactory = function(){
};
/**
* Factory method. creates person and returns its instance
*
* @returns {RiaShamans.JSExamples.Person}
*/
RiaShamans.JSExamples.PersonFactory.createPerson = function(name, age, mass){
var result = new RiaShamans.JSExamples.Person(name, age, mass);
return result;
};