Список неочевидных, но при этом полезных хаков, которые позволят использовать отладчик вашего браузера1 более полноценно. Для понимания материала статьи потребуется как минимум средний уровень владения инструментами разработчика.

▍ Содержание


  • Продвинутые условные точки останова
  • Отслеживание вызовов класса с помощью monitor()
  • Вызов и отладка функции
  • Остановка выполнения при изменении URL
  • Отладка считывания свойств
  • Использование copy()
  • Отладка HTML/CSS

    Продвинутые условные точки останова


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

    ▍ Точки логирования/трассировки


    К примеру, можно выполнять в точках останова команду console.log, превращая их в точки логирования, которые производят вывод в консоль без приостановки выполнения. Эта функциональность доступна во всех ведущих браузерах (Chrome Logpoints, Edge Tracepoints, Firefox Logpoints).



    Если вы хотите также подсчитывать, сколько раз выполняется строка, то вместо console.log используйте console.count.

    ▍ Панель наблюдения


    Также можно использовать console.log в панели наблюдения. Например, чтобы сохранять снимок localStorage при каждой остановке приложения в отладчике, создайте в панели наблюдения элемент console.table(localStorage):


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



    Затем добавьте, например, это выражение для записи снимка DOM: (window.doms = window.doms || []).push(document.documentElement.outerHTML). Теперь после любого изменения поддерева DOM отладчик будет останавливать выполнение, и в конце массива window.doms будет создаваться новый снимок DOM. (Невозможно создать такую точку останова при изменении DOM, которая бы не останавливала выполнение).

    ▍ Трассировка стеков вызовов


    Предположим, у вас есть функция, показывающая спиннер загрузки, и другая функция, его скрывающая. Но где-то в коде вы вызываете первую без соответствующей второй. Как обнаружить источник этого непарного вызова? Используйте console.trace в условной точке останова, расположенной в методе показа спиннера, выполните код, найдите последнюю трассировку стека вызовов этого метода и кликните по вызывающему компоненту для перехода к его коду:


    ▍ Изменение поведения программы


    Используя выражения, оказывающие побочные эффекты на поведение программы, можно изменять это поведение «на лету» прямо в браузере.

    Например, вы можете переопределить параметр id функции getPerson. Поскольку id=1 будет оценено как true, эта условная точка будет останавливать отладчик. Чтобы это предотвратить, добавьте в выражение false.



    ▍ Быстрый и грубый способ профилирования быстродействия


    Не стоит загрязнять профилирование производительности такими вещами, как время вычисления условных точек останова, но если вы хотите быстро и грубо измерить продолжительность выполнения чего-либо, то можете использовать в этих точках доступное в консоли API тайминга.

    В качестве начальной установите точку останова с условием console.time('label'), а в качестве завершающей — с условием console.timeEnd('label'). Теперь при каждом выполнении указанного процесса браузер будет выводить в консоль продолжительность этого выполнения.



    ▍ Использование арности функций


    ▍ Остановка на основе указанного количества аргументов


    Приостановка, только когда текущая функция вызывается с 3 аргументами:

    arguments.callee.length === 3

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



    ▍ Остановка на основе несоответствия указанному числу аргументов


    Приостановка, только когда текущая функция вызывается с несоответствующим числом аргументов:

    (arguments.callee.length) != arguments.length



    Пригождается при поиске багов в точках вызова функции.

    ▍ Использование времени


    ▍ Пропуск загрузки страницы


    Не останавливать выполнение, пока не пройдёт 5 секунд после загрузки страницы:

    performance.now() > 5000

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

    ▍ Пропуск N секунд


    Не останавливать выполнение, если точка останова встречается в течение 5 следующих секунд:

    window.baseline = window.baseline || Date.now(), (Date.now() - window.baseline) > 5000

    Сбрасывайте счётчик из консоли в любой нужный вам момент:

    window.baseline = Date.now()

    ▍ Использование CSS


    Производите остановку на основе вычисленных значений CSS, например, только когда в теле документа используется фон красного цвета:

    window.getComputedStyle(document.body).backgroundColor === "rgb(255,0,0)"

    ▍ Только чётные вызовы


    Остановка только при каждом втором выполнении строки:

    window.counter = (window.counter || 0) + 1, window.counter % 2 === 0

    ▍ Случайная остановка


    Можно производить остановку случайно на основе заданной частоты, например, останавливать только 1 из каждых 10 выполнений:

    Math.random() < 0.1

    ▍ Никогда не останавливаться здесь (Chrome)


    Если кликнуть правой кнопкой в поле gutter и выбрать «Never Pause Here», Chrome создаст условную точку останова, которая всегда будет false, и отладчик на этой строке никогда останавливаться не будет.



    Пригождается, когда вам нужно исключить остановку в определённой строке XHR (XmlHttpRequest), проигнорировать исключение и так далее.

    ▍ Автоматическое присваивание ID экземплярам классов


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

    (window.instances = window.instances || []).push(this)

    Затем для извлечения этого уникального ID нужно выполнить window.instances.indexOf(instance) (например, window.instances.indexOf(this), находясь в методе класса).

    ▍ Программное переключение


    Используйте глобальное логическое значение для активации одной или более условных точек останова:



    Затем программно переключайте это логическое значение, например:

    • Вручную из консоли:

    window.enableBreakpoints = true;

    • Из других точек останова:



    • Из таймера через консоль:

    setTimeout(() => (window.enableBreakpoints = true), 5000);

    • И так далее.

    Отслеживание вызовов класса с помощью monitor() (Chrome)


    В Chrome есть удобный метод командной строки monitor, позволяющий легко отслеживать все вызовы методов класса. Вот пример с классом Dog:

    class Dog {
      bark(count) {
        /* ... */
      }
    }

    Если нам нужно знать все вызовы, совершаемые ко всем экземплярам Dog, то мы выполняем из командной строки следующее:

    var p = Dog.prototype;
    Object.getOwnPropertyNames(p).forEach((k) => monitor(p[k]));

    И получаем в консоли такой вывод:

    > function bark called with arguments: 2

    Также, если вам нужно останавливать выполнение на определённых вызовах метода, а не просто выводить результат в консоль, то вместо monitor используйте debug.

    ▍ Отслеживание вызовов из конкретного экземпляра (Chrome)


    Если вы не знаете класс, но у вас есть его экземпляр:

    var p = instance.constructor.prototype;
    
    Object.getOwnPropertyNames(p).forEach((k) => monitor(p[k]));

    Пригождается, когда вы хотите занести в журнал функцию, которая проделывает это для любого класса (а не только для Dog).

    ▍ Вызов и отладка функции


    Прежде чем вызывать в консоли функцию для отладки, вызовите debugger. Например, если дано:

    function fn() {
      /* ... */
    }
    

    Выполните из консоли:

    > debugger; fn(1);

    И затем «Step into the next function call» для отладки реализации fn.

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

    В Chrome также можно при желании выполнить из командной строки debug(fn), и отладчик будет останавливать выполнение внутри fn при каждом её вызове.

    ▍ Остановка выполнения при изменении URL


    Чтобы остановить выполнение до того, как одностраничное приложение изменит URL (то есть до любого события маршрутизации):

    const dbg = () => {
      debugger;
    };
    history.pushState = dbg;
    history.replaceState = dbg;
    window.onhashchange = dbg;
    window.onpopstate = dbg;

    Ну а создание версии dbg, которая останавливает выполнение, не нарушая навигацию, останется для читателей в качестве упражнения.

    Также заметьте, что вызовы кода window.location.replace/assign здесь не обрабатываются напрямую, поскольку страница после присваивания сразу выгружается, и отлаживать уже нечего.

    Если же вы всё равно хотите увидеть исходный код этих перенаправлений (и отладить состояние в момент этого перенаправления), то в Chrome можете применить debug к сопутствующим методам:

    debug(window.location.replace);
    debug(window.location.assign);

    ▍ Отладка считывания свойств


    Если у вас есть объект, и вы хотите знать, когда в нём считывается свойство, используйте геттер объектов вместе с вызовом debugger. Например, переделайте {configOption: true} в {get configOption() { debugger; return true; }} (в самом исходном коде или с помощью условной точки останова).

    Пригождается, когда вы передаёте куда-то настройки конфигурации и хотите видеть, как они используются.

    ▍ Использование copy() (Chrome, Firefox)


    Вы можете копировать интересующую вас информацию из браузера напрямую в буфер обмена без обрезания строк, используя API copy(). В качестве такой информации может выступать:

    • Снимок текущей DOM: copy(document.documentElement.outerHTML).
    • Метаданные ресурсов (например, изображений): copy(performance.getEntriesByType("resource")).
    • Большой отформатированный blob-объект JSON: copy(JSON.parse(blob)).
    • Дамп localStorage: copy(localStorage).
    • И так далее.

    Отладка HTML/CSS


    При диагностировании проблем с HTML/CSS кодом может пригодиться консоль JS.

    ▍ Инспектирование DOM при отключённом JS


    Находясь в инспекторе DOM, в любой момент нажмите Ctrl+\ (Chrome/Windows), чтобы приостановить выполнение JS-кода. Это позволит вам просмотреть снимок DOM, не беспокоясь о том, что его изменит JS или какие-то события (например, наведение курсора мыши).

    ▍ Инспектирование исчезающего элемента


    Предположим, вы хотите просмотреть элемент DOM, который появляется только при определённых условиях. Для инспектирования такого элемента на него необходимо навести курсор мыши, но когда вы это делаете, он исчезает:


    Чтобы просмотреть его, выполните в консоли: setTimeout(function() { debugger; }, 5000);. Это даст вам 5 секунд для активации UI, затем по прошествии этих 5 секунд выполнение JS приостановится, и уже ничто не заставит нужный элемент исчезнуть. Теперь вы можете навести курсор на инструменты разработки, не потеряв его из виду:



    В момент приостановки выполнения JS вы можете инспектировать элемент, редактировать его CSS-код, выполнять команды в консоли JS и так далее.

    Этот приём пригождается при инспектировании DOM, которая зависит от конкретного положения курсора, выбранного элемента и так далее.

    ▍ Сохранение снимков DOM


    Чтобы сделать копию DOM в её текущем состоянии, используйте:

    copy(document.documentElement.outerHTML);

    Также можете делать снимок DOM каждую секунду:

    doms = [];
    setInterval(() => {
      const domStr = document.documentElement.outerHTML;
      doms.push(domStr);
    }, 1000);

    Или просто выводить снимок в консоль:

    setInterval(() => {
      const domStr = document.documentElement.outerHTML;
      console.log("snapshotting DOM: ", domStr);
    }, 1000);

    ▍ Мониторинг выбранного элемента


    (function () {
      let last = document.activeElement;
      setInterval(() => {
        if (document.activeElement !== last) {
          last = document.activeElement;
          console.log("Focus changed to: ", last);
        }
      }, 100);
    })();



    ▍ Поиск элементов, выделенных жирным


    const isBold = (e) => {
      let w = window.getComputedStyle(e).fontWeight;
      return w === "bold" || w === "700";
    };
    Array.from(document.querySelectorAll("*")).filter(isBold);

    ▍ Отслеживание только потомков


    Можно мониторить только потомков элемента, выбранного в инспекторе:

    Array.from($0.querySelectorAll("*")).filter(isBold);

    ▍ Ссылка на выбранный элемент


    Использование $0 в консоли станет автоматической ссылкой на выбранный в инспекторе элемент.

    ▍ Ссылка на предыдущие элементы (Chrome, Edge)


    В Chrome и Edge можно обращаться к последнему проинспектированному элементу с помощью $1, к элементу перед ним с помощью $2 и так далее.

    ▍ Получение слушателей событий (Chrome)


    В Chrome можно инспектировать слушателей событий выбранного элемента с помощью getEventListeners($0). Например, так:



    ▍ Мониторинг связанных с элементом событий (Chrome)


    Отладка всех событий, связанных с выбранным элементом: monitorEvents($0)
    Отладка конкретных событий, связанных с выбранным элементом: monitorEvents($0, ["control", "key"])

    ▍ Сноска


    1. Если для приёма не указаны поддерживающие его браузеры, то он по умолчанию работает в Chrome, Firefox и Edge.

    Telegram-канал со скидками, розыгрышами призов и новостями IT ?

Комментарии (9)


  1. Mingun
    14.09.2024 06:53
    +1

    Гм, а как-то можно отключить приостановку на точках останова, созданных debugger;? Несколько раз сталкивался с таким антиотладочным приёмом, что выполняется код вида:

    function e() { debugger; setTimeout(e, 0); }
    

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


    1. venanen
      14.09.2024 06:53
      +1

      Смотря где эта функция лежит. Можно попробовать ее переопределить.
      Можно так попробовать: https://stackoverflow.com/questions/8635502/how-do-i-clear-all-intervals


      1. Mingun
        14.09.2024 06:53
        +1

        Её не переопределить было, она динамически генерировалась, через eval(). Деталей не помню, давненько это уже было. И хак с очисткой интервалов не сработал (я тогда тоже вышел на этот вопрос со Stack Overflow), потому что тот eval() тоже в какой-то цикл запрятан был, видимо.


    1. claimc
      14.09.2024 06:53
      +1

      При первом срабатывании выполнить в консоли e = ()=>{}, чтобы повторных срабатываний небыло.


      1. Mingun
        14.09.2024 06:53
        +1

        Это не сработает. Код там сложнее был и функция то ли была безымянной, то ли имя генерировалось случайным образом, в общем, просто «занипить» требуемую функцию было невозможно.


    1. yrub
      14.09.2024 06:53
      +2


      1. Mingun
        14.09.2024 06:53

        Да, в современном Firefox (как минимум, с версии 115.15.0esr), тоже есть такая кнопка и она действительно эффективно отключает debugger;. Правда, и все остальные точки тоже, но хотя бы так. А ещё в контекстном меню отладчика есть пункт «Игнорировать строку», который также успешно отключил точку останова в модельном примере. Не знаю, правда, сработает ли он на минифицированном исходнике, когда всё в одну строку записано, да и против динамической генерации функции тоже, скорее всего не поможет. Но тогда, когда я столкнулся с такой проблемой, не было и этого!


    1. mysherocker
      14.09.2024 06:53
      +1

      многие антиотладочные приёмы можно обойти, переопределив ключевые объекты до старта отлаживаемого кода:

      window.debugger = function ()  {};


      1. Mingun
        14.09.2024 06:53
        +1

        Это не работает. Во-первых, debugger; — никакая не функция, а специнструкция. А во-вторых, вы не можете переопределить её «до старта отлаживаемого кода», потому что этот самый код запускается сразу же с загрузкой страницы, просто пока инструменты разработчика не открыты, инструкция ничего не делает.