Если открыть исходник почти любого сайта старше пяти лет, скорее всего, где-нибудь в навигации найдётся вот такая конструкция: <a href="javascript:void(0)">. Ссылка, которая выглядит как ссылка, но при клике ничего не делает — она нужна только для того, чтобы JavaScript-обработчик мог поймать клик и сделать что-то своё: открыть модалку, развернуть меню, отправить форму.

Конструкция жива до сих пор, но за последние годы стала плохой практикой. Разберёмся, что такое void в JavaScript, как браузер обрабатывает псевдо-URL javascript: в атрибуте href, почему две эти штуки скомбинировали именно так — и какими способами в современном коде делают то же самое, но без побочных эффектов.

Откуда взялся void в JavaScript

Слово void в английском буквально означает «пустота». В программировании этот термин обычно описывает функцию, которая ничего не возвращает — такую, у которой нет осмысленного результата, только побочный эффект. В C, C++, Java для этого есть отдельный тип void, который пишется в сигнатуре функции: «эта функция ничего не вернёт, не пытайся использовать её значение».

В JavaScript типа void нет — зато есть оператор с таким же именем. Он вычисляет своё выражение и затем гарантированно возвращает undefined, что бы выражение ни выдало. Синтаксис похож на вызов функции, но это не функция:

void 0           // undefined
void (1 + 2)     // undefined  (выражение посчитано, но результат выкинут)
void "hello"     // undefined
void someFn()    // undefined  (функция вызвана, её возврат проигнорирован)

На вид бесполезный оператор — зачем нужна штука, которая всегда возвращает undefined? Изначально у него было две практические задачи.

Первая — гарантированно получить настоящий undefined, не полагаясь на одноимённую глобальную переменную. До ES5 (это пятая редакция стандарта ECMAScript, вышедшая в 2009 году) переменную undefined можно было переназначить:

// в старом IE такое работало:
undefined = 42;
console.log(undefined); // 42 (!)

// а void 0 всегда возвращал «настоящий» undefined:
console.log(void 0); // undefined

Сейчас это уже не актуально — в современных стандартах undefined сделали неизменяемым свойством глобального объекта. Но в коде из 2000-х конструкция void 0 до сих пор встречается как привычка.

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

<h1>Привет, Мир!</h1>
<script>
  void(
    document.body.style.backgroundColor = 'red',
    document.body.style.color = 'white'
  );
</script>

Что увидим в окне браузера:

Страница с красным фоном и белым заголовком «Привет, Мир!» после применения void-оператора к выражению со стилями

Запятая внутри void(...) — это оператор запятая, который вычисляет все свои операнды слева направо и возвращает значение последнего. Стили применились, а void сверху всё равно вернёт undefined. Именно эту способность «запустить код, гарантировав undefined на выходе» и стали использовать в атрибуте href — как заглушку.

Псевдо-URL javascript: в атрибуте href

Браузеры понимают в атрибуте href не только обычные URL вроде /about или https://example.com, но и несколько специальных схем. Одна из них — javascript:, так называемый псевдо-URL: вместо адреса страницы там лежит JavaScript-код, который выполняется при клике по ссылке.

<a href="javascript:console.log('Hello world');alert('This is alert');">Link</a>

Когда пользователь кликает по такой ссылке, браузер не переходит ни на какой URL — вместо этого выполняет код после двоеточия как обычный JavaScript. В DevTools-консоли появится строка Hello world, а сверху в окне покажется системный alert:

Результат клика по ссылке javascript:console.log() и alert() — Hello world в консоли и системный alert

Технология появилась ещё в Netscape Navigator в середине 1990-х, до того, как у тега <a> появились нормальные обработчики событий. Сегодня псевдо-URL javascript: применяется крайне редко: для inline-кода есть атрибут onclick, для современного подхода — addEventListener. Но одна частная задача за конструкцией закрепилась: показать пользователю ссылку, которая выглядит как ссылка, но никуда не ведёт. Это нужно, когда дизайн требует кликабельного элемента с подчёркиванием в виде ссылки, но настоящей навигации не предполагается — например, по клику открывается модальное окно или разворачивается аккордеон.

Самый простой способ написать такую заглушку был бы href="#" — но у него есть неприятный побочный эффект: браузер прокрутит страницу к якорю # (то есть к самому верху, если на странице нет элемента с таким id) и допишет # в URL. Чтобы этого избежать, придумали трюк: положить в href код, который ничего не делает и не имеет побочных эффектов. Тут и пригодился оператор void.

Комбинация: href="javascript:void(0)"

Соединив две конструкции, получаем хрестоматийное:

<a href="javascript:void(0)">Открыть модалку</a>

Что тут происходит по шагам. Браузер видит href и понимает: это псевдо-URL javascript:, значит надо при клике выполнить выражение после двоеточия. Выражение — void(0): оператор void, применённый к нулю. Скобки тут чисто для читаемости, можно было бы написать void 0 — работает так же. Оператор void возвращает undefined.

А вот важная часть: если код в javascript:-ссылке возвращает undefined (или null, или ничего вообще), браузер просто не предпринимает дальнейших действий — ни навигации, ни замены документа, ни прокрутки. А если возвращает строку, скажем "hello", то текущий документ заменяется на эту строку (старое поведение, до сих пор работает):

<!-- этот клик заменит всю страницу на текст "hello" -->
<a href="javascript:'hello'">Заменить</a>

Поэтому простое href="javascript:0" работало бы для нас отлично (число 0 — не строка), но javascript:void(0) читается как явный сигнал намерения: «это специально пустая ссылка, ничего не должно произойти». К концу 2000-х конструкция стала идиомой, особенно в эпоху jQuery — когда обработчики кликов навешивались отдельно через $('a').click(...).

Типичный паттерн того времени — ссылка-пустышка в навигации, к которой потом цепляется JS:

<nav>
  <a href="/">Главная</a>
  <a href="/products">Товары</a>
  <a id="orders-btn" href="javascript:void(0)">Заказы</a>
  <a href="/contact">Контакты</a>
</nav>

<script>
  document.getElementById('orders-btn').addEventListener('click', () => {
    openOrdersModal();
  });
</script>

Ссылка «Заказы» визуально не отличается от соседних — та же подсветка, тот же подчёркнутый стиль. Но при клике она не уводит со страницы: href ничего не делает, а отдельный обработчик открывает модалку.

Чем заменить в современном коде

В современном фронтенде javascript:void(0) считается анти-паттерном. Не потому что «не работает» — работает прекрасно, — а потому что для той же задачи есть три способа чище. Каждый решает её правильнее с точки зрения семантики, доступности и безопасности.

1. Использовать <button type="button">. Если по клику не происходит навигации — это не ссылка, а кнопка. И HTML, и CSS, и скрин-ридеры (программы для незрячих, которые читают страницу голосом) рассчитаны именно на такое разделение. Скрин-ридер видит <button> и говорит «кнопка», пользователь жмёт — ожидает действие, а не переход. Это правильно.

<nav>
  <a href="/">Главная</a>
  <a href="/products">Товары</a>
  <button type="button" id="orders-btn" class="nav-link">Заказы</button>
  <a href="/contact">Контакты</a>
</nav>

Атрибут type="button" важен внутри формы — без него <button> по умолчанию type="submit" и отправит форму при клике, что обычно не то что нужно. Снаружи формы можно опустить, но привычка ставить везде безопаснее. Визуальное оформление «как ссылка» легко делается классом — убираем стандартный border/background и красим текст в синий с подчёркиванием.

2. Настоящая ссылка + event.preventDefault(). Бывают сценарии, когда по UX лучше оставить именно тег <a> — обычно потому, что элемент находится в потоке текста или потому что хочется, чтобы по правому клику можно было открыть в новой вкладке. В таком случае оставляем настоящий href с осмысленным URL и отменяем навигацию в JS:

<a href="/orders" id="orders-btn">Заказы</a>

<script>
  document.getElementById('orders-btn').addEventListener('click', (e) => {
    e.preventDefault();
    openOrdersModal();
  });
</script>

Бонус — progressive enhancement, постепенное улучшение. Если JavaScript не загрузился (медленный интернет, отключён, упал по ошибке) — ссылка работает как обычная: ведёт на /orders, и пользователь всё равно видит список заказов, пусть и без модалки. С javascript:void(0) у пользователя в такой ситуации был бы тупик.

3. href="#" с отменой по умолчанию. Самый старый и до сих пор частый паттерн. Работает, но требует обязательного preventDefault, иначе сработает скролл вверх:

<a href="#" id="orders-btn">Заказы</a>

<script>
  document.getElementById('orders-btn').addEventListener('click', (e) => {
    e.preventDefault();
    openOrdersModal();
  });
</script>

Обычно его избегают — вариант 1 (с <button>) семантически точнее, а вариант 2 даёт fallback. Но если в кодовой базе уже href="#" стоит в сотне мест — переводить их на javascript:void(0) точно не нужно. Лучше тогда поэтапно мигрировать на <button>.

Подводные камни

Помимо устаревшего вида у javascript:void(0) есть несколько конкретных проблем — ради них и стоит переписывать существующий код.

Content Security Policy блокирует javascript:-URL. CSP — это HTTP-заголовок, которым сервер говорит браузеру, какие источники скриптов разрешены. Современные сайты со строгой настройкой script-src (без 'unsafe-inline') запрещают inline-код в HTML — в том числе и в псевдо-URL. Браузер тихо проигнорирует клик, обработчик не выполнится, пользователь решит что «кнопка сломалась». С точки зрения CSP javascript:void(0) — это inline-script, и под строгие политики он не попадает. <button> такой проблемы не имеет вообще, у него обработчик навешивается отдельно через addEventListener.

Скрин-ридеры озвучивают как ссылку. ARIA — это Accessible Rich Internet Applications, набор атрибутов и соглашений, по которым делается доступная для людей с инвалидностью разметка. Скрин-ридер (программа NVDA, JAWS, VoiceOver и т.п., которая голосом читает содержимое экрана незрячим пользователям) видит <a> и говорит «ссылка, открыть модалку». Пользователь жмёт — и попадает не на новую страницу, как ему обещали, а в модальное окно, которое нужно ещё научиться закрывать. Если же на месте этого элемента стоит <button>, скрин-ридер скажет «кнопка, открыть модалку» — и ожидание совпадёт с поведением. Это базовое правило WAI-ARIA (Web Accessibility Initiative): семантика элемента должна соответствовать его роли, не обманывать пользователя.

Поисковые роботы тратят на это crawl-бюджет. Когда Googlebot обходит сайт, он пробует следовать по каждой ссылке — в том числе по href="javascript:void(0)". Бот не выполняет JS-обработчик, навешенный отдельно, и видит просто URL javascript:void(0), который ему обработать нечем. Ничего криминального не происходит, но это потерянное время для бота — которое тот мог бы потратить на индексацию настоящих страниц сайта. На крупных сайтах с тысячами страниц crawl-бюджет (число запросов Googlebot за единицу времени) — ограниченный ресурс, и каждая псевдо-ссылка работает против.

Навигация с клавиатуры теряет смысл. По стандарту, клавишей Tab можно перейти между интерактивными элементами страницы, а Enter — активирует выбранный. На <a href="javascript:void(0)"> клавиатурник попадёт по Tab, как на обычную ссылку, нажмёт Enter — и сработает click-обработчик. На вид всё ок. Но <button> поддерживает ещё и Space для активации, плюс правильнее выглядит в фокус-индикаторе браузера, плюс корректно работает с дополнительным софтом вроде клавиатурных тренажёров и адаптивных контроллеров. То, что вы сэкономили на семантике, пользователь оплатит в реальном опыте.

Итог

javascript:void(0) — идиома из эры jQuery, которая решала реальную проблему: дать кликабельный элемент в стилях ссылки, но без навигации. Технически работает: оператор void возвращает undefined, псевдо-URL javascript: ничего не делает при undefined-результате, страница не перезагружается.

Современный фронтенд предлагает три варианта чище, каждый по-своему лучше: <button type="button"> — правильная семантика для не-ссылочных действий; <a href="/real-url"> с preventDefault — progressive enhancement, рабочая ссылка для случаев без JS; <a href="#"> — legacy-фоллбек, но только если уже использовался в проекте.

Видишь javascript:void(0) в кодовой базе — не повод спешно переписывать прямо сейчас, но при следующем касании этого участка лучше перевести на <button>. С точки зрения CSP, доступности и crawl-бюджета это маленькое улучшение, которое к концу года из мелких касаний складывается в заметный качественный сдвиг.