Эта статья была рецензирована Дэном Принцем . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!
В недавней ветке на форумах SitePoint был дан некоторый код, позволяющий одному выпадающему блоку управлять, когда виден другой выпадающий список. Хотя код работал просто отлично, я понял, что он оставляет желать лучшего. Он был хрупким и не мог противостоять даже небольшим изменениям в сопровождающем HTML.
Вот оригинальный код CSS:
#second { display: none; } #second.show { display: block; }
и оригинальный код JavaScript:
document.getElementById("location").onchange = function () { if (this[this.selectedIndex].value === "loc5") { document.getElementById("second").className = "show"; } else { document.getElementById("second").className = ""; } };
В этой статье я продемонстрирую несколько простых методов рефакторинга JavaScript, которые можно применить к приведенному выше коду, чтобы упростить его повторное использование и адаптировать к будущим изменениям.
Зная, какой путь выбрать
У JavaScript есть много способов решить ту же задачу, и некоторые из них работают лучше, чем другие. Есть ли способы улучшить код прямо сейчас, чтобы мы не возвращались к нему позже? Конечно! Но когда есть несколько возможных способов сделать что-то, как мы можем определить, какой из них лучше всего подойдет?
Одним из распространенных методов улучшения кода является устранение дублирования (используя принцип « не повторяйся» ). Оттуда, однако, может быть более полезным перейти от конкретного к более общему коду, который позволяет нам обрабатывать более широкий диапазон ситуаций.
Конкретный код имеет тенденцию быть хрупким, когда дело доходит до обработки будущих изменений. Код не существует в вакууме, и его необходимо будет изменить в ответ на другие действия вокруг него и в коде HTML. Однако, с учетом прошлого опыта, мы можем посмотреть на общие изменения, которые происходят, и улучшения, которые сокращают количество повторений кода. Неизменно вы обнаружите, что это означает сделать код более общим.
Но будьте осторожны! Может быть легко сделать наш код слишком общим, чтобы его стало трудно понять. Нахождение хорошего баланса между универсальным и читаемым — вот где мы находим улучшенный код.
Методы рефакторинга JavaScript: специфичные для общего
В ходе разработки через тестирование (TDD) вы не можете не столкнуться с этим принципом как частью процесса:
Поскольку тесты становятся более конкретными, код становится более общим.
Циклы TDD Роберта Мартина хорошо освещают эту идею. Основным преимуществом здесь является то, что общий код в конечном итоге может обрабатывать более широкий спектр ситуаций и сценариев.
Обратите внимание: если вы хотите больше узнать о TDD, ознакомьтесь с нашим коротким мини-курсом Test-Driven Development с Node.js.
Глядя на приведенный выше код, некоторые очевидные специфические для общих улучшений сразу же доступны
- Хранение строк в переменных поможет нам управлять ими из одного места.
-
onchange
событияonchange
проблематичен, поскольку его можно перезаписать. Мы должны рассмотреть использованиеaddEventListener
вместо этого. - Свойство
className
перезапишет существующие имена классов. Мы должны рассмотреть возможность использованияclassList
вместо этого.
После внесения всех этих улучшений мы получим код, который более устойчив к будущим изменениям и его легче обновлять. Итак, начнем …
Используйте переменные для предотвращения дублирования
Идентификатор выпадающего списка («location») и его значение триггера («loc5») являются полезными ссылками для сохранения вместе. Второй элемент <select>
также упоминается дважды, который мы можем вывести в отдельную переменную, чтобы предотвратить беспорядок и обеспечить более легкое обслуживание .
Например, вместо двух ссылок на один и тот же элемент, который нужно было бы изменить, если изменился идентификатор элемента:
// bad code if (...) { document.getElementById("second").className = "show"; } else { document.getElementById("second").className = ""; }
Мы можем хранить ссылку на этот элемент в переменной, ограничивая будущие изменения только местом, где назначена переменная:
// good code var target = document.getElementById("second"); if (...) { target.className = "show"; } else { target.className = ""; }
Вытягивая эти строки вместе в верхнюю часть кода и выделяя части условия if, специфичная для универсального метода приводит к коду, который легче поддерживать как сейчас, так и в будущем. Если какой-либо из идентификаторов или значений параметров изменяется, их все можно легко найти в одном месте, вместо того, чтобы искать код для всех их вхождений.
// improved code var source = document.getElementById("location"); var target = document.getElementById("second"); var triggerValue = "loc5"; source.onchange = function () { var selectedValue = this[this.selectedIndex].value; if (selectedValue === triggerValue) { target.className = "show"; } else { target.className = ""; } };
Улучшение обработки событий
Традиционные обработчики событий все еще довольно популярны (и в этом случае использовались правильно), но у них есть некоторые проблемы. Главным среди них является то, что при настройке обработчика события для элемента таким способом вы перезапишете любой предыдущий обработчик для того же события.
// bad code source.onchange = function () { // ... };
В настоящее время приведенный выше код работает. Мы можем продемонстрировать это с помощью теста.
Краткое примечание о тестировании
Философия : тесты — отличный способ убедиться, что код, который вы пишете, ведет себя так, как вы ожидаете. Они уменьшают вероятность того, что изменения, внесенные вами в код, приведут к поломке чего-либо еще в другом месте кода. Введение в тестирование, к сожалению, выходит за рамки этой статьи (хотя в SitePoint есть много отличного контента по этой теме). Вы все равно сможете следовать, не написав тест в своей жизни.
Синтаксис : В следующих тестах используется среда тестирования Jasmine . Тесты Jasmine (также известные как specs) определяются путем вызова глобальной функции Jasmine it
, которая принимает строку и дополнительную функцию в качестве аргументов. Строка — это заголовок спецификации, а функция — сама спецификация. Вы можете прочитать больше о Жасмин на домашней странице проекта .
Обратите внимание, что эта статья будет посвящена тестированию внешнего кода. Если вы ищете что-то сфокусированное на серверной части, обязательно ознакомьтесь с нашим курсом: Разработка через тестирование в Node.js
Запуск тестов
Учитывая предыдущее состояние нашего кода, пройдут следующие два теста:
it("should add the 'show' class name when the 'loc5' option is selected", function() { changeSelectTo(source, "loc5"); expect(target.classList.contains("show")).toBe(true); }); it("should remove the 'show' class name when an option value different from 'loc5' is selected", function() { changeSelectTo(source, "loc2"); expect(target.classList.contains("show")).toBe(false); });
Функция changeSelectTo
изменяет значение элемента <select>
и ожидание (построенное с использованием функции ожидания Jasmine) устанавливает, что элемент имеет правильное имя класса.
Но как только onchange
обработчик onchange
— что может сделать любой другой код — функция, которая изменила имя класса, теряется, и вещи начинают работать неправильно. Мы можем продемонстрировать это с помощью дальнейшего теста:
it("should toggle the class name even when the onchange event is replaced", function () { changeSelectTo(source, "loc2"); expect(target.classList.contains("show")).toBe(false); // Overwrite the onchange handler source.onchange = function doNothing() { return; }; changeSelectTo(source, "loc5"); expect(target.classList.contains("show")).toBe(true); // fails });
Этот тест не проходит, как видно из этого CodePen . Обратите внимание, что специальный код Jasmine находится в отдельной ручке, которую можно найти здесь .
Рефакторинг нашего кода для прохождения теста
Мы можем легко выполнить этот тест, используя addEventListener , который позволяет назначать любое количество функций одному событию. Параметр false
указывает, используется ли для упорядочения событий перехват событий (когда true
) или всплытие событий (когда false
). Quirksmode дает хороший обзор порядка событий для событий.
// good code source.addEventListener("change", function (evt) { // ... }, false);
Вот как это изменение влияет на код:
// improved code var source = document.getElementById("location"); var target = document.getElementById("second"); var triggerValue = "loc5"; source.addEventListener("change", function () { var selectedValue = this[this.selectedIndex].value; if (selectedValue === triggerValue) { target.className = "show"; } else { target.className = ""; } }, false);
С addEventListener
строкой addEventListener
все тесты теперь проходят.
Примечание : в тестовом коде я назвал функцию toggleShowOnSelectedValue
, чтобы вам было проще переключаться между подходами при тестировании различных методов onchange
:
//source.onchange = toggleShowOnSelectedValue; source.addEventListener("change", toggleShowOnSelectedValue, false);
Попробуйте в CodePen выше. Попробуйте переключить закомментированные строки и посмотрите, что произойдет.
Улучшение обработки классов
Другая проблема с кодом заключается в том, что второй элемент <select>
потеряет все предыдущие классы, которые он мог иметь, из-за замены className
всего, что было раньше.
// bad code target.className = "show";
Мы можем видеть, что проблема происходит в следующем сбое, который ожидает, что класс indent
все еще будет оставаться в элементе select после его отображения:
it("should retain any existing class names that were on the target element", function () { changeSelectTo(source, "loc2"); target.classList.add("indent"); expect(target.classList.contains("indent")).toBe(true); changeSelectTo(source, "loc5"); expect(target.classList.contains("indent")).toBe(true); // fails });
Из-за className
заменяющего полное имя класса, любые другие классы, которые были там, также удаляются.
Вы можете увидеть неудачный тест в следующем CodePen . Обратите внимание, что специальный код Jasmine находится в отдельной ручке, которую можно найти здесь .
Вместо того, чтобы иметь эти потенциальные проблемы, мы можем использовать classList для добавления и удаления имен классов.
// good code target.classList.add("show"); // ... target.classList.remove("show");
Это теперь приводит к прохождению теста, как показано ниже:
Результирующий код после этих улучшений теперь:
// improved code var source = document.getElementById("location"); var target = document.getElementById("second"); var triggerValue = "loc5"; source.addEventListener("change", function () { var selectedValue = this[this.selectedIndex].value; if (selectedValue === triggerValue) { target.classList.add("show"); } else { target.classList.remove("show"); } }, false);
Если вас беспокоит использование API classList, поскольку вы хотите поддерживать IE9 и более старые браузеры, вы можете вместо этого использовать отдельные функции addClass и removeClass для достижения аналогичных результатов.
Вывод
Улучшение вашего кода не должно быть трудной или сложной задачей.
Принцип, характерный для общего принципа, — это полезный побочный эффект от разработки, основанной на тестировании. Независимо от того, тестируете ли вы код или нет, вы также можете воспользоваться этими общими методами кода, которые делают ваш код более гибким. Это освобождает вас от необходимости часто возвращаться для исправления кода.
Попробуйте добавить некоторые из этих улучшений в свой собственный код и дайте нам знать, как они улучшили ваши позиции, или свяжитесь с нами на форумах для дальнейшего обсуждения и помощи.