четверг, 23 апреля 2009 г.

JavaScript: Выяснение имени класса (конструктора)

В статье "JavaScript: используйте arguments.callee вместо названия функции" я уже обращал ваше внимание на то, что в JavaScript в общем случае нельзя полагаться на то, что имена функций будут соответствовать тем функциям, которым вы их присвоили. Вещь это настолько важная, что не грех будет кратко повторить суть рассуждений.

В JavaScript функция является одним из типов данных, специфика которого заключается в том, что к нему можно обращаться, вызывая его как функцию и как конструктор. Но если это тип данных, то, что бы с ним работать, нам нужно получить ссылку на него, которая может содержатся в какой-то переменной. Это и происходит при объявлении функции, по сути имя функции - это переменная, которой присваивается значение - функция. В этом смысле следующие две строчки эквивалентны:
function x(){}
var x = function(){}
Между этими строками есть небольшое отличие, но в данном случае оно не существенно. Так вот, само название ПЕРЕМЕННАЯ говорит нам о том, что она может менять своё значение, по-этому опираться на то, что эта переменная будет всегда ссылаться на один раз присвоенную ей функцию, нельзя. Следующий пример это демонстрирует:
function x(){};
//...
x = 5;
//...
x(); //Error!
Но что же делать, если про функцию всё-таки нужно узнать её имя? Например, у нас может быть написан следующий код:
function x1(){}
var x2;
if (x1) x2 = x1;
x1 = 5;
В результате ссылка на функцию есть, но как её найти, имея лишь саму функцию - не понятно.
В некотором частном случае, а именно если объявление функции и манипуляции с ней происходили в глобальном контексте (глобальной области видимости переменных), выяснение имени возможно.

Здесь следует сказать пару слов о глобальном контексте. В JavaScript определён объект Global, доступный по ссылке this. Это означает, что если мы объявляем переменную не внутри какой-либо функции, а просто в сценарии, то она автоматически становится свойством объекта Global и к ней можно обращаться не только по имени, но и как к свойству этого объекта:
var x = 5;
alert(this.x); // '5'
В частном, но наиболее часто встречающемся случае клиентского JavaScript (client-side JavaScript), т.е. расширения стандарта ECMAScript в браузерах моделью DOM, нам доступна ссылка window, укзывающая на объект Window, который является расширением объекта Global для браузерной среды, и в нём, соответственно, так же можно, обращаться к его свойствам как к переменным, однако в общем случае использование ссылки window нельзя назвать корректным, по крайней мере лично мне хотелоось бы писать решения, которые работали бы, кроме клиентского JavaScript, ещё и, скажем, в Java-программах, использующих Scripting API, поддержка которого есть в JDK6.

Со ссылкой же на объект Global есть некоторая сложность, связанная с тем, что она по сути и доступна будет лишь в глобальном контексте и в функциях, которые не являются свойствами различных объектов, поскольку в других местах ссылка this будет ссылаться на другие объекты. Что бы решить эту проблему, предлагаю ввести специальную переменную global, которая будет ссылаться на глобальный контекст:
var /** @type {Global} */ global = this;
Теперь нам будет проще написать нужную функцию, но куда её лучше всего разместить? Мне представляется логичным помещение её в Function.prototype, поскольку тогда она станет доступна у всех функций (т.к. любая функция представляет собой экземпляр объекта Function и ссылается по-этому своей ссылкой __proto__ на объект Function.prototype).

Итак, какой код получаем в результате:
var /** @type {Global} */ global = this;

//...

/**
* Функция, возвращающая имя функции this, если оно присутствует в глобальном
* контексте.
*
* @return {string} имя функии, доступное по ссылке this или null, если имя не
* найдено.
*/
Function.prototype.getName = function() {

    for (var /** @type {string} */ i in global)
        if (global[i] === this)
            return i;

    return null;
}

//...


//Тестируем:

//Объект узнаёт имя своего конструктора:
function x1(){
    this.showMyName = function() {
        alert('My constructor name is ' + this.constructor.getName());
    }
}
y = new x1;
y.showMyName(); // 'My constructor name is x1'

var x2;
if (x1) x2 = x1;
x1 = 5;
y.showMyName(); // 'My constructor name is x2'

// Функция узнаёт своё имя:
function x3(){ alert('My name is ' + arguments.callee.getName()); }
x3(); // 'My name is x3'

// Узнаём у функции её имя:
function x4(){}
alert(x4.getName()); // 'x4'
Таким образом, функция, если ссылка на неё присутствует в глобальном контексте, всегда сможет узнать своё имя.

Update: К сожалению, в последней на сегодняшний день версии Internet Explorer`а - 8 (тестировал на версии 8.0.7600.16385) - всем переменным глобального пространства имён и, соответственно, функциям, автоматически присваивается модификатор {Don`t Enum}, из-за чего данный метод в этом браузере не работает... :(

3 комментария:

dyn комментирует...

А как быть если в конструкторе нужно узнать имя создаваемого объекта? Например:
var name = myObject();
Хочу знать имя name в функции конструкторе myObject(). перебор в цикле свойств объекта window в ИЕ выдает только стандартные объекты и обработчики (в остальных браузерах работает). Причем если вызвать window['name'] то все нормально - получим ссылку на name, а в цикле никак(имеется в виду ИЕ). Работает также если объявить через
this.name = new myObject() даже в локальных областях видимости. Вобщем очень хочется знать name в конструкторе :) как быть?

dyn комментирует...

опечатался :)
var name = new myObject();

C'est la vie комментирует...

Такого способа я не знаю. Вообще-то, согласно правилам семантики языка, в выражении var name = new myObject(); сначала происходит создание переменной name типа undefined, затем вычисляется правая часть, т.е. создаётся новый объект и вызывается для него конструктор, и только потом результат этого выражения присваивается переменной name. Т.е. на этапе работы конструктора нельзя понять, какой из undefined-переменных будет присвоен результат этой работы. К тому же мы не можем быть полностью уверены, что результат работы конструктора вообще попадёт в переменную в глобальной области видимости - с таким же успехом это может быть поле какого-либо объекта или инкапсулированная в каком-либо замыкании переменная...
Мне кажется, если задача стоит так, лучше использовать другой подход - инициалилизацию объекта, когда он уже создан, например - и вынести зависимуж от имени переменной логику в него.