ООП в JavaScript - правильное объявление классов, методов и свойств
Когда-нибудь каждый JavaScript-программист осознаёт, что JS — объектно-ориентированный язык, и от функционального программирования пытается перейти к ООП. И здесь его подстерегают некоторые опасности, происходящие от непонимания того факта, что JS — язык не классов (как Паскаль или Цэ-два-креста), а прототипов.
Так, уже многое написано о проблеме наследования (которого в JS нет). Я же постараюсь рассказать о менее освещённом, но едва ли не более важном подводном камне: грамотной реализации методов.
Программисты пытаются объявлять классы в привычной для них форме, из-за чего возникают утечки памяти и прочие неприятные вещи. На самом деле нужно всего лишь научиться использовать прототипы.
Эта статья предназначена прежде всего для начинающих JS-программистов.
Ниже я буду использовать понятие «класс» в том смысле, в каком оно понимается в Паскале или Цэ-двух-крестах; хоть в JS таких классов, вообще говоря, нет, однако кое-что весьма сходно по форме и смыслу.
С самого начала всем становятся известны две базовые вещи:
- класс описывается функцией-конструктором;
- методы являются свойствами-функциями.
Поэтому программисты начинают писать весьма естественно:
-
function Test(){
-
// объявляем и инициализируем свойства
-
this.x=5;
-
this.y=3;
-
// объявляем методы
-
this.sum=function(){
-
return this.x+this.y;
-
}
-
// выполняем иные конструктивные действия
-
alert("Constructor: x="+this.x+", y="+this.y+", sum="+this.sum());
-
}
После этого вроде бы получается то, что мы и хотели: получается класс Test с двумя свойствами x (изначально 5) и y (изначально 3) и методом sum, вычисляющим сумму x и y. При конструировании выводится элёт с иксом, игреком и суммой.
Но что происходит на самом деле? При конструировании объекта Test каждый раз вызывается функция Test. И каждый раз она создаёт новую анонимную функцию и присваивает её свойству sum! В результате в каждом объекте создаётся свой, отдельный метод sum. Если мы создадим сто объектов Test — получим где-то в памяти сто функций sum.
Очевидно, так делать нельзя. И важно это осознать как можно скорее.
После понимания этого факта начинающие программисты часто поступают следующим образом: создают отдельно функцию sum, а в конструкторе её присваивают свойству:
-
function Test(){
-
// объявляем и инициализируем свойства
-
this.x=5;
-
this.y=3;
-
// прикручиваем методы
-
this.sum=Test_sum;
-
// выполняем иные конструктивные действия
-
alert("Constructor: x="+this.x+", y="+this.y+", sum="+this.sum());
-
}
-
-
// реализуем методы
-
function Test_sum(){
-
return this.x+this.y;
-
}
В результате, действительно, функция Test_sum создаётся только один раз, а при каждом конструировании нового объекта Test создаётся только ссылка sum.
В то же время это малограмотный вариант. Всё можно сделать гораздо красивее и правильнее, используя самую основу JavaScript - прототипы:
-
function Test(){
-
// объявляем и инициализируем свойства
-
this.x=5;
-
this.y=3;
-
// выполняем иные конструктивные действия
-
alert("Constructor: x="+this.x+", y="+this.y+", sum="+this.sum());
-
}
-
-
// объявляем методы
-
Test.prototype.sum=function(){
-
return this.x+this.y;
-
}
Мы создаём свойство sum не класса Test, а его прототипа. Поэтому у каждого объекта Test будет функция sum. Собственно, на то он и прототип, чтобы описывать вещи, которые есть у каждого объекта. Более того, обычные, не функциональные, свойства тоже было бы логично загнать в прототип:
-
function Test(){
-
// выполняем иные конструктивные действия
-
alert("Constructor: x="+this.x+", y="+this.y+", sum="+this.sum());
-
}
-
-
// объявляем, инициализируем, реализуем свойства и методы
-
Test.prototype.x=5;
-
Test.prototype.y=3;
-
Test.prototype.sum=function(){
-
return this.x+this.y;
-
}
Плохо здесь то, что объявления свойств и методов идут после их использования в конструкторе. Но с этим придётся смириться…
Ещё здесь неприятно многократное повторение Test.prototype. К счастью, JS — это не Цэ-два-креста, и у нас есть замечательное предложение this. Впрочем, нас подстерегает неприятный сюрприз: нижеследующий вариант не работает.
-
function Test(){
-
// выполняем иные конструктивные действия
-
alert("Constructor: x="+this.x+", y="+this.y+", sum="+this.sum());
-
}
-
-
// объявляем, инициализируем, реализуем свойства и методы
-
with(Test.prototype){
-
x=5;
-
y=3;
-
sum=function(){
-
return this.x+this.y;
-
}
-
}
Почему не работает — загадка. Как ни крути, а слово prototype придётся повторять:
-
function Test(){
-
// выполняем иные конструктивные действия
-
alert("Constructor: x="+this.x+", y="+this.y+", sum="+this.sum());
-
}
-
-
// объявляем, инициализируем, реализуем свойства и методы
-
with(Test){
-
prototype.x=5;
-
prototype.y=3;
-
prototype.sum=function(){
-
return this.x+this.y;
-
}
-
}
Явное преимущество здесь в группировании объявлений всей начинки класса Test в один блок — за исключением остающегося осторонь конструктора. Но и с этим можно справиться, если вспомнить, что функцию можно объявить через минимум три синтаксиса:
-
with(Test=function(){
-
// выполняем иные конструктивные действия
-
alert("Constructor: x="+this.x+", y="+this.y+", sum="+this.sum());
-
}){
-
// объявляем и инициализируем свойства
-
prototype.x=5;
-
prototype.y=3;
-
// объявляем методы
-
prototype.sum=function(){
-
return this.x+this.y;
-
}
-
}
В результате получается почти та естественная запись, с которой мы начали, разве что слово this заменили на prototype; ну и переместили в начало «иные конструктивные действия» — как я уже сказал, с этим, к сожалению, придётся смириться.
Впрочем, если от конструктора ничего, кроме создания свойств и методов, не требуется, получается и вовсе красота:
-
with(Test=new Function){
-
// объявляем и инициализируем свойства
-
prototype.x=5;
-
prototype.y=3;
-
// объявляем методы
-
prototype.sum=function(){
-
return this.x+this.y;
-
}
-
}
Как видим, приложив лишь небольшие усилия по переосмыслению основ ООП в JS, можно научиться грамотно объявлять классы.
Источник статьи: Хабрахабр / Web-разработка


July 2nd, 2007 at 14:45 Quote
Ммм, полезно и вовремя!
August 14th, 2007 at 23:15 Quote
так помоему тоже ничего :)
Test.prototype={
x:5,
y:3,
sum:function(){
return this.x+this.y;
}
}
September 11th, 2008 at 15:10 Quote
Вот здесь еще один интересный подход к ООП в JavaScript: http://ajaxoop.org/