понедельник, 5 января 2009 г.

JavaScript: используйте arguments.callee вместо названия функции

Часто разработчики, когда им нужно изнутри функции сослаться на саму эту функцию, используют её имя, заявленное при её объявлении. Например, так:
var factorial = function(x){
    return x == 1 ? 1 : x + factorial(--x);
}
В данном коде мы, фактически, обращаемся к внешней цепочке областей видимости и читаем из неё переменную "factorial" и, надеясь, что это - функция, вызываем её.

Проблема заключается в том, что JavaScript - язык, в высшей степени чреватый конфликтами имён в случае, если над разработкой одного и того же функционала работает не один, а несколько человек. И в этой ситуации нельзя быть до конца уверенным, что переменной "factorial" в какой-то момент кто-нибудь не решит присвоить другую функцию или даже какое-нибудь значение типа числа, при этом функция может быть, скажем, записана в другую переменную и по этой причине разработчик, использующий эту функцию, может думать, что всё впорядке и функция должна работать:
var fac = factorial;
factorial = 27;
alert(fac(factorial));

Но функция выдаст ошибку:
factorial is not a function
factorial(26)test.js (line 2)
Просто нужно помнить, что функции в JavaScript - это такой особый тип данных, а не неизменяемая часть программы, как, например, в Java. Функцию можно присваивать любой переменной, свойству объекта или элементу массива, жонглируя ей как угодно. Эта возможность фактически означает, что у функций в JavaScript нет своих постоянных имён! Поэтому для того, что бы не лишать программистов этой возможности, лучше всегда изнутри функции ссылаться на неё с помощью встроенного и поддерживаемого свойства псевдо-массива Arguments, называемого "callee". Это свойство всегда ссылается на функцию, которая выполняется в данный момент, поэтому вы оказываетесь застрахованы от недоразумений, продемонстрированных выше:
function factorial(x) {
    return x == 1 ? 1 : x + arguments.callee(--x);
}
var fac = factorial;
factorial = 27;
alert(fac(factorial)); // Возвращает "378".
Но в JavaScript всё-таки есть способ элегантно обойти данную ситуацию. Если вам не очень нравится прибегать к вызову свойства callee объекта Arguments, то можно воспользоваться конструкцией, называемой "литералом функции" с необязательным параметром, содержащим имя функции:
var factorial = function f(x) {
    return x == 1 ? 1 : x + f(--x);
};
alert(f); // Ошибка: "f is not defined"
В этом коде не создаётся внешней переменной "f", здесь функция, как и в предыдущем примере, присваивается только внешней переменной "factorial", а переменная "f" определяется только внутри области видимости функции и служит для ссылки на данную функцию, так же, как и "arguments.callee", так что можно без опаски использовать переменную "f" для своих нужд - на работу данной функции это никак не повлияет.

Минус этого элегантного приёма состоит в том, что такие трюки, к сожалению, увеличивают требования к разработчикам, поддерживающим код. За JavaScript незаслуженно закрепилась репутация простого и чуть ли не детского языка программирования и поэтому когда разработчики сталкиваются с его нетривиальными приёмами, то скорее склонны раздражаться на того разработчика, который их использовал, чем менять давно и прочно сложившийся стереотип и глубже изучать этот интересный язык.

6 комментариев:

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

Это поразительно, javascript не перестаёт удивлять!

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

Эт точно! :)))

Юрий Дидковский комментирует...

В данной статье ошибочное мнение о имени функционального выражениия (function expression):

var factorial = function(){
...
}

в данном случае в переменную factorial заносится анонимная функция, которая должна по-хорошему быть рекурсивно вызвана через arguments.callee. А так как arguments.callee является нежелательной конструкцией, то можно переписать так:

(function(){
var factorial = function(n){
... factorial(n);
}

})();
В этом случае внешние переменные не страшны, а если ещё немного дописать - получится неплохой модуль, который можно вызывать извне ;-)
За напоминание как вычисляется факториал - автору спасибо.

Юрий Дидковский комментирует...

А по поводу матчасти и "псевдо-массива" arguments - незачет:

Объект активации (Activation object, сокращённо AO) — специальный объект, который создаётся при входе в контекст функции и инициализируется свойством arguments — Объект аргументов (Arguments object):

AO = {
arguments:
};

Объект аргументов (Arguments object, сокращённо ArgO) – объект, находящийся в объекте активации контекста функции и содержащий следующие свойства:
◦callee – ссылка на выполняемую функцию;
◦length – количество реально переданных параметров;
◦свойства-индексы (числовые, приведённые к строке), значения которых – есть формальные параметры функции (слева направо в списке параметров). Количество этих свойств-индексов == arguments.length. Значения свойств-индексов объекта arguments и присутствующие формальные параметры – взаимозаменяемы

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

Оппа! К сожалению, не настроил уведомление о комментариях, так что увидел коммент только сейчас. Но, если ещё актуально, то можно прямо сейчас открыть консоль и проверить, как именно работает function expression:
var factorial = function() {
return arguments.callee === factorial;
}
factorial();
У меня в Яндекс.Браузере (что на WebKit`е и V8) вывел однозначное true, так что никакой анонимной функции там нет - присваивается прямо исходная функция.

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

@Yury, по поводу вашего второго комментария не понятно - что Вы имеете в виду? Вам не нравится термин? "Псевдомассивом" ("pseudo-array") arguments называется повсеместно, в т.ч. в наиболее популярной и глубокой книжке по JavaScript - "JavaScript. The Definitive Guide" Флэнагана (см., например, разд. 7.11 последнего на сегодняшний момент 6-го изд.) - эта O'Reilly`вская книга с носорогом на обложке настолько популярна, что в честь неё даже назвали 2 реализации JavaScript-engine`а для платформы Java - Rino и Nashorn (тоже "носорог", только на немецком). В других местах этой книги и в других источниках я встречал термин "объект, подобный массиву", но это, хотя и более понятно, но, на мой взгляд, излишне-многословно, так что я предпочитаю называть его псевдомассивом. Приставка "псевдо" конкретно означает, что объект этого условного конструктора Arguments, не является массивом потому, что не имеет ссылки "__proto__" (или, как спецификация ей называет, [[Prototype]]) на Array.prototype и, в связи с этим, не может дёргать для него методы массивов, не проведя с ним определённых преобразований (типа Array.prototype.slice.call(arguments, 0)). Но кроме этого, этот объект ведёт себя во всём остальном как массив - у него есть индексированные параметры и свойство length. Да, у этого объекта есть ещё и свойство callee, но это дополнительная штука, запрещённая, к тому же, в наиболее популярном на сегодняшний день строгом режиме (strict-mode). И по полиморфизму оно не делает его НЕ псевдо-массивом - можно считать его просто потомком с доп. свойством.