Как отслеживать изменения поля объекта без изменения исходного кода?
Что проверяют
Знаете ли вы механизмы метапрограммирования в JavaScript и их ограничения. Обычно следом спрашивают, чем Proxy отличается от Object.defineProperty и как на этом построена реактивность во Vue.
Как отвечать
Два основных способа, и важно сказать, чем они различаются:
Object.defineProperty: заменяете существующее поле на пару геттер и сеттер прямо на исходном объекте. Работает для всех, кто уже держит ссылку на объект, но только для известных заранее полей и только если свойствоconfigurable. Новые поля иdeleteне отслеживаются. На этом строилась реактивность Vue 2, отсюда иVue.set;Proxy: обёртка с ловушкамиset,get,deleteProperty. Ловит любые поля, включая новые, но только при обращении через прокси: код со ссылкой на исходный объект меняет его мимо ловушек. Так устроеныreactiveво Vue 3 и наблюдаемые объекты MobX.
В ловушке set пишите через Reflect.set(target, key, value, receiver) и возвращайте его результат: так сохраняются сеттеры и наследование, а в строгом режиме false из ловушки превращается в TypeError.
Ограничения, которые стоит назвать:
- глубина: прокси поверхностный, вложенные объекты оборачивают лениво в ловушке
get; - идентичность:
proxy !== target, это ломает сравнения и поиск вWeakMap, где ключом лежит оригинал; - встроенные объекты и приватные поля: методы
Map,Set,Dateи классы с#privateпри вызове через прокси бросаютTypeError, методы приходится привязывать к исходному объекту; - массивы:
pushвызываетsetдважды, для индекса и дляlength.
Для отладки подойдёт defineProperty с debugger в сеттере: DevTools остановится ровно там, где поле меняют.
Где спотыкаются
Оборачивают объект в Proxy, а остальной код продолжает работать с оригиналом. Изменения идут мимо, и кажется, что прокси «не работает».
Забывают вернуть true из set. В модулях и классах действует строгий режим, и присваивание падает с TypeError.
Предлагают Object.observe или MutationObserver. Первый так и не вошёл в стандарт и давно убран из браузеров, второй следит только за DOM, а не за обычными объектами.