Опубликован: 23.12.2005 | Уровень: специалист | Доступ: платный | ВУЗ: Московский физико-технический институт
Лекция 6:

Классы

Можно ли создать статическое поле в прототипе

Кое-что похожее на статическое поле создать можно без особых усилий. Во всяком случае, это будет некое хранилище данных, доступных для всех экземпляров класса. Выглядеть это будет так:

_global.myClass = function(){}
_global.myClass.prototype.staticField = 5;
a = new myClass();
b = new myClass();
	// Значение статического свойства читаем как обычно
trace("a.staticField = " + a.staticField);
	// А изменяем только напрямую через __proto__
a.__proto__.staticField = 10;
trace("b.staticField = " + b.staticField);

Этот код выводит вот что:

a.staticField = 5
b.staticField = 10

Как и следовало ожидать. Однако нужно соблюдать осторожность: если мы попробуем изменить staticField напрямую через объект (например, a.staticField = 10 ), мы, разумеется, внесем изменения лишь в сам объект, а не в прототип. И другим объектам этого класса изменения будут недоступны. Можно исправить ситуацию, заведя не просто статическое поле, а свойство с функциями доступа. Получается вот что:

_global.myClass = function(){}
	// setter и getter размещаем в прототипе
_global.myClass.prototype.setStField = 
	function(val){this.__proto__.stFieldData = val;};
_global.myClass.prototype.getStField =
	function(){return this.__proto__.stFieldData;};
	
	// И прямо в прототипе создаем свойство
_global.myClass.prototype.addProperty(
	"staticField", 
	_global.myClass.prototype.getStField,
	_global.myClass.prototype.setStField
);
	// Устанавливаем начальное значение
_global.myClass.prototype.staticField = 5;
a = new myClass();
b = new myClass();
	// Значение статического свойства читаем как обычно
trace("a.staticField = " + a.staticField);
	// И записывать теперь можно тоже как обычно
a.staticField = 10;
trace("b.staticField = " + b.staticField);

На выходе получаем, как и в прошлый раз

a.staticField = 5
b.staticField = 10

К сожалению, недостатки есть и у этого варианта. Вспомним: в С++ и в Java к статической переменной можно обращаться не только через объект, но и через класс. Мы, конечно, можем написать что-то вроде MyClass.prototype.staticField = 10, но... нельзя ли, чтоб было совсем похоже на С++ или Java (особенно на Java)? Без всякого слова prototype? Можно! Добро пожаловать за этим в следующий параграф!

Способы эмуляции статических и приватных полей и методов

Этот параграф может быть пропущен при первом чтении. Заинтересует же он прежде всего тех, кто хочет непременно эмулировать на Флэше любимые элементы семантики Java и C++. А также любителей головоломок.

Статическое поле (свойство)

Усовершенствовать статические поля из предыдущего параграфа можно по двум направлениям:

  • Устроить так, чтобы все-таки можно было обращаться к статическому полю только через имя класса.
  • Спрятать получше переменную в прототипе, чтобы к ней нельзя было обратиться напрямую. (Это нужно, если ваши getter и setter делают какую-то дополнительную работу, кроме простой выдачи и установки значения; и вы хотите, чтобы весь доступ к свойству шел только через них.)

Вот три варианта кода. Один решает только первую из указанных проблем, второй решает обе (хотя делает он тривиальные getter и setter ). Наконец, третий позволяет getter и setter задавать. Во всех вариантах дополнительная функция добавляется в Function.prototype и становится доступной как метод любого объекта -функции (в том числе - конструктора класса ). Отметим также, что использованные здесь идеи принадлежат Тимоти Гролео (Timothee Groleau), его прекрасные статьи, упомянутые в списке литературы мы всячески рекомендуем. Итак, сначала самый простой вариант:

Function.prototype.addStaticProperty = function(name, propVal){
		// Устанавливаем начальное значение
	this.prototype[name] = propVal;
		// Сохраняем ссылку на нужный нам объект-функцию
		// (конструктор) в переменной, к которой будет иметь
		// доступ сгенерированная функция
	var thisVar = this;
	var getter = function() {
  	    return thisVar.prototype[name];
 	}
 	var setter = function(newVal) {
  	thisVar.prototype[name] = newVal;
 	}
		// Как сам конструктор, так и прототип должны получить
		// это свойство (ссылающееся на одни и те же данные).
	this.addProperty(name, getter, setter);
   this.prototype.addProperty(name, getter, setter);
}
	// Прячем метод от for...in, наподобие системных методов
	// В принципе, этого можно и не делать.
ASSetPropFlags(Function.prototype, "addStaticProperty", 1);

	// Тестируем
_global.SomeClass = function(){}
SomeClass.addStaticProperty("testProp", "Тестовое значение");
a = new SomeClass();
b = new SomeClass();
trace(a.testProp);
SomeClass.testProp = "Второе тестовое значение";
trace(b.testProp)
b.testProp = "Третье тестовое значение";
trace(SomeClass.testProp);

На выходе получим:

Тестовое значение
Второе тестовое значение
Третье тестовое значение

Так что статическое свойство успешно работает. Теперь давайте скроем данные, к которым обращается свойство, - поместим их не в прототип, а в контекст вызова addStaticProperty. Вот так:

Function.prototype.addStaticProperty = function(name, propVal){
		// Храним значение прямо в контексте вызова
		// addStaticProperty, прямо в аргументее ее.
	var getter = function() {
    	return propVal;
 	}
 	var setter = function(newVal) {
  	    propVal = newVal;
 	}
		// Как сам конструктор, так и прототип должны получить
		// это свойство (ссылающееся на одни и те же данные).
   this.addProperty(name, getter, setter);
   this.prototype.addProperty(name, getter, setter);
}

Весь остальной текст программки оставляем таким же; тестовый запуск дает тот же самый результат. Наконец, если мы хотим сами установить getter и setter, нам придется поступить следующим образом:

// Суффикс GS в названии функции означает "Getter, Setter"
Function.prototype.addStaticProperty_GS = function
  (name, propVal, argGetter, argSetter){
		// Храним значение прямо в контексте вызова
		// addStaticProperty, прямо в аргументее ее.
	var getter = function() {
  	  return argGetter(propVal);
 	}
 	var setter = function(newVal) {
  	  propVal = argSetter(newVal);
 	}
		// Как сам конструктор, так и прототип должны получить
		// это свойство (ссылающееся на одни и те же данные).
	this.addProperty(name, getter, setter);
    this.prototype.addProperty(name, getter, setter);
}
	// Прячем метод от for...in, наподобие системных методов
	// В принципе, этого можно и не делать.
ASSetPropFlags(Function.prototype, "addStaticProperty", 1);
	// Ссылка будет локальной, только если мы поместим
	// весь этот код в функцию. Но мы хотим подчеркнуть, 
	// что testGetter по смыслу - временная переменная.
var testGetter = function(intrinsicVar){
	return "Возвращаем: " + intrinsicVar;
}
	// Комментарий, который мы написали к testGetter, 
	// относится и сюда тоже.
var testSetter = function(newValue){
	return newValue.toUpperCase();
}
	// Тестируем
_global.SomeClass = function(){}
SomeClass.addStaticProperty_GS
  ("testProp", "Тестовое значение", testGetter, testSetter);
a = new SomeClass();
b = new SomeClass();
trace(a.testProp);
SomeClass.testProp = "Второе тестовое значение";
trace(b.testProp)
b.testProp = "Третье тестовое значение";
trace(SomeClass.testProp);

Обратите внимание, что getter в данном случае принимает аргумент: значение внутренней переменной, в которой хранятся данные; setter, в отличие от обычного поведения, возвращает значение, которое будет этой внутренней переменной присвоено.

Запускаем код на выполнение и получаем:

Возвращаем: Тестовое значение
Возвращаем: ВТОРОЕ ТЕСТОВОЕ ЗНАЧЕНИЕ
Возвращаем: ТРЕТЬЕ ТЕСТОВОЕ ЗНАЧЕНИЕ

Забавный эффект: начальное значение свойства не прошло через функцию- setter и поэтому не приведено к верхнему регистру. Если вы считаете, что подобное поведение провоцирует ошибки (а не "дает некоторую гибкость", например), вы легко можете это исправить, вставив в конце функции addStaticProperty_GS строчку propVal = argSetter(propVal).

Анатолий Федоров
Анатолий Федоров
Россия, Москва, Московский государственный университет им. М. В. Ломоносова, 1989