Оптимизация JS с помощью функционального подхода
Существует ряд способов, которые могут помочь вам повысить производительность вашего кода. Вы можете проанализировать алгоритмы, которые вы используете, и попытаться ускорить некоторые циклы. Кроме того, вы можете изучить передовой опыт ваших коллег и постараться избежать чрезмерного использования конкатенации и замыканий. Но что, если вместо того, чтобы стараться сократить время выполнения функции, вы могли бы сократить количество их вызовов?
Функциональный подход к написанию кода предлагает нам несколько методов, которые мы могли бы использовать для сокращения времени выполнения программ:
- Ленивая оценка
- Мемоизация
Перед тем, как мы начнем разбор методов, стоит поговорить о некоторых терминах, которые необходимы для понимания того, о чем пойдет речь:
- Чистая функция: функция, которая не имеет побочных действий на глобальную область видимости;
- Ссылочная прозрачность: возможность замены любого подвыражения чистой функции на аналогичное без изменений самой функции и конечного результата.
Ленивая оценка
В JavaScript состав функции играет важную роль в реализации ленивых вычислений. Так как речь идет о строгом языке программирования, каждая функция вычисляется еще до ее вызова. Давайте рассмотрим пример. Предположим, что нам необходимо извлечь конечное количество чисел из бесконечного множества. Мы можем создать функцию range (start, end), которая создает массив целых чисел, начиная со start и заканчивая значением end.
var firstTenItems = range(1, Infinity).take(10);
firstTenItems();//-> 1, 2, 3, 4....10?
В нестрогом языке, таком как Haskell, мы могли бы написать эквивалентную программу, которая будет действительно выдавать значение от одного до десяти. В JavaScript аналогичная программа будет генерировать бесконечный цикл, так как среда будет пытаться оценить базовый диапазон функции, прежде чем выполнить необходимые 10 итераций. Так как ленивые вычисления не встроены в JavaScript, единственным вариантом является создание их альтернативы с помощью API библиотеки Lazy.js, которая основана на функциональном подходе и призвана обойти прожорливый механизм оценки JavaScript.
1.var evenNumbers = Lazy.generate(function (x) {
2.return 2 * x;
3.}); //-> 2, 4, 6, 8,...
4.
5.evenNumbers.take(300).each(doWork);
Lazy.generate - функция, которая по существу является структурой потока данных. Она может быть использована, чтобы генерировать бесконечное количество элементов. Хотя это звучит странно, за кулисами это сила ленивой помощи функции состава, которая позволяет этому происходить. В предыдущем примере кода показана классическая проблема разделения функций, которые генерируют бесконечное множество, и селекторов, которые определяют необходимые значения.
Использование Lazy.js позволяют эффективно решить эту задачу, так как инициализация следующего потока откладывается до возникновения необходимости вызова каждой функции. Без нее решение этой задачи выглядело бы примерно следующим образом:
01.var threshold = 10;
02.for (var i = 0; i < Infinity; i+=2) {
03.if (i >= threshold) {
04.break;
05.}
06.else {
07.var item = items[i];
08.
09.// do work on item
10.}
11.}
Это абсолютно не масштабируемый, одноразовый код, который крайне сложен в поддержке и сопровождении. Используя Lazy.js, мы избегаем громоздкого кода, что существенно повышает эффективность программы и простоту ее обслуживания, так как это позволяет избежать ненужных вычислений (или, по крайней мере, отложить вычисления до возникновения необходимости в их проведении).
Мемоизация
Другой способ реализации ленивых вычислений сведение вычислений функции до вызова к минимуму. В обычных объектно-ориентированных системах эта задача решается с помощью создания кэша, который проверяется каждый раз перед вызовом функции. По возвращению результата, функции присваивается ключ, который на нее ссылается, а пара ключ-значение сохраняется в кэше.
В функциональных языках подобным образом механизм реализован кэширования, но он встроен в платформу. Результат выполнения функции хранится в памяти и может быть восстановлен при любом вызове. Эта техника называется мемоизацией и применима только к чистым функциям. Напомним, что чистые функции всегда возвращают одно и то же значение при одних и тех же входных параметрах. С помощью мемоизации набор входных параметров кодируется как ключ и привязывается к результату функции. Эта пара используется каждый раз, когда функция вызывается с теми же параметрами.Рассмотрим функцию, к
Рассмотрим функцию, которая вычисляет хэш результата, основанного на уникальной комбинации имени и пароля пользователя. В зависимости от типа выбранного вами алгоритма, эта функция может быть достаточно ресурсоемкой. Кроме того, использование мемоизации позволит сделать работу функции более надежной с точки зрения безопасности.
1.function generateSecureHash(username, password) {
2.
3.return hash;
4.}
5.generateSecureHash('jim', 'Jim@Pwd!');
Исходя из того, generateSecureHash - чистая функция, и при введении одних и тех же значений она должна возвращать одинаковые результаты, что позволяет нам воспользоваться мемоизацией и пропустить этап вычислений при каждом новом вводе одних и тех же данных. Эффективность мемоизации прямо пропорциональна модульности вашего кода. Если вы разделяете крупные задачи на несколько мелких функций, вы можете добиться отличного эффекта с помощью мелкозернистого кэширования.
Так как мемоизация не предусмотрена в JavaScript, нам придется реализовать ее с помощью расширяемой функции:
1.var computeFactors = (function factors(num) {
2.
3.// ....
4.
5.return factors;
6.}).memoize();
7.
8.computeFactors(100); //-> [1,2,4,8,16,32,64]
Вот пример реализации мемоизации объекта:
01.Function.prototype.memoized = function (key) {
02.
03.if (this.length === 0) {
04.console.log(\"Cannot memoize function call with empty arguments\");
05.return;
06.}
07.else {
08.// Ручная идентификация объекта
09.if (key.getId && (typeof key.getId === 'function')) {
10.// Получение ID
11.key = key.getId();
12.}
13.else {
14.// Обработка объектов с помощью JSON.stringifying
15.if (!isNumeric(key) && !isString(key)) {
16.// stringify key
17.key = JSON.stringify(key);
18.}
19.}
20.}
21.
22.// Создание локального кэша
23.this._cache = this._cache || {};
24.var val = this._cache[key];
25.
26.if (val === undefined || val === null) {
27.alert(\"Cache miss, apply function\");
28.console.log(\"Cache miss\");
29.val = this.apply(this, arguments);
30.this._cache[key] = val;
31.}
32.else {
33.console.log(\"Cache hit\");
34.}
35.return val;
36.};
37.
38.
39.
40.
41.Function.prototype.memoize = function () {
42.
43.var fn = this;
44.
45.if (fn.length === 0 || fn.length > 1)
46.return fn;
47.
48.return function () {
49.return fn.memoized.apply(fn, arguments);
50.};
51.};
Функциональные языки предоставляют механизмы для ленивой оценки, что освобождает разработчиков от необходимости беспокоиться о реализации и тестировании этого механизма. В JavaScript, мы можем сделать то же самое, используя конструкции высокого уровня, чтобы создавать и обрабатывать любые способы оптимизации кода с помощью мемоизации. На самом деле это дает некоторые преимущества, так как мы всегда можем протестировать функции без использования способов оптимизации, чтобы оценить именно их работу без воздействия других механизмов.