Статьи

Стоп Вложенные функции! (Но не все из них)

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



В раннем детстве производительность не была очень важной.

В раннем детстве производительность не была очень важной. От модемных подключений 56K (или хуже) до компьютера Pentium 133 МГц конечного пользователя с 8 МБ ОЗУ Интернет должен был работать медленно (хотя это не мешало всем жаловаться на это). По этой причине для начала был создан JavaScript, чтобы перенести простую обработку, такую ​​как проверка формы, в браузер, что делает определенные задачи более простыми и быстрыми для конечного пользователя. Вместо заполнения формы, нажатия кнопки «Отправить» и ожидания не менее тридцати секунд, чтобы сообщить, что вы ввели неверные данные в поле, JavaScript позволил веб-авторам проверить ваш ввод и предупредить вас о любых ошибках перед отправкой формы.

Перенесемся в сегодня. Конечные пользователи наслаждаются многоядерными компьютерами и компьютерами с частотой несколько ГГц, изобилием оперативной памяти и высокой скоростью соединения. JavaScript больше не относится к проверке неформальной формы, но он может обрабатывать большие объемы данных, изменять любую часть страницы на лету, отправлять и получать данные с сервера, а также добавлять интерактивность к статической странице, в остальном — все в названии повышения опыта пользователя. Эта модель хорошо известна во всей компьютерной индустрии: растущее количество системных ресурсов позволяет разработчикам писать более сложные и зависящие от ресурсов операционные системы и программное обеспечение. Но даже с таким обильным и постоянно растущим количеством ресурсов разработчики должны помнить о количестве ресурсов, которые потребляет их приложение, особенно в Интернете.

Сегодняшние движки JavaScript опережают движки десяти лет назад, но они не все оптимизируют. То, что они не оптимизируют, оставлено разработчикам.

Существует также новый набор веб-устройств, смартфонов и планшетов, работающих на ограниченном наборе ресурсов. Их урезанные операционные системы и приложения, безусловно, являются хитом, но основные поставщики мобильных ОС (и даже поставщики настольных ОС) рассматривают веб-технологии в качестве своей платформы для разработчиков, подталкивая разработчиков JavaScript к обеспечению эффективности и производительности своего кода.

Плохо работающее приложение испортит хороший опыт.

Самое главное, пользовательский опыт зависит от хорошей производительности. Красивые и естественные пользовательские интерфейсы, безусловно, повышают удобство работы пользователя, но неэффективное приложение может испортить хороший опыт. Если пользователи не хотят использовать ваше программное обеспечение, то какой смысл его писать? Поэтому крайне важно, чтобы в наши дни, в эпоху веб-ориентированной разработки, разработчики JavaScript писали лучший из возможных кодов.

Так какое отношение все это имеет к функциям?

То, где вы определяете свои функции, влияет на производительность вашего приложения.

Существует много анти-паттернов JavaScript, но одна из них, включающая функции, стала несколько популярной, особенно в толпе, которая стремится принудить JavaScript эмулировать функции на других языках (такие функции, как конфиденциальность). Это вложение функций в другие функции, и если оно выполнено неправильно, это может отрицательно повлиять на ваше приложение.

Важно отметить, что этот анти-шаблон применяется не ко всем экземплярам вложенных функций, но обычно определяется двумя характеристиками. Во-первых, создание рассматриваемой функции обычно откладывается, то есть вложенная функция не создается движком JavaScript во время загрузки. Это само по себе неплохо, но это вторая характеристика, которая снижает производительность: вложенная функция многократно создается из-за повторяющихся вызовов внешней функции. Поэтому, хотя может быть легко сказать, что «все вложенные функции плохие», это, конечно, не так, и вы сможете определить проблемные вложенные функции и исправить их, чтобы ускорить работу вашего приложения.


Первым примером этого анти-паттерна является вложение функции в нормальную функцию. Вот упрощенный пример:

1
2
3
4
5
6
7
8
9
function foo(a, b) {
    function bar() {
        return a + b;
    }
 
    return bar();
}
 
foo(1, 2);

Вы можете не писать этот точный код, но важно распознать шаблон. Внешняя функция foo() содержит внутреннюю функцию bar() и вызывает эту внутреннюю функцию для выполнения работы. Многие разработчики забывают, что функции являются значениями в JavaScript. Когда вы объявляете функцию в своем коде, движок JavaScript создает соответствующий объект функции — значение, которое можно присвоить переменной или передать другой функции. Акт создания функционального объекта похож на любой другой тип значения; движок JavaScript не создает его, пока это не нужно. Так что в случае вышеприведенного кода движок JavaScript не создает внутреннюю функцию bar() пока не выполнится foo() . При выходе из функции foo() объект функции bar() уничтожается.

Тот факт, что foo() имеет имя, подразумевает, что оно будет вызываться несколько раз по всему приложению. Хотя одно выполнение foo() будет считаться нормальным, последующие вызовы вызывают ненужную работу для движка JavaScript, поскольку он должен воссоздавать объект функции bar() для каждого выполнения foo() . Итак, если вы вызываете foo() 100 раз в приложении, движок JavaScript должен создать и уничтожить 100 объектов функций bar() . Большое дело, верно? Движок должен создавать другие локальные переменные внутри функции каждый раз, когда она вызывается, так зачем заботиться о функциях?

В отличие от других типов значений, функции обычно не меняются; функция создается для выполнения конкретной задачи. Так что не имеет смысла тратить циклы ЦП, воссоздавая какое-то статическое значение снова и снова.

В идеале, объект функции bar() в этом примере должен создаваться только один раз, и этого легко достичь, хотя, естественно, более сложные функции могут потребовать обширного рефакторинга. Идея состоит в том, чтобы переместить объявление bar() за пределы foo() чтобы объект функции создавался только один раз, например:

1
2
3
4
5
6
7
8
9
function foo(a, b) {
    return bar(a, b);
}
 
function bar(a, b) {
    return a + b;
}
 
foo(1, 2);

Обратите внимание, что новая функция bar() не совсем такая, как была внутри foo() . Поскольку старая функция bar() использовала параметры a и b в foo() , новая версия нуждалась в рефакторинге, чтобы принять эти аргументы для выполнения своей работы.

В зависимости от браузера этот оптимизированный код может быть на 10–99% быстрее, чем вложенная версия. Вы можете просмотреть и запустить тест для себя на jsperf.com/nested-named-functions . Имейте в виду, простота этого примера. Увеличение производительности на 10% (на самом низком уровне спектра производительности) не так много, но оно будет выше, если задействовать больше вложенных и сложных функций.

Чтобы, возможно, запутать проблему, оберните этот код в анонимную, автоматически выполняющуюся функцию, например:

01
02
03
04
05
06
07
08
09
10
11
12
13
(function() {
 
function foo(a, b) {
    return bar(a, b);
}
 
function bar(a, b) {
    return a + b;
}
 
foo(1, 2);
 
}());

Обертывание кода в анонимной функции является распространенным шаблоном, и на первый взгляд может показаться, что этот код повторяет вышеупомянутую проблему производительности, помещая оптимизированный код в анонимную функцию. Хотя при выполнении анонимной функции наблюдается незначительное снижение производительности, этот код вполне приемлем. Самоисполняющаяся функция служит только для хранения и защиты функций foo() и bar() , но, что более важно, анонимная функция выполняется только один раз — таким образом, внутренние функции foo() и bar() создаются только один раз. Однако в некоторых случаях анонимные функции так же (или даже более) проблематичны, как и именованные функции.


Что касается этой темы производительности, анонимные функции могут быть более опасными, чем именованные функции.

Опасна не анонимность функции, а то, как ее используют разработчики. При настройке обработчиков событий, функций обратного вызова или функций итераторов довольно часто используются анонимные функции. Например, следующий код назначает прослушиватель события click для документа:

1
2
3
document.addEventListener(«click», function(evt) {
    alert(«You clicked the page.»);
});

Здесь анонимная функция передается addEventListener() для подключения события click к документу; Таким образом, функция выполняется каждый раз, когда пользователь нажимает в любом месте на странице. Чтобы продемонстрировать еще одно общее использование анонимных функций, рассмотрим этот пример, в котором библиотека jQuery используется для выбора всех элементов <a /> в документе и их итерации с помощью метода each() :

1
2
3
$(«a»).each(function(index) {
    this.style.color = «red»;
});

В этом коде анонимная функция, переданная методу each() объекта jQuery, выполняется для каждого элемента <a /> найденного в документе. В отличие от именованных функций, где они подразумеваются для многократного вызова, повторное выполнение большого количества анонимных функций довольно явно. Ради производительности крайне важно, чтобы они были эффективными и оптимизированными. Взгляните на следующий (еще раз упрощенный) плагин jQuery:

01
02
03
04
05
06
07
08
09
10
11
12
$.fn.myPlugin = function(options) {
 
    return this.each(function() {
        var $this = $(this);
         
        function changeColor() {
            $this.css({color : options.color});
        }
         
        changeColor();
    });
};

Этот код определяет чрезвычайно простой плагин под названием myPlugin ; это так просто, что многие общие черты плагина отсутствуют. Обычно определения подключаемых модулей заключаются в самозаполняющиеся анонимные функции, и обычно для параметров предоставляются значения по умолчанию, чтобы обеспечить доступность допустимых данных для использования. Эти вещи были удалены для ясности.

Цель этого плагина состоит в том, чтобы изменить цвет выбранных элементов на тот, который указан в объекте options переданном myPlugin() . Это делается путем передачи анонимной функции в итератор each() , в результате чего эта функция выполняется для каждого элемента в объекте jQuery. Внутри анонимной функции внутренняя функция changeColor() выполняет фактическую работу по изменению цвета элемента. Как написано, этот код неэффективен, потому что, как вы уже догадались, changeColor() определена внутри итерационной функции … заставляя движок JavaScript воссоздавать changeColor() при каждой итерации.

Сделать этот код более эффективным довольно просто и следует тому же шаблону, что и раньше: рефакторинг функции changeColor() которая должна быть определена вне каких-либо содержащих функций, и позволяет ей получать информацию, необходимую для ее работы. В этом случае changeColor() требуется объект jQuery и новое значение цвета. Улучшенный код выглядит так:

01
02
03
04
05
06
07
08
09
10
11
12
function changeColor($obj, color) {
    $obj.css({color : color});
}
 
$.fn.myPlugin = function(options) {
 
    return this.each(function() {
        var $this = $(this);
         
        changeColor($this, options.color);
    });
};

Интересно, что этот оптимизированный код увеличивает производительность с гораздо меньшим запасом, чем пример foo() и bar() , поскольку Chrome лидирует в пакете с 15% -ным приростом производительности ( jsperf.com/function-nesting-with-jquery-plugin ). Правда в том, что доступ к DOM и использование API jQuery вносят свой вклад в повышение производительности, особенно jQuery each() , которая заведомо медленная по сравнению с нативными циклами JavaScript. Но, как и прежде, имейте в виду простоту этого примера. Чем больше вложенных функций, тем больше выигрыш в производительности от оптимизации.

Другой вариант этого анти-паттерна — вложение функций внутри конструкторов, как показано ниже:

01
02
03
04
05
06
07
08
09
10
11
function Person(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
 
    this.getFullName = function() {
        return this.firstName + » » + this.lastName;
    };
}
 
var jeremy = new Person(«Jeremy», «McPeak»),
    jeffrey = new Person(«Jeffrey», «Way»);

Этот код определяет функцию-конструктор Person() и представляет (если не было очевидно) человека. Он принимает аргументы, содержащие имя и фамилию человека, и сохраняет эти значения в свойствах firstName и lastName соответственно. Конструктор также создает метод с именем getFullName() ; он объединяет свойства firstName и lastName и возвращает результирующее строковое значение.

Когда вы создаете какой-либо объект в JavaScript, объект сохраняется в памяти

Этот шаблон стал довольно распространенным в сегодняшнем сообществе JavaScript, потому что он может эмулировать конфиденциальность, функцию, для которой в настоящее время JavaScript не предназначен (обратите внимание, что конфиденциальность не в приведенном выше примере; вы рассмотрим это позже). Но используя этот шаблон, разработчики создают неэффективность не только во времени выполнения, но и в использовании памяти. Когда вы создаете какой-либо объект в JavaScript, объект сохраняется в памяти. Он остается в памяти до тех пор, пока все ссылки на него не будут установлены на null или не будут выходить за рамки. В случае объекта jeremy в приведенном выше коде функция, назначенная getFullName , обычно сохраняется в памяти до тех пор, jeremy объект jeremy находится в памяти. Когда jeffrey объект jeffrey , создается новый объект-функция, который назначается jeffrey getFullName , и он тоже потребляет память до тех пор, пока jeffrey находится в памяти. Проблема здесь в том, что jeremy.getFullName — это объект-функция, отличный от jeffrey.getFullName ( jeremy.getFullName === jeffrey.getFullName приводит к jeremy.getFullName === jeffrey.getFullName false ; запустите этот код по адресу http://jsfiddle.net/k9uRN/ ). Они оба имеют одинаковое поведение, но это два совершенно разных функциональных объекта (и, таким образом, каждый из них потребляет память). Для ясности взгляните на рисунок 1:

фигура 1

Здесь вы видите объекты jeremy и jeffrey , каждый из которых имеет свой собственный getFullName() . Таким образом, каждый созданный объект Person имеет свой уникальный getFullName() каждый из которых использует свой собственный фрагмент памяти. Представьте себе, что вы создаете 100 объектов Person : если каждый getFullName() потребляет 4 КБ памяти, то объекты 100 Person будут использовать как минимум 400 КБ памяти. Это может сложить, но это может быть решительно уменьшено с помощью объекта- prototype .

Как упоминалось ранее, функции являются объектами в JavaScript. Все функциональные объекты имеют свойство prototype , но оно полезно только для функций конструктора. Короче говоря, свойство prototype буквально является прототипом для создания объектов; все, что определено в прототипе функции конструктора, распределяется между всеми объектами, созданными этой функцией конструктора.

К сожалению, в образовании JavaScript недостаточно прототипов.

К сожалению, в образовании JavaScript не уделяется достаточного внимания прототипам, но они абсолютно необходимы для JavaScript, потому что он основан на прототипах и построен на них — это язык прототипов. Даже если вы никогда не вводили слово prototype в своем коде, они используются за кулисами. Например, каждый собственный метод на основе строк, такой как split() , substr() или replace() , определен в прототипе String() . Прототипы настолько важны для языка JavaScript, что, если вы не принимаете прототипную природу JavaScript, вы пишете неэффективный код. Рассмотрим приведенную выше реализацию типа данных Person : создание объекта Person требует, чтобы механизм JavaScript выполнял больше работы и выделял больше памяти.

Итак, как с помощью свойства prototype сделать этот код более эффективным? Что ж, сначала взглянем на переработанный код:

01
02
03
04
05
06
07
08
09
10
11
function Person(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
}
 
Person.prototype.getFullName = function() {
    return this.firstName + » » + this.lastName;
};
 
var jeremy = new Person(«Jeremy», «McPeak»),
    jeffrey = new Person(«Jeffrey», «Way»);

Здесь определение метода getFullName() перемещено из конструктора в прототип. Это простое изменение имеет следующие эффекты:

  • Конструктор выполняет меньше работы и, следовательно, выполняется быстрее (на 18% -96% быстрее). Запустите тест в своем браузере, если хотите.
  • Метод getFullName() создается только один раз и используется всеми объектами Person ( jeremy.getFullName === jeffrey.getFullName приводит к jeremy.getFullName === jeffrey.getFullName true ; запустите этот код по адресу http://jsfiddle.net/Pfkua/ ). Из-за этого каждый объект Person использует меньше памяти.

Вернитесь к рисунку 1 и обратите внимание, что у каждого объекта есть свой метод getFullName() . Теперь, когда getFullName() определен в прототипе, диаграмма объекта изменяется и показана на рисунке 2:

фигура 2

У jeremy и jeffrey больше нет собственного getFullName() , но механизм JavaScript найдет его по прототипу Person() . В более старых механизмах JavaScript процесс поиска метода в прототипе может повлечь за собой снижение производительности, но не в современных механизмах JavaScript. Скорость, с которой современные двигатели находят прототипные методы, чрезвычайно высока.

Но как насчет конфиденциальности? В конце концов, этот анти-шаблон возник из-за ощущения потребности в элементах частного объекта. Если вы не знакомы с шаблоном, взгляните на следующий код:

01
02
03
04
05
06
07
08
09
10
function Foo(paramOne) {
    var thisIsPrivate = paramOne;
 
    this.bar = function() {
        return thisIsPrivate;
    };
}
 
var foo = new Foo(«Hello, Privacy!»);
alert(foo.bar());

Этот код определяет функцию-конструктор с именем Foo() и имеет один параметр с именем paramOne . Значение, переданное в Foo() , хранится в локальной переменной с именем thisIsPrivate . Обратите внимание, что thisIsPrivate является переменной, а не свойством; поэтому он недоступен вне Foo() . Есть также метод, определенный внутри конструктора, и он называется bar() . Поскольку bar() определена в Foo() , она имеет доступ к переменной thisIsPrivate . Поэтому, когда вы создаете объект Foo и вызываете bar() , возвращается значение, присвоенное thisIsPrivate .

Значение, присвоенное thisIsPrivate , сохраняется. К нему нельзя получить доступ за пределами Foo() , и, следовательно, он защищен от внешней модификации. Это здорово, правда? Ну да и нет. Понятно, почему некоторые разработчики хотят эмулировать конфиденциальность в JavaScript: вы можете гарантировать, что данные объекта защищены от несанкционированного доступа. Но в то же время вы вносите неэффективность в свой код, не используя прототип.

Итак, еще раз, как насчет конфиденциальности? Ну, это просто: не делай этого. В настоящее время язык официально не поддерживает элементы закрытых объектов, хотя это может измениться в будущем пересмотре языка. Вместо того, чтобы использовать замыкания для создания закрытых членов, соглашение, обозначающее «частные члены», заключается в добавлении идентификатора с подчеркиванием (то есть: _thisIsPrivate ). Следующий код переписывает предыдущий пример, используя соглашение:

01
02
03
04
05
06
07
08
09
10
function Foo(paramOne) {
    this._thisIsPrivate = paramOne;
}
 
Foo.prototype.bar = function() {
    return this._thisIsPrivate;
};
 
var foo = new Foo(«Hello, Convention to Denote Privacy!»);
alert(foo.bar());

Нет, это не личное, но соглашение о подчеркивании в основном гласит: «Не прикасайся ко мне». Пока JavaScript полностью не поддерживает частные свойства и методы, разве вы не захотите иметь более эффективный и производительный код, чем конфиденциальность? Правильный ответ: да!


Где вы определяете функции в своем коде, влияет на производительность вашего приложения; имейте это в виду при написании кода. Не вкладывайте функции внутри часто вызываемой функции. Это тратит впустую циклы процессора. Что касается функций конструктора, охватите прототип; невыполнение этого приводит к неэффективному коду. В конце концов, разработчики пишут программное обеспечение для пользователей, и производительность приложения так же важна для пользовательского интерфейса, как и пользовательский интерфейс.