Роботландский Университет © А.А.Дуванов |
Объектно-ориентированное программирование (ООП) современный способ создания программных кодов, который пришел на смену структурному программированию. Вернее будет сказать, что ООП не заменило структурное программирование, а довело его до логического сов ершенства.
Суть структурного программирования в построении иерархического дерева решения и максимальной изоляции программного кода, выполняющего единую задачу. Структурные программисты достигают свой цели, собирая процедуры и данные, относящиеся к одной логической единице кода в отдельный файл, конструируя собственные типы данных при помощи таких средств языка, как структура (struct в СИ).
Программа Сидорова работает с прямоугольниками. Логично сконструировать новый тип данных Rectangle. Вот как это описывается в Си++:
// Документация на новый тип данных Rectangle. struct Rectangle { int a; // Ширина прямоугольника. int b; // Высота прямоугольника. }
Всякий раз, когда нужен новый прямоугольник, он определяется так:
Rectangle rec;
Эта запись очень привычна. Все выглядит так же, как и описание:
int a;
Можно присвоить переменной rec значение:
rec.a = 10; // Задали высоту прямоугольника rec. rec.b = 20; // Задали ширину прямоугольника rec.
Можно использовать значение переменной rec в выражениях:
perimeter = 2*(rec.a + rec.b); // Периметр прямоугольника.
Можно использовать новый тип данных как аргумент функции:
int function Perimeter(Rectangle x) { return 2*(x.a+x.b); }
В ООП-языках можно конструировать не только новые данные, но объединять данные с процедурами (функциями), которые их обрабатывают. В JavaScript это называется объектом (в Си++ классом).
//-- Документация на объект Rectangle. -- // Конструктор объекта. function Rectangle(a, b) { // Описание свойств объекта (его данных). this.a = a; // Ширина прямоугольника. this.b = b; // Высота прямоугольника. } //-- Конец документации на объект Rectangle.
В примере 2 описан объект Rectangle, почти так, как это
сделано в примерe 1
var rec = new Rectangle(10,20); // Создан экземпляр объекта rec.
Можно использовать значение переменной rec в выражениях:
var perimeter = 2*(rec.a + rec.b); // Периметр прямоугольника.
Можно использовать экземпляр объекта как аргумент функции:
function Perimeter(x) { return 2*(x.a+x.b); } var p = Perimeter(rec);
Добавим в построенный объект функции (методы), которые обрабатывают данные объекта.
//-- Документация объекта Rectangle. -- // Конструктор объекта. function Rectangle(a, b) { // Описание свойств объекта (его данных). this.a = a; // Ширина прямоугольника. this.b = b; // Высота прямоугольника. // Описание методов объекта (его функций). this.perimeter = _perimeter; // Ссылка на функцию _perimeter, // вычисляющую периметр. } // Описание метода perimeter. function _perimeter() { return 2*(this.a+this.b); } //---Конец документации объекта Rectangle
Теперь объект Rectangle содержит не только данные, но и функции. Можно писать такие коды:
// Создадим первый прямоугольник: var p1 = new Rectangle(10,20); // Создадим второй прямоугольник: var p2 = new Rectangle(35,70); // Найдем сумму периметров: var sum = p1.perimeter()+p2.perimeter();
Обратите внимание на строку, определяющую метод perimeter:
this.perimeter = _perimeter;
В ней определен метод с именем perimeter и указано, что реализует этот метод функция с именем _perimeter. Имена метода и функции, которая его реализует, могут быть любыми. Но для того, чтобы было меньше путаницы, рекомендуется имя функции образовывать от имени метода приставкой _.
Понятие объекта несложное, но к нему надо привыкнуть. Давайте еще раз посмотрим на пример с объектом Rectangle, описанный в основной части урока, и заглянем на кухню Ивана.
Объект в JavaScript, так же как и обычная функция, описывается ключевым словом function:
//-- Документация объекта Rectangle. -- // Конструктор объекта. function Rectangle(a,b) { // Свойства. this.width = a; // Ширина прямоугольника. this.height = b; // Высота прямоугольника. // Методы. this.square = _square; // Площадь прямоугольника. this.perimeter = _perimeter; // Периметр прямоугольника. this.radius = _radius; // Радиус описанной окружности. } // Реализация методов объекта. function _square() { return this.width * this.height; } function _perimeter() { return 2*(this.width + this.height); } function _radius() { var temp = this.width*this.width + this.height*this.height; return Math.sqrt(temp)/2; } //-- Конец документации объекта Rectangle. --
Описание
function Rectangle(a,b) { .... }
называется конструктором объекта. Сидоров использовал внутри этого описания переменные с приставкой this:
this.width = a; // Ширина прямоугольника. this.height = b; // Высота прямоугольника.
Ключевое слово this является указателем на экземпляр объекта, который будет создан при помощи конструктора. То есть оно задает смысл переменным и функциям, как свойствам и методам будущих экземпляров объекта.
Условно можно сказать, что ключевое слово this превращает в описании конструктора переменную в свойство, а ссылку на функцию в метод объекта.
Пользуйтесь простым правилом, позволяющим отличить обычную функцию от
конструктора объекта: если внутри
var r1 = new Rectangle(3,4); // Первый экземпляр прямоугольника. var r2 = new Rectangle(10,20); // Второй экземпляр прямоугольника.
По эти двум командам и документации на объект браузер создаст два экземпляра прямоугольника r1 и r2. У каждого экземпляра будут свои переменные width и height. Обратиться к ним можно так:
var x = r1.width; // x получает значение 3. var y = r2.height; // y получает значение 20.
Вы можете проверить работу объекта Rectangle в своем браузере:
![]() |
Объект Rectangle |
Захотел Сидоров создать новый объект Kvadrat. Квадрат, то есть. Но по лености своей природной, решил он не утруждать себя новыми описаниями, а использовать уже построенный объект Rectangle. Ведь квадрат это тоже прямоугольник, только квадратный!
Пусть все свойства и методы прямоугольника перейдут по наследству к квадрату, решил Сидоров. А так как серая жизнь надоела Ивану, он решил добавить для квадрата новое свойство цвет:
// Описание конструктора (a - сторона квадрата, с - его цвет). function Kvadrat(a,c) { this.parent = Rectangle; // Определили родительский объект. this.parent(a,a); // Вызвали конструктор родителя. this.color = c; // Определили свойство "цвет" }
Теперь Сидоров смело писал:
var kv = new Kvadrat(100, "красный"); // Создал квадрат! var s = kv.square(); // Нашел его площадь. var p = kv.perimeter(); // Нашел периметр квадрата. alert("Этот квадрат "+kv.color+"\nЕго площадь="+s+"\nА периметр ="+ p+"\nСторона квадрата="+kv.width);
Описанный механизм очень важен для объектно-ориентированного программирования. Он носит название наследование.
Вы можете проверить работу объекта Kvadrat в своем браузере:
![]() |
Объект Kvadrat |
Сидел однажды Иван на кухне и чесал свой бугристый затылок. Не нравится ему встроенный объект Date. Хочется Ивану, чтобы этот объект выдавал день недели по-русски. Учил Иван, конечно, в школе английский, но по-русски читать сообщения все же приятней!
И написал Иван с горя такой код:
var d = new Date(); d.mygetDay = _mygetDay; // Вернуть день недели по-русски. function _mygetDay() { var dayNames = new Array("воскресенье", "понедельник", "вторник", "среда", "четверг", "пятница", "суббота"); return dayNames[this.getDay()]; } alert(d.mygetDay());
Мама, родная! подумал Иван. Что ж это такое я натворил!
Но раз уж, написано, отступать некуда. Иван запустил скрипт и получил день недели по-русски!
Дело в том, что команда:
d.mygetDay = _mygetDay;
добавляет к методам экземпляра d новый метод с именем mygetDay и сообщает браузеру, что этот метод описан функцией _mygetDay.
Теперь экземпляр d обладает всеми свойствами и методами объекта Date и еще одним методом mygetDay, который придумал Иван.
Конечно, другой экземпляр, например,
var d1 = new Date();
ничего про особенности экземпляра d не знает, и если написать:
var x = d1.mygetDay();
то браузер выдаст сообщение об ошибке.
Давайте вспомним аналогии урока. Объект это документация на изготовление телевизора. Экземпляр объекта это отдельный телевизор, Иван приделал к своему конкретному телевизору d дополнительную ручку. При этом, конечно, у соседа телевизор d1 остался в прежнем состоянии.
![]() |
День недели по-русски |
А можно ли изменить не экземпляр объекта, а документацию на заводе? Так, чтобы с новой ручкой стали сходить с конвейера все телевизоры?
Нет проблем! В языке JavaScript есть ключевое слово prototype. Оно как раз и указывает на заводскую документацию объекта. Пишем так:
Date.prototype.mygetDay = _mygetDay; // Вернуть день недели по-русски. function _mygetDay() { var dayNames = new Array("воскресенье", "понедельник", "вторник", "среда", "четверг", "пятница", "суббота"); return dayNames[this.getDay()]; } var d = new Date(); var d1 = new Date(); alert(d.mygetDay()); alert(d1.mygetDay());
Первая строка в этом коде вводит в документацию объекта Date новый метод с именем mygetDay. Теперь все экземпляры объекта Date будут иметь этот новый метод, придуманный Иваном на кухне за кружкой крепкого чая. Правда, это будет работать только внутри того HTML-файла, в котором расположен скрипт Ивана, монтирующий новую ручку.
![]() |
День недели по-русски 2 |
Иван зачем-то создал такой объект:
//-- Документация на объект Num. -- function Num(a) { this.number = a; this.mul2 = _mul2; } function _mul2() { return this.number*2; } //-- Конец документации на объект Num. --
Объект может хранить число и умножать его на 2.
Затем Иван написал:
var x = new Num(10); var y = x.mul2(); alert(y);
Как он и ожидал, на экране появилось число 20.
Иван выпил кружку чая, и создал для объекта Num наследника:
//-- Документация на объект Numа (наследован от Num) -- function Numa(a) { this.parent = Num; // Родителем объявлен Num. this.parent(a); // Вызван конструктор родителя. this.put = _put; // Объявлен новый метод у потомка. } function _put() { alert("Исходное число="+this.number); } //-- Конец документации на объект Numа. --
Для проверки сына Иван написал:
var z = new Numa(100); z.put();
На экране появилась надпись: Исходное число=100.
Иван выпил еще чаю.
Добавлю я к объекту Num новый метод, осенило вдруг Ивана.
Он написал:
Num.prototype.mul3 = _mul3; function _mul3() { return this.number*3; }
Проверил:
var t = new Num(10); alert(t.mul3());
На экране появилось число 30.
Интересно, подумал Иван, а потомок понимает новый метод своего родителя?
Он написал:
var k = new Numa(10); alert(k.mul3());
Браузер выдал сообщение об ошибке.
Такое наследование называется статическим. При создании наследника в его документацию копируются все свойства и методы родителя, после чего родственная связь теряется. Если родитель приобретает новый метод, потомок об этом ничего не знает.
Можно создать наследника и не разрывать пуповины. Это достигается при помощи динамического наследования. Если объект создан способом динамического наследования, то копируется не документация родителя, а ссылка на нее. И если у родителя происходят изменения, это также будет касаться всех динамических наследников.
Синтаксис создания объекта с динамическим наследованием выглядит так:
//-- Документация на объект Numа (динамически наследован от Num). -- function Numa(a) { this.parent = Num; // Родителем объявлен Num. this.parent(a); // Вызван конструктор родителя. this.put = _put; // Объявлен новый метод у потомка. } Numa.prototype = new Num; // Задана динамическая связь с родителем. function _put() { alert("Исходное число="+this.number); } //--- Конец документации на объект Numа. --
![]() |
Статическое наследование Динамическое наследование |
К Ивану пришел его приятель Петр Мячиков и задал вопрос о методе sort объекта Array.
Я ничего не понял про сортировку массива, в частности, про функцию, которая задает правила сравнения двух элементов. Посмотри на последний пример, приведенный в справочнике:
метод | описание |
---|---|
sort() |
Мне не понятно, что возвращает функция Compare, если
a и b не определены в момент обращения к ней:
Иван стал объяснять.
В качестве аргумента метода sort указывается функция для сравнения двух элементов Compаre (имя для этой функции мы придумываем сами, оно может быть любым).
Запись set.sort(Compare) не является записью композиции двух функций, то есть, она не означает, что сначала работает Compare, а затем над результатом работает sort.
Указание Сompare выступает в качестве аргумента функции sort.
Мы привыкли к тому, что аргументом является переменная, константа или выражение. Оказывается, аргументом может быть и функция, вернее ссылка на функцию. Именно роль ссылки на функцию играет ее имя.
Вызов set.sort(Compare(a,b)) будет выполняться как композиция, то есть сначала работает функция Compare(a,b), а затем ее значение передается в функцию sort.
Вызов set.sort(Compare) означает совсем другое. Здесь в функцию sort передается ссылка на функцию Compare. Внутри функции sort функция Compare вызывается как обычно:
Compare(список фактических параметров).
В нашем случае функция Compare должна иметь два аргумента и своим возвращаемым значением показывать, какой из этих двух аргументов будет первым в смысле того порядка, который мы хотим задать:
Как работает метод sort?
Это неизвестно, но ясно: чтобы переставлять элементы в массиве, ему необходимо эти элементы попарно сравнивать друг с другом. Вот всякий раз, когда нужно сравнить два элемента, метод sort и обращается к функции Сompare(a,b). Если функция возвращает отрицательное число, то a ставится впереди b как меньший элемент.
Если массив состоит из чисел, и мы хотим его упорядочить по возрастанию, то в качестве функции Compare годится такой код:
function Compare(a,b) { if (a < b) return -1; else if(a == b) return 0; else return 1; }
Тот же результат можно получить короче:
function Compare(a,b) { return a-b; }
Для упорядочивания по убыванию подойдет такой код:
function Compare(a,b) { return b-a; }
Если же мы хотим оставить массив на месте, то можно написать:
function Compare(a,b) { return 0; }
Предположим, придуман такой порядок.
Число a считается меньше числа b, если у него меньше младшая цифра. Числа считаются равными, если их младшие цифры совпадают, и число a больше числа b в противном случае.
Для сортировки массива по этому правилу можно написать такую функцию сравнения:
function Compare(a,b) { if (a%10 < b%10) return -1; else if(a%10 == b%10) return 0; else return 1; }
То же самое можно задать короче:
function Compare(a,b) { return a%10 - b%10; }
Например, массив [26,71,9,1] после такой сортировки преобразуется в массив [71,1,26,9].
Еще пример. Отсортируем числа в порядке возрастания их синусов:
function Compare(a,b) { return Math.sin(a) - Math.sin(b); }
Для того же исходного набора [26,71,9,1] получим отсортированный массив [9,26,1,71].
Долго беседовал Иван с Мячиковым, но Петр не вникал. Тогда Иван решил написать свою функцию сортировки, чтобы показать Мячикову работу sort с функцией Compare изнутри.
Запустите этот скрипт и убедитесь, что он работает.
Скрипт работает, да, но Почему-то оба сообщения alert дают один и тот же результат: [1,9,26,71].
Получается, что если мы записываем массив как аргумент функции, а функция что-то с массивом делает, то исходный массив портится!
Происходит так потому, что в функцию передается не сам массив, а ссылка на него. Как исправить функцию sortRU, чтобы она не портила исходный массив?
Вопрос, несмотря на внешнюю простоту, довольно коварен. Простое решение не помогает:
function sortRU(s, Comp) { var set = new Array(); set = s; // создаем локальную копию массива? var change=true; while(change) { change = false; for(var i=0; i<set.length-1; i++) if(Comp(set[i],set[i+1])>0) { var x = set[i]; set[i] = set[i+1]; set[i+1] = x; change = true; } } return set; } |
Дело в том, что код set = s; создает копию ссылки, а не копию массива.
Для создания настоящей копии придется честно копировать все элементы массива:
function sortRU(s, Comp) { // Формируем локальную копию массива s. var set = new Array(); for(var i=s.length; --i>=0;) set[i]=s[i]; .... // Всю работу выполняем в локальной копии. ... return set; } |
Теперь массив-аргумент s испорчен не будет!
Правда, возникает новый любопытный вопрос. Когда пишем
Если функция возвращает ссылку, то это катастрофа: будет возвращена ссылка на локальный массив, который уничтожается при выходе из функции.
Однако в этом месте Javascript поступает благоразумно. По команде
Иван сказал, что приведенный выше алгоритм сортировки не оптимальный:
function sortRU(s, Comp) { var i; // Формируем локальную копию массива. var set = new Array(); for(i=s.length; --i>=0;) set[i]=s[i]; var change=true; // Были перестановки в массиве. while(change) { change = false; for(i=0; i<set.length-1; i++) if(Comp(set[i],set[i+1])>0) { // Перестановка в правильном порядке соседних элементов. var x = set[i]; set[i] = set[i+1]; set[i+1] = x; change = true; } } return set; } |
Работает он так.
В цикле while просматривается массив, и сравниваются соседние элементы. Если элементы стоят неправильно, они обмениваются местами, а переменная change устанавливается в true.
Просмотры повторяются, пока значение переменной change равно true. Прокрутим алгоритм для массива [26,71,9,1] (упорядочивание по возрастанию).
Первый просмотр: 26,71,9,1 26,9,71,1 26,9,1,71 |
Всплыл на свое место элемент-пузырек 71. |
Второй просмотр: 26,9,1,71 9,26,1,71 9,1,26,71 |
Всплыл на свое место элемент-пузырек 26. |
Третий просмотр: 9,1,26,71 1,9,26,71 1,9,26,71 |
Всплыл на свое место элемент-пузырек 9. |
Четвертый просмотр: 1,9,26,71 1,9,26,71 1,9,26,71 |
При последнем просмотре не было сделано ни одной перестановки, поэтому переменная change остается равным false, и просмотры заканчиваются.
Каждый просмотр заставляет один элемент всплывать на свое место. Именно поэтому метод и называется: метод пузырька.
Ясно, что при каждом новом просмотре длину просматриваемой части можно сокращать на 1. Понятно также, что какая-то часть массива уже может стоять в правильном порядке, и пузырьки будут всплывать на свое место, которое не обязательно отличается на 1 от предыдущего всплытия.
Значит, длину просматриваемой части в оптимальном алгоритме нужно сокращать до места всплытия последнего пузырька, что может оказаться существенно больше единицы.
Посмотрим это на примере сортировки массива [2, 5, 1, 6, 7].
Первый просмотр (смотрим пять элементов): 2, 5, 1, 6, 7 2, 1, 5, 6, 7 2, 1, 5, 6, 7 |
Всплыл на свое место элемент-пузырек 5. |
Понятно, что при следующем просмотре работать за элементом 5 уже не надо. | |
Второй просмотр (смотрим два элемента): 2, 1 1, 2 |
Всплыл на свое место элемент-пузырек 2. |
Перестановка была, и теперь надо смотреть один элемент. Но один элемент всегда в полном порядке. Значит, цикл можно закончить.
Руководствуясь этими идеями, Иван написал улучшенный алгоритм пузырька. У него получился такой скрипт:
![]() |