В данной заметке исследуется приём функционального программирования, который я называю динамическим определением функции. Не раз и не два я убеждался в оправданности существования этого приёма, особенно когда речь шла он написании кросс-браузерных и оптимизированных по времени выполнения библиотек.
Постановка задачи. Проблема разогрева.
Написать такую функцию foo, чтобы она возвращала объект Date, содержащий время первого вызова той же самой функции.
Решение 01. Допотопное
Самое простое решение - использовать глобальную переменную t, содержащую объект Data. При самом первом вызове foo мы записываем время в t, при вызовах последующих попросту возвращаем значение t:
var t;
function foo() {
if (t) {
return t;
}
t = new Date();
return t;
}
К таком коду две серьёзные претензии можно предявить. Во-первых, t - это глобальная переменная, и значение её, стало быть, можеть быть измененно между вызовами foo. Во-вторых, такой код неоптимален - мы вычисляем некоторое условие при каждом вызове foo. В данном конкретном примере вычисление условия не обходится нам дорого, но в практические приложения зачастую содержат несколько затратных условий в условных управляющих структурах.
Решение 02. Приём создания собственного пространства имён (модульный приём)
Смягчить острые недостатки решения 01 нам может помочь приём создания собственного пространства имён - приписываемый Корнфорду или Крокфорду. Мы можем использоваеть замыкание, чтобы переменную t видела только наша функция foo:
var foo = (function() {
var t;
return function() {
if (t) {
return t;
}
t = new Date();
return t;
}
})();
От необходимости каждый раз прогонять t через if мы, тем не менее, не ушли. Замыкание - вещь мощная, но в данном случае его использование мне представляется не оправданным.
Функции как объекты
Осознавая в полной мере тот факт, что функции в javascript - это объекты и могут обладать свойствами, мы можем добиться решения того же качества, что и с решение с замыканием, но, в данном случае, как мне кажется, концептуально более простого:
function foo() {
if (foo.t) {
return foo.t;
}
foo.t = new Date();
return foo.t;
}
От глобальных переменных мы окончательно ушли, а вот с условием никак порвать не можем. Наконец, парарарам, на арене появляется
Решение 04. Динамическое определение функции
var foo = function() {
var t = new Date();
foo = function() {
return t;
};
return foo();
};
При первом вызове foo мы создаём новый объект Date и - внимание, финт ушами - переприсваеваем foo новую функцию, содержащую значение t в собственном теле. Затем эта результата вызова это новой функции возвращается в старой. Все последующие вызовы foo попросту возвращают содержащееся в теле foo значение t - без всяких условных конструкций, которые, напомню, могут быть очень и очень затратными.
Данный приём мы ещё можем вот так воспринимать: внешняя функция, сначала присовенная foo - это своего рода обещание. Она, эта функция, как бы обещает, что при первом вызове она переопределит foo как нечто более полезное. Термин “обещание” неточно происходит из механизма ленивых вычислений Схемы. Каждый яваскритпер просто должен изучить Схему, потому что о функциональном программировании заведомо больше книг по Схеме, нежели о функциональном программировании на javascript.
От теории к практике. Вычисляем скролл страницы
При написании кроссбраузерного js-кода, зачастую различные браузер-специфичные данные обернуты одной функцией. Такая нормализация различий существенно упрощает дальнейшую разработку. Когда вызывается функция-обёртка, выполняется кусок кода, специфический для каждого конкретного браузера.
Ну вот например, для реализации drag’n'drop библиотек нам почти наверняка понадобится информация о положении курсора, поставляемая событиями мыши. Только вот события мыши возвращают координаты курсора относительно окна браузера (т.е. видимой области страницы), а не страницы. Стало быть, чтобы получить абсолютные координаты курсора, мы должны добавить размеры прокрутки (скролла) страницы. И, стало быть, нам нужна такая функция, назовём её getScrollY. А поскольку при перетаскивании элемента новые координаты курсора должны вычисляться непрерывно, вопрос об эффективности такого вычисления встаёт ребром.
Но вот незадача, каждый из великой тройки браузеров, реализует свой алгоритм нахождения скролла. Более того, IE в разных свои ипостасях реализаует два таких алгоритма. Ричарл Корнфорд написал об этих 4 алгоритмах в своей статье об обнаружении свойств [в браузере]. Самая неприятная ловушка на пути реализации всех 4 алгоритмов в одной обёртке - это то, что один из этих алгоритмов использует document.body. А document.body, напомним, не существует на момент загрузки скриптов в секции head. Так что мы никак не можем определить без дополнительных на то усилий (ну, например, добавления детектирования в событие onload), какой из алгоритмов вычисления скролла нам в дальнейшем использовать.
Есть два основных подхода к изничтожению подобных проблем. Первый - детектирование браузера, второй - детектирование браузер-специфических свойств. Первый метод - откровенный моветон, он неприемлем ввиду своей хрупкости и потенциально высокой ошибкоёмкости. Второй куда лучше, но, тем не менее, он не эффективен.
Это как раз тот самый случай, когда нас здорово может выручить динамическое опредение функций:
var getScrollY = function() {
if (typeof window.pageYOffset == 'number') {
getScrollY = function() {
return window.pageYOffset;
};
} else if ((typeof document.compatMode == 'string') &&
(document.compatMode.indexOf('CSS') >= 0) &&
(document.documentElement) &&
(typeof document.documentElement.scrollTop == 'number')) {
getScrollY = function() {
return document.documentElement.scrollTop;
};
} else if ((document.body) &&
(typeof document.body.scrollTop == 'number')) {
getScrollY = function() {
return document.body.scrollTop;
}
} else {
getScrollY = function() {
return NaN;
};
}
return getScrollY();
}
Выводы
Динамическое определение функций позволяет мне писать компактный, ясный и эффективынй код. Каждый раз, когда я сталкиваюсь с этим приёмом, я никак не налюбуюсь на заложенные в javascript ресурсы программирования в функциональной парадигме.
Javascript - язык одновременно объектно-ориентированный и функциональный. Полки в книжных магазинах ломятся от фолиантов, в которых рассказывается об объекно-ориентированных приёмах, в то время как о функциональных приёмах написано совсем немного. Этот крен надо выправлять.