2 July 2007

Методы в JavaScript

Website development
Когда-нибудь каждый JavaScript-программист осознаёт, что JS — объектно-ориентированный язык. И здесь его подстерегают некоторые опасности, происходящие от непонимания того факта, что JS — язык не классов (как Паскаль или Цэ-два-креста), а прототипов.
Так, уже многое написано о проблеме наследования (котого в JS нет). Я же постараюсь рассказать о менее освещённом, но едва ли не более важном подводном камне: грамотной реализации методов.
Программисты пытаются объявлять классы в привычной для них форме, из-за чего возникают утечки памяти и прочие неприятные вещи. На самом деле нужно всего лишь научиться использовать прототипы.
Эта статья предназначена прежде всего для начинающих JS-программистов.

Ниже я буду использовать понятие «класс» в том смысле, в каком оно понимается в Паскале или Цэ-двух-крестах; хоть в JS таких классов, вообще говоря, нет, однако кое-что весьма сходно по форме и смыслу.
С самого начала всем становятся известны две базовые вещи:
  1. класс описывается функцией-конструктором;
  2. методы являются свойствами-функциями.

Поэтому программисты начинают писать весьма естественно:
	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 — это не Цэ-два-креста, и у нас есть предложение with. С другой стороны, многие авторитетные люди не рекомендуют использовать with вообще. Поэтому нижеследующие варианты использовать не следует.
Буквально сразу же нас подстерегает неприятный сюрприз: этот код не работает.
	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;
		}
	}

Однако не будем забывать, что предложение with использовать не рекомендуется. Поэтому в итоге остановимся на третьем варианте объявления.
Tags:JavaScriptпрограммированиеООП
Hubs: Website development
+32
26.7k 98
Comments 97
Top of the last 24 hours