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

Ты делаешь крестики-нолики. Ставишь крестик в левый верхний угол. И крестик появляется в левом столбце целиком — во всех трёх рядах сразу. Ты поставил один. Код показывает три.
Это не глюк отрисовки. В памяти действительно один ряд — просто на него смотрят три разных места. Поменял через одно — увидели все.
Симптом всегда один: «меняю один элемент — меняются все». А причин у новичков обычно три. Начнём с самой частой.
Сначала: как проверить, что дело в этом
Одна строчка в консоли:
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 — это всегда решение?
Нет, это крайняя мера. Глубокая копия медленнее и не копирует функции. Обычно достаточно создавать объекты в нужном месте, а не копировать готовые.
Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.





