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

комментарии (97)