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

Чистые и нечистые функции

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

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


function mouseOnLeftSide(mouseX) {
    return mouseX < window.innerWidth / 2;
}

document.onmousemove = function(e) {
    console.log(mouseOnLeftSide(e.pageX));
};

Функция mouseOnLeftSide() принимает координату X и сравнивает ее с размером половины ширины окна — если она меньше, значит указатель мыши слева. Однако, mouseOnLeftSide() не является чистой функцией. Мы знаем это потому, что внутри тела функции используется значение, которое не было ей явно передано:

return mouseX < window.innerWidth / 2;

Функции было передано значение mouseX, но не window.innerWidth. Это означает, что функция обращается к данным, которые ей не были переданы и поэтому она не является чистой.

Проблема с нечистыми функциями

Вы можете спросить, а в чем, собственно, проблема — этот кусок кода отлично работает и делает то, что от него ожидается. Представьте, что вы получили баг-репорт от пользователя, что при ширине окна меньше 500 пикселей, функция работает неверно. Как вы будете проверять это? У вас есть два варианта:

  • Вы можете тестировать вручную, передвигая мышь в браузере, пока не найдете проблему.
  • Вы можете написать юнит-тесты (для их написания есть неплохое введение от Ребекки Мерфи), которые не только отследят баг, но и предупредят его появление в будущем.

Чтобы проверить это сразу и избежать нового появления бага, мы выберем второй вариант и начнем писать тест. И тут мы встретимся с новой проблемой: как правильно настроить наш тест? Мы знаем, что надо настроить тест с шириной окна меньше 500 пикселей, но как? Функция зависит от значения window.innerWidth и именно оно вызывает проблемы.

Преимущества чистых функций

Упрощение тестирования

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

function mouseOnLeftSide(mouseX, windowWidth) {
    return mouseX < windowWidth / 2;
}

document.onmousemove = function(e) {
    console.log(mouseOnLeftSide(e.pageX, window.innerWidth));
};

Ключевое отличие этого варианта состоит в том, что mouseOnLeftSide() теперь принимает два аргумента: координату X указателя мыши и ширину окна. Это значит, что mouseOnLeftSide() теперь чистая функция: все необходимые ей данные явно передаются в качестве входных и она не пытается получить доступ к внешним данным.

С точки зрения функциональности она идентична предыдущему примеру, но при этом значительно улучшена ее поддерживаемость и тестируемость. Теперь нам не нужны хаки с подсовыванием в тесты фиктивного window.innerWidth, вместо этого мы просто вызываем mouseOnLeftSide() с нужными нам аргументами:

mouseOnLeftSide(5, 499) // ensure it works with width < 500

Самодокументирование

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

function mouseOnLeftSide(mouseX, windowWidth)

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

Избегание глобальных переменных в функциях

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

Дополнительным свойством чистых функций является ссылочная прозрачность. Это сложный термин с простым смыслом: с одинаковыми входными данными мы получаем одинаковые выходные данные. Возвращаясь к mouseOnLeftSide, взглянем на ее первый вариант:

function mouseOnLeftSide(mouseX) {
    return mouseX < window.innerWidth / 2;
}

В этой функции нет ссылочной прозрачности. Я могу вызывать ее 5 раз с одним значением, изменить размер окна браузера и получить 5 разных результатов. Это немного надуманный пример, но с функциями, возвращающими разные значения при идентичных входных данных всегда сложнее работать — ведь вы не можете гарантировать их поведение. По той же причине их сложнее тестировать, так как у вас нет полного контроля над данными, передаваемыми в функцию.

С другой стороны, наша улучшенная функция mouseOnLeftSide обладает ссылочной прозрачностью, так как все нужные данные подаются на вход и не запрашиваются из вне:

function mouseOnLeftSide(mouseX, windowWidth) {
    return mouseX < windowWidth / 2;
}

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

Какие функции надо делать чистыми

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

Рассмотрим пример кода, в котором мы выбираем элемент в DOM и меняем его фоновый цвет на красный:

function changeElementToRed() {
    var foo = document.getElementById('foo');
    foo.style.backgroundColor = "red";
}

changeElementToRed();

В этом фрагменте есть две проблемы, решаемые за счет переделки функции в чистую:

  1. Эта функция совершенно непригодна к повторному использованию, она привязана к конкретному элементу DOM. И мы не сможем применить ее к другому элементу.
  2. Эту функцию сложно тестировать по причине ее нечистоты. Для тестирования нам надо создавать элемент со специальным ID, вместо того, чтобы использовать обычный элемент.

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

function changeElementToRed(elem) {
    elem.style.backgroundColor = "red";
}

function changeFooToRed() {
    var foo = document.getElementById('foo');
    changeElementToRed(foo);
}

changeFooToRed();

Мы сделали функцию changeElementToRed() не привязанной к конкретному элементу и более абстрактной. В то же время мы сделали ее чистой, получив все преимущества, о которых мы упоминали ранее.

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

Заключение

“Чистые функции”, “побочные эффекты”, “ссылочная прозрачность” это все термины, обычно ассоциируемые с чисто функциональными языками программирования, но это не значит, что мы не можем их применять и к JavaScript. Учет этих принципов и грамотное применение их на практике дают вам преимущество в виде более надежного, самодокументирующегося кода, с которым проще работать и который реже ломается. Я призываю вас учитывать это при написании или при рефакторинге существующего кода. Да, привыкание к этому займет некоторое время, но скоро вы сможете делать это, не задумываясь. А в будущем вы сами (и другие разработчики, которые будут работать с вашим кодом) оцените это по достоинству.