Привет! Cегодня расскажу, как можно с помощью отладчика решить, на мой взгляд, нетривиальную проблему JavaScript.

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

Часто такое поведение искажает данные пользователя, приводит к ошибкам и является нежелательным.

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

Давайте рассмотрим простой пример.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Debug property mutation example</title>
    <script>
        const user = {
            firstName: 'Vasilij',
            middleName: 'Alibabaevich',
            lastName: 'Radner',
            aka: 'Alibaba',
            getFullName() {
                return `${this.lastName} ${this.firstName.slice(0, 1)}. ${this.middleName.slice(0, 1)}.`
            }
        };
    </script>
    <script src="object-property-mutation.js" type="application/javascript"></script>
</head>
</head>
<body>
<script>
    Promise.resolve(user).then(user.getFullName.bind(user)).then(console.log);
</script>
</body>
</html>


Сейчас веб программа не работает, так как в консоли есть ошибка и нет вывода ФИО.

image

Читаем самое верхнее сообщение в консоли:

Uncaught (in promise) TypeError: Cannot read property 'slice' of undefined

Не обработано (в промис) ошибка типа: не могу прочитать свойство slice от неопределено.

Нажимаем на ссылку и переходим к месту ошибки.

getFullName() {
                return `${this.lastName} ${this.firstName.slice(0, 1)}. ${this.middleName.slice(0, 1)}.`
}

Видим, что ошибочное выражение
this.firstName.slice(0, 1)
состоит из четырех операторов:

  1. два оператора точка
  2. один оператор-разделитель запятая
  3. один оператор группировки — пара круглых скобок

Давайте читать инструкцию. Первым вычисляется левое выражение

this.firstName

Оно состоит из оператора точка, слева первичное выражения this и идентификатора firstName справа. Результатом этого выражения будет undefined. Выполнение следующего оператора точка вызывает ошибку. Так как оператор точка работает только с объектными типами, его выполнение от undefined приводит к ошибке — не могу получить свойство slice от undefined.

Получается, что где-то этому свойству было присвоено значение undefined…

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

Выбираем инструмент остановка на исключении


image

Видим, что в стеке всего два вызова. Переходим в предыдущий вызов.

image

Видим, что нет явной инструкции, которая изменяют свойство firstName.

image

Делаем вывод, что изменение не происходит в этом стеке вызовов.

Как вам такое?


И как найти негодяя который изменил свойство моего объекта?

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

Мне очень интересно узнать, как другие специалисты JavaScript решают подобные проблемы.

Знаете, когда я встретился с этим поведением JavaScript впервые, я потратил пару часов на расследование и выдрал клок волос с челки…

Отключаем инструмент пауза на исключении.

Итак, вот наш новый план мы определим в объекте user свойство firstName, используя геттер и сеттер.

В сеттер добавим инструкцию отладки, используя оператор debugger и оператор точка с запятой.

const user = {
            _firstName: 'Vasilij',
            set firstName(value) {
                debugger;
                this._firstName = value;
            },
            get firstName() {return this._firstName},
            middleName: 'Alibabaevich',
            lastName: 'Radner',
            aka: 'Alibaba',
            getFullName() {
                return `${this.lastName} ${this.firstName.slice(0, 1)}. ${this.middleName.slice(0, 1)}.`;
            }
        };

Двигаясь дальше по стеку вызовов, найдем инструкцию, которая меняет свойство firstName.

Отладчик остановился в сеттере до того, как новое значение будет записано в объект.

image

Видим, что значение параметра value undefined.

Теперь, используя стек вызовов мы легко переходим в предыдущий вызов.

image

иииии, победааааа ура.

Есть еще более простой способ решить эту проблему, используя инструмент отладки остановка на исключении.

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

Так как мы знаем, что при попытке получения свойства от undefined возникает ошибка.

Включаем инструмент остановка на исключении, присваиваем переменной user значение undefined.

    const user = undefined;

Мы снова остановились в месте искажения свойства firstName.

image

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

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

github.com/NVBespalov/js-lessons/tree/error/property-mutation

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


  1. JustDont
    25.12.2019 01:25
    +1

    Мне очень интересно узнать, как другие специалисты JavaScript решают подобные проблемы.

    Пишут на typescript без any и с тайпгардами на всех входах-выходах из кода TS.
    Субъективщина конечно, но вот лично мне это нравится куда больше обнимашек с дебаггером.

    ЗЫ: Те, кто слабее духом, но всё же достаточно сильны, чтоб не обниматься с дебаггером — пишут тесты.


  1. tbl
    25.12.2019 01:53
    +1

    Еще может быть такое, что объект проедет по цепочке JSON.stringify и JSON.parse, растеряв по дороге часть пропертей, а потом, еще и смимикрирует по прототипу под первоначальный объект через услужливую соломку в виде Object.create. В итоге у тебя выродится где-то внутрях костыльного фреймворка (правда, будет не так концентрированно, а размажется по 20-30 методам):

    function lookBroIveLostSomeProperty(someObject: SomeObject): SomeObject {
      const {
        someProperty,
        ...oO
      } = JSON.parse(JSON.stringify(someObject));
    
      return Object.assign(Object.create(SomeObject.prototype), oO);
    }
    


  1. Cerberuser
    25.12.2019 05:42

    А я, когда мне надо было понять, где дёргается свежесозданный объект, просто подменял его на его копию, завёрнутую в Proxy. Поставил внутри геттера/сеттера (в зависимости от конкретной задачи — мне надо было внутри геттера) либо debugger, либо console.trace, — и всё как на ладони, даже в минифицированном коде.


    1. JustDont
      25.12.2019 09:57

      Кстати да, если надо разобраться в километрах лапши — подставить вместо переменных Proxy (или там observable из MobX, которые представляют собой ту же прокси) это отличное решение.


  1. XAHTEP26
    25.12.2019 12:35
    +1

    Другие специалисты JavaScript просто не засоряют глобальное пространство имен.


    1. Sirion
      25.12.2019 12:54

      Другие специалисты. Только после вашего комментария обратил внимание на эту чудесную фразу)