В восьмой
лекции были рассмотрены основы
объектно-ориентированного программирования в
JavaScript. В
данной лекции эта тема будет продолжена
рассмотрением методов наследования, а также
полезных (и опасных) свойств замыкания.
Наследование
В предыдущей
лекции была создана функция
'Cat'
:
function
Cat(name){
this.name = name;
}
Cat.prototype = {
species: 'Cat',
talk: function(){ alert('Meow!'); },
callOver: function(){ alert(this.name+' ignores you'); },
pet: function(){ alert('Pet!'); }
}
Теперь можно
создать любое количество котов, но как быть,
если мы захотим создать объект другого типа,
например, собаку? В этом случае понадобится
создать совершенно новую функцию, со своими
собственными прототипами. Если два объекта
используют одни и те же функции (например, можно
было бы добавить функции
sleep (спать), eat
(есть), и play (играть)),
то в результате мы бы имели чрезмерное
дублирование кода. Решением является концепция
наследования.
По сути
наследование позволяет определить объекты "предки"
и "потомки". Потомок наследует все свойства
своего предка. Можно было создать, например,
функцию "Animal",
"Pet" или "Mammal".
Обе функции Cat и
Dog обладали бы
многими свойствами функции предка
Animal, и нам
пришлось бы писать этот код один раз.
Проблема в
том, что JavaScript
не имеет в действительности встроенного
механизма наследования, поэтому эту
функциональность необходимо создавать
самостоятельно. Для этого существует несколько
различных способов. Один из них состоит в
использовании функции "call".
Эта функция позволяет вызывать одну функцию из
контекста другой, т.е. мы можем определить, как
действует ключевое слово "this".
С помощью "call"
можно написать класс
Animal (Животное), а затем вызвать его из
класса Cat или
Dog.
function Animal(name){
this.name = name;
this.species = 'Animal';
this.sleep = function(){ alert(this.name+' спит: Хрррр'); }
}
function Cat(name){
Animal.call(this, name);
this.talk = function(){ alert('Мяу!'); }
}
function Dog(name){
Animal.call(this, name);
this.talk = function(){ alert('Гав!'); }
}
var sam = new Cat('Sam');
var joe = new Dog('Joe');
sam.sleep(); // Sam спит: Хрррр
joe.sleep(); // Joe спит: Хрррр
sam.talk(); // Мяу!
joe.talk(); // Гав!
Хотя это
работает, мы немного ограничены в своих
возможностях. Например, прототипирование не
действует при использовании этого метода: все
прототипы, заданные на
Animal, не будут переноситься в функции
Cat или
Dog. Как мы знаем
из предыдущей лекции, определенные внутренне с
помощью "this."
функции создают новый экземпляр всякий раз при
создании новой копии предка. В этом случае
всякий раз при создании функции
Animal,
Cat или
Dog появляется
новая копия функций
species и sleep.
Как можно догадаться, это не самый эффективный
способ.
Лучшим
подходом является прототипирование всего
родительского класса на классе-потомке. Это
предоставляет доступ ко всем свойствам и методам
класса предка:
function Animal(name){
this.name = name;
}
Animal.prototype = {
species: 'Animal',
sleep : function(){ alert(this.name+' спит: Хрррр'); }
}
function Cat(name){
Animal.apply(this, arguments);
}
Cat.prototype
= new Animal;
Cat.prototype.species = 'Cat';
Cat.prototype.talk =
function(){ alert('Мяу!'); }
function Dog(name){
Animal.apply(this, arguments);
}
Dog.prototype
= new Animal;
Dog.prototype.talk =
function(){ alert('Гав!'); }
var sam = new Cat('Sam');
var joe = new Dog('Joe');
sam.sleep(); //
Sam спит : Хрррр
joe.sleep(); // Joe спит: Хрррр
alert(sam.species); // Cat
alert(joe.species); // Animal - для Dog
функция species не определена
Можно
продолжить это дальше и создать отдельные
функции для различных пород собак или кошек и
т.д.
Замыкание
Замыкание (сlosure)
является одним из наиболее мощных средств
JavaScript. Если
воспользоваться простым объяснением, то
замыкание связывает внутренние и внешние
переменные в функции. Почему это так важно?
Потому что замыкание позволяет эмулировать почти
любое свойство любого языка программирования,
даже если оно не существует в
JavaScript. Это
звучит немного непонятно, поэтому лучше начать с
более простого примера:
function beginAdding(a){
a *= 5;
return function finishAdding(b){ alert(a+b); }
}
var add = beginAdding(10);
add(20); // 70
Можно видеть,
что в приведенном коде переменной
'a' присваивается
значение 10 и
передается в функцию
beginAdding, в то время как переменной
'b' присваивается
значение 20 и
передается в функцию
finishAdding.
А что
содержится в переменной
'add'? Она содержит функцию
finishAdding с
копией связанной с ней всей функции
beginAdding. Копия
переменной 'a' из
beginAdding
сохраняется в памяти для дальнейшего
использования.
Теперь, имея
представление о замыкании, прежде чем продолжать,
необходимо обсудить проблему утечки памяти.
Internet Explorer,
в частности, подвержен достаточно неприятным
утечкам памяти при использовании замыкания.
Чтобы понять, почему необходимо знать о сборке
мусора в Internet
Explorer, посмотрим, как в этом браузере
выполняется очистка памяти от ненужных объектов.
Internet Explorer
имеет два отдельных сборщика мусора: один для
JavaScript и
другой для объектов DOM.
При выгрузке страницы браузер просматривает и
удаляет весь код
JavaScript и все объекты
DOM со страницы.
Утечка происходит, когда имеются циклические
ссылки из объекта DOM
в JavaScript и
снова на объект DOM
или из
JavaScript-->Dom-->Javascript.
Internet Explorer
запутывается и не удаляет объекты при
циклической ссылке.
var someInput
= document.getElementById('inputbox');
var someFunction = function(){
alert(someInput.value);
}
someInput.onclick = someFunction;
Здесь
представлен бесконечный цикл в терминах
замыканий. Объект DOM
someInput вовлечен
в замыкание с функцией
someFunction и наоборот. Браузер не может
определить, что удалить в первую очередь,
поэтому в результате имеем утечку памяти.
Замыкания
создают, часто даже не осознавая этого. Возьмем,
например, простую функцию:
function Animal(name){
this.sleep = function(){ alert(name+' спит: Хрррр'); }
}
Можно не
заметить этого, но переменная
'name' в функции
sleep приходит из
родительской функции
Animal. Это создает замыкание.
Даже
определение простейшей функции может создавать
замыкание:
var x = 5;
var n = function(){
y=10;
return y;
}
Здесь также
создается замыкание, хотя на первый взгляд это
не так. Почему? Когда мы создаем функцию, она
получает доступ ко всем переменным в своей
текущей области действия, поэтому мы создаем
новую ссылку на переменную
х.
Теперь мы
подошли к понятию области действия. Каждая
функция имеет свою область действия, в которой
она выполняется. Все области действия кода
сохраняются во внутреннем стеке памяти. Когда
создается замыкание, оно получает доступ к одной
из этих областей действия. Если создается
несколько замыканий в одной и той же области
действия, то каждое замыкание будет в
действительности указывать на одинаковые копии
каждой переменной области действия. Например:
var x
= 5;
var alertX1 = function(){ alert(x); }
x
= 10;
var alertX2 = function(){ alert(x); }
alertX1();
alertX2();
Обе функции
в этом случае выведут 10.
Почему? Потому что они обе указывают на одну и
ту же копию х.
Если
изменить x в любой
точке, то обе функции отразят это. Как это
исправить? Проще всего изменить область действия
замыкания:
function makeClosure(x){
return function(){ alert(x); }
}
var x =
5;
var alertX1 = makeClosure(x);
x
= 10;
var alertX2 = makeClosure(x);
alertX1(); // 5
alertX2(); // 10
Это решает
проблему. Если, однако, этот код создавал утечку
памяти, то утечка будет существенно больше, чем
в предыдущем примере; и также используется
больший объем памяти. Оказывается, что в
действительности имеется три различных области
действия в этом простом примере:
Можно видеть,
что переменная х копируется в каждую из двух
областей действия. Это связано с тем, что
x является строкой
или числом. JavaScript всегда передает строки и
числа по значению - то есть всегда делается
копия переменной. С объектами все происходит
иначе. Если х
является функцией, массивом или базовым объектом,
то в этом случае ссылка в двух наших функциях
происходит на одну и ту же копию
x и поэтому в
результате выводимое сообщение будет одинаковым:
function makeClosure(x){
return function(){ alert(x.val); }
}
var x =
{val:5};
var alertX1 = makeClosure(x);
x.val =
10;
var alertX2 = makeClosure(x);
alertX1(); // 10
alertX2(); // 10
Отличие
состоит в том, что x
теперь является объектом. Каждая из
дополнительных областей действия указывает на
одну и ту же копию x.
При изменении значения x
оно изменяется в каждой области действия, в
которой х
упоминается.
Как
избежать утечки памяти при использовании
замыканий? Необходимо избегать использования
циклических ссылок. Наиболее распространенной
причиной утечки памяти является присоединение
событий, таких, как событие
onclick, к
объектам DOM.
Часто
совершенно невинный с виду код будет создавать
утечку и ее бывает крайне сложно обнаружить. К
счастью, протестировать наличие утечки памяти
достаточно легко. Если при каждом обновлении
страницы используемая память увеличивается, то
приложение имеет утечку. Отследить, где это
происходит, совершенно другая задача, но по
крайней мере теперь известно о наличии проблемы!
Следующая
лекция будет посвящен основам приложений
AJAX!
|