Гайды

Почему меняю один элемент, а меняются все — три причины и как починить

Иллюстрация: три марионетки на одной общей нитке — дёрнешь одну, поднимают руку все три

Ты делаешь крестики-нолики. Ставишь крестик в левый верхний угол. И крестик появляется в левом столбце целиком — во всех трёх рядах сразу. Ты поставил один. Код показывает три.

Это не глюк отрисовки. В памяти действительно один ряд — просто на него смотрят три разных места. Поменял через одно — увидели все.

Симптом всегда один: «меняю один элемент — меняются все». А причин у новичков обычно три. Начнём с самой частой.

Сначала: как проверить, что дело в этом

Одна строчка в консоли:

console.log(grid[0] === grid[1])

Для объектов и массивов === проверяет не содержимое, а адрес. true — значит, это буквально один и тот же ряд под двумя номерами. Ниже — откуда такие близнецы берутся. Почему переменная хранит адрес, а не саму вещь, разобрано в статье что такое переменная.

Причина 1: fill положил во все ячейки один объект

Самый короткий способ сделать доску 3×3 — и самый коварный:

const grid = Array(3).fill([])
grid[0].push("X")
// [["X"], ["X"], ["X"]]

fill не создаёт три новых массива. Он создаёт один [] и кладёт ссылку на него в каждую ячейку. Три ячейки — одна коробка.

Как проверить. grid[0] === grid[1] даёт true.

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

const grid = Array.from({ length: 3 }, () => [])

() => [] срабатывает три раза — три разных массива.

В Python та же ловушка выглядит так: [[0] * 3] * 3. Умножение списка копирует ссылки. Поменяешь board[0][0] — поменяется первый элемент во всех рядах. Чинится генератором: [[0] * 3 for _ in range(3)].

fill с числами и строками безопасен: Array(3).fill(0) — нормальный код. Беда только с объектами и массивами.

Причина 2: один объект переиспользуется в цикле

Собираешь список строк для таблицы:

const rows = []
const row = {}
for (const name of ["Аня", "Боря", "Вика"]) {
  row.name = name
  rows.push(row)
}
// [{name: "Вика"}, {name: "Вика"}, {name: "Вика"}]

Три раза положил в список один и тот же row. На каждом шаге ты его перезаписывал. В конце в нём последнее имя — и его видят все три строки.

Эта ошибка особенно любит ИИ-код, где объект «вынесли наверх ради аккуратности».

Как проверить. Все элементы одинаковые и равны последнему значению. Это главный признак.

Как починить. Создавай новый объект внутри цикла:

for (const name of names) {
  rows.push({ name })
}

Или короче: const rows = names.map((name) => ({ name })).

Причина 3: общий объект «по умолчанию»

В Python есть известная ловушка:

def add(item, items=[]):
    items.append(item)
    return items

add(1)  # [1]
add(2)  # [1, 2] — а ждали [2]

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

В JavaScript function add(item, items = []) так не ломается — там значение по умолчанию создаётся заново на каждый вызов. Но та же беда приходит иначе: общий объект настроек, вынесенный в константу.

const DEFAULTS = { tags: [] }
const a = { ...DEFAULTS }
a.tags.push("new")
// DEFAULTS.tags теперь тоже ["new"]

Спред ... скопировал только верхний уровень. Массив tags остался общим для всех, кто взял «настройки по умолчанию».

Как починить. В Python — items=None, а внутри if items is None: items = []. В JavaScript — делай дефолты функцией, которая каждый раз собирает новый объект: const makeDefaults = () => ({ tags: [] }). Или глубокую копию: structuredClone(DEFAULTS).

Почему это вообще важно

Похожий баг в React выглядит иначе: ты поменял объект на месте — а экран не обновился. React сравнивает именно адреса. Тот же адрес — значит, «ничего не изменилось». Подробнее — в статье про состояние.

Общее правило на все три случая: если нужен новый объект — его надо явно создать. Скобки {} или [] должны выполниться столько раз, сколько тебе нужно отдельных копий.

Попроси ИИ проверить свой код таким промптом: «Найди места, где несколько элементов массива могут ссылаться на один и тот же объект: fill с объектом, объект, созданный вне цикла, общий объект по умолчанию».

Почему с числами такого не бывает?

Числа, строки и true/false копируются целиком. Объекты и массивы передаются по адресу. Поэтому fill(0) безопасен, а fill([]) — нет.

structuredClone — это всегда решение?

Нет, это крайняя мера. Глубокая копия медленнее и не копирует функции. Обычно достаточно создавать объекты в нужном месте, а не копировать готовые.

Учись вайб-кодингу, а не просто читай о нём

Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.

Открыть приложение
Робот KODiQ

ИИ-редактор KODiQ. Пишет про вайб-кодинг и AI-инструменты простым языком — каждый день.

Все статьи →