В 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>
Сейчас веб программа не работает, так как в консоли есть ошибка и нет вывода ФИО.
Читаем самое верхнее сообщение в консоли:
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)
состоит из четырех операторов:- два оператора точка
- один оператор-разделитель запятая
- один оператор группировки — пара круглых скобок
Давайте читать инструкцию. Первым вычисляется левое выражение
this.firstName
Оно состоит из оператора точка, слева первичное выражения this и идентификатора firstName справа. Результатом этого выражения будет undefined. Выполнение следующего оператора точка вызывает ошибку. Так как оператор точка работает только с объектными типами, его выполнение от undefined приводит к ошибке — не могу получить свойство slice от undefined.
Получается, что где-то этому свойству было присвоено значение undefined…
Чтобы решить эту проблему попробуем пойти от обратного. Воспользуемся инструментом отладки остановка на исключении. Двигаясь от места ошибки по стеку вызовов вниз, попробуем перейти к инструкции, которая изменила свойство.
Выбираем инструмент остановка на исключении
Видим, что в стеке всего два вызова. Переходим в предыдущий вызов.
Видим, что нет явной инструкции, которая изменяют свойство firstName.
Делаем вывод, что изменение не происходит в этом стеке вызовов.
Как вам такое?
И как найти негодяя который изменил свойство моего объекта?
Пожалуйста, напишите в комментах как бы вы нашли его?
Ребята, кто со мной работает и кому я это рассказал, напишите плиз звездочку в комменте.
Мне очень интересно узнать, как другие специалисты 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.
Отладчик остановился в сеттере до того, как новое значение будет записано в объект.
Видим, что значение параметра value undefined.
Теперь, используя стек вызовов мы легко переходим в предыдущий вызов.
иииии, победааааа ура.
Есть еще более простой способ решить эту проблему, используя инструмент отладки остановка на исключении.
Вот наш новый план: сделаем объект user не объектом и, используя инструмент остановку по исключению, легко попадем к ошибочной инструкции.
Так как мы знаем, что при попытке получения свойства от undefined возникает ошибка.
Включаем инструмент остановка на исключении, присваиваем переменной user значение undefined.
const user = undefined;
Мы снова остановились в месте искажения свойства firstName.
Это все, что я хотел вам сегодня рассказать об отладке нежелательных изменений у объектов.
Спасибо за прочтение статьи. Ставьте лайки, подписывайтесь на канал, делитесь этим видео и статьей с друзьями, всего хорошего.
github.com/NVBespalov/js-lessons/tree/error/property-mutation
Комментарии (6)
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); }
Cerberuser
25.12.2019 05:42А я, когда мне надо было понять, где дёргается свежесозданный объект, просто подменял его на его копию, завёрнутую в Proxy. Поставил внутри геттера/сеттера (в зависимости от конкретной задачи — мне надо было внутри геттера) либо debugger, либо console.trace, — и всё как на ладони, даже в минифицированном коде.
JustDont
25.12.2019 09:57Кстати да, если надо разобраться в километрах лапши — подставить вместо переменных Proxy (или там observable из MobX, которые представляют собой ту же прокси) это отличное решение.
JustDont
Пишут на typescript без any и с тайпгардами на всех входах-выходах из кода TS.
Субъективщина конечно, но вот лично мне это нравится куда больше обнимашек с дебаггером.
ЗЫ: Те, кто слабее духом, но всё же достаточно сильны, чтоб не обниматься с дебаггером — пишут тесты.