Мой путь к взлому приложения и как этого избежать

Перво-наперво

Прежде чем перейти к тому, как я сломал веб-приложение, которое я создавал, что побудило меня написать эту статью, давайте разберемся, что означают Deep Copy и Shallow Copy и как JavaScript управляет ими внутри.

Неглубокое копирование

Допустим, вы создаете два объекта, A и B, и назначаете им некоторые свойства.

A = {
name: "Johny",
Age: "47
}
B = {
name: "Annie",
age: 42
}

Они одинаковы? Ну нет. Они не выглядят одинаково, и, конечно же, JavaScript здесь согласуется со здравым смыслом, что часто может быть не так.

Что, если мы назначим B на A?

A = B

Теперь они одинаковы, у них одни и те же свойства и, может быть, одна и та же ссылка на память? Не будем забегать вперед, но Javascript действительно считает их одинаковыми, как и планировалось.

Но вы знаете, что? Это была ошибка, я никогда не хотел иметь две копии Б, ничего личного, Энни. К счастью, я помню, что такое свойства A, так что давайте вернемся к этому, назначив их нашим ключам A Object.

A.name = "Johny"
A.age = 47

Отлично, теперь у нас вернулся наш друг Джонни. Надеюсь, A напечатает только что обновленные атрибуты, которые мы ему дали.

Что это? Только что звонила Энни, и теперь она чувствует себя как Джонни.

Что здесь случилось? Ну, мелкая копия. Помните наш первый рисунок? Когда мы назначили B на A

A = B

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

Что это значит? Что ж, это означает, что они являются одним и тем же Объектом во всех отношениях, которые вы можете себе представить; если вы обновите, удалите или добавите значения в A или B, те же изменения будут отражены в аналоге.

B.name = "Johny"

Это существенно изменяет значение, хранящееся в памяти. Поскольку одна и та же память используется как A, так и B, значение параметра «имя» будет одинаковым для A и B.

Если мы добавим новый ключ к B, A также будет обновлен:

А теперь давайте сбросим оба наших объекта A и B. Поскольку они «одинаковые», мы можем просто сказать:

A = {}

И они оба станут пустыми… или будут?

Они не будут. Нашему пустому объекту {} назначена своя собственная область памяти, что означает, что A теперь указывает на эту область и отделена от B, поскольку они больше не ссылаются на один и тот же участок памяти.

Когда дело доходит до мелких копий, есть множество примеров, с которыми вы можете поиграть, чтобы проверить, насколько интуитивными они иногда могут быть. Довольно легко испортить все приложение, если вы не будете осторожны, верно? Но прежде чем мы туда доберемся…

Глубокая копия

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

Существуют различные способы достижения глубокого копирования.

Самый простой — использовать расширенный синтаксис, который создает совершенно новый объект со свойствами существующего объекта, давайте посмотрим.

A = {name: "Johny", age: 47}
B = A
C = {...A}

Давайте посмотрим, что Javascript думает о наших недавно созданных объектах.

Однако не все так, как кажется:

Наш оператор спреда выполнил свою работу, и мы получили два объекта с одинаковыми свойствами, которые не совпадают. И мы можем безопасно изменять свойства A, не отражаясь на C.

Доступны различные другие методы, для более глубокого их объяснения я настоятельно рекомендую Документацию MDN по глубокому копированию.

Как я сломал свое приложение?

Вы могли бы подумать

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

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

Давайте рассмотрим более простой пример, демонстрирующий, в чем именно заключалась основная проблема. Я поделюсь всем кодом, чтобы вы могли открыть свою любимую IDE и создать файл .js, чтобы следовать дальше.

Итак, у меня было два массива. Оба сохраняли элементы, которые я извлекал из API, но у них было ключевое отличие: в то время как один был изменчивым и подвергался изменениям, другой должен оставаться прежним на случай, если я захочу сбросить первый. Давайте запишем это, используя datausa.io API.

Давайте проверим консоль, чтобы увидеть, что мы получаем:

Звучит правильно. Оба массива печатаются одинаково, давайте изменим год 2020 нашего mutableArray на наш старый добрый объект A и console.log() в обоих массивах.

mutableArray.data[0] = {name: "Johny", age: 47}
console.log(mutableArray.data[0], originalArray.data[0])

И я могу поспорить, что вы уже можете догадаться, куда это идет.

Что происходит? FetchUSAData() возвращает объект Javascript, данные, которые ссылаются на определенную область памяти, а затем мы указываем как originalArray, так и mutableArray ссылаться на одну и ту же область. Это означает, как мы видели ранее в этой статье, что изменение одного из массивов также изменит другой.

Как мы можем это исправить? Ранее мы видели, что можно исправить с помощью синтаксиса распространения «…», но мы также можем использовать комбинацию методов JSON stringify() и parse():

mutableArray = JSON.parse(JSON.stringify(data))

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

Ограничения представленных методов

Как синтаксис распространения «…», так и JSON.parse(JSON.stringify(Object)) имеют свои ограничения.

Распространение синтаксиса

В качестве примера возьмем следующий объект А.

У Объекта А есть имя, возраст и Объект, называемый хобби. Давайте глубоко скопируем A в новый объект с именем B, используя наш синтаксис распространения «…».

B = {...A}

И мы получаем копию A, как и планировалось. Но знаете что, давайте изменим имя и хобби Объекта А, по правде говоря, я не могу быть лучшим в Magic The Gathering, и я бы предпочел, чтобы меня звали Бенни.

A.name = "Benny"
A.hobbies[1].expertise = "A little above average"
console.log(B)

А наш Б выглядит так:

Похоже, имя не изменилось, что и должно было случиться, поскольку мы только что узнали, что синтаксис распространения «…» создает глубокую копию объекта, однако… хобби изменились?

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

Вы можете обойти это ограничение, используя синтаксис распространения «…» для каждого уровня данных, в этом случае:

B = {...A, hobbies.{...A.hobbies}}

Это работает.. но какой ценой? Это решение быстро становится уродливым, поскольку ваши объекты становятся все более и более вложенными.

Parse() и Stringify() объекта

Этот метод работает в большинстве случаев, включая описанные выше. Однако он потерпит неудачу, если ваш объект не сериализуем, что часто бывает не так. Наиболее распространенные примеры несериализуемых объектов, с которыми вы столкнетесь:

Классы
Методы класса
Функции
Узлы DOM

Заключение

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

Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord . Заинтересованы в хакинге роста? Ознакомьтесь с разделом Схема.