Каждое окно и каждая вкладка браузера — это отдельный, изолированный мир. У них свой объект window, свой JavaScript-контекст, и напрямую дотянуться из одного до переменных другого нельзя — иначе любой открытый сайт читал бы данные соседней вкладки. Но задача «передать что-то из одного окна в другое» встречается постоянно: авторизация в попапе, который должен вернуть токен на основную страницу; виджет в отдельном окне; синхронизация корзины между вкладками интернет-магазина.

Для этого в браузере есть несколько штатных каналов связи. Разберём четыре подхода — от классической пары «окно + попап» до широковещательной рассылки на все вкладки сайта — и в конце сведём их в таблицу, чтобы было понятно, что под какую задачу брать.

Открываем окно и держим на него ссылку

Начнём с самого прямого сценария: главная страница открывает дочернее окно и общается с ним. Метод window.open() не просто открывает попап — он возвращает ссылку на его объект window. Эту ссылку и нужно сохранить, через неё пойдёт вся связь.

let popup;

function openPopup() {
  const options = "width=360,height=420,resizable=no,menubar=no,toolbar=no";
  popup = window.open("/widget.html", "widget", options);
}

Третий аргумент — строка с параметрами окна (размеры, наличие тулбаров) без пробелов, через запятую. Второй аргумент — имя окна: если открыть open() с тем же именем ещё раз, браузер переиспользует уже открытое окно, а не создаст новое.

Обратная ссылка тоже есть: у дочернего окна свойство window.opener указывает на то окно, которое его открыло. Так что связь потенциально двусторонняя — родитель знает про ребёнка через переменную popup, ребёнок про родителя через opener.

postMessage: двусторонний канал

Иметь ссылку на чужое окно — не значит читать его переменные напрямую (браузер это запретит для разных источников). Легальный способ обмена — метод postMessage(): одно окно отправляет данные, другое ловит их событием message.

Отправляем из главного окна в попап:

function greetPopup() {
  popup.postMessage(
    { type: "greet", text: "Привет из главного окна" },
    window.location.origin
  );
}

Передавать можно не только строки: аргумент клонируется по алгоритму структурного клонирования, поэтому спокойно уходят объекты, массивы, Map, Date и прочее (кроме функций и DOM-узлов).

Ловим сообщение на принимающей стороне — это работает одинаково и в попапе, и в родителе, обмен данными между окнами вообще идёт через одно и то же событие message объекта window:

window.addEventListener("message", (event) => {
  if (event.data?.type === "greet") {
    console.log(event.data.text);
  }
});

Из попапа назад в родителя отправляем через opener — и, например, тут же закрываем окно:

function reply(text) {
  window.opener.postMessage({ type: "reply", text }, window.location.origin);
  window.close();
}

Поле type в объекте — не требование API, а удобная привычка: на один и тот же слушатель обычно прилетают разные сообщения, и по типу их проще разбирать.

Не ставь звёздочку: targetOrigin и проверка origin

Второй аргумент postMessage() — это targetOrigin, то есть источник, которому сообщение адресовано. В примерах из интернета там часто стоит "*" — «отправить кому угодно». Это удобно и опасно: если пользователь перейдёт в попапе на чужой сайт, сообщение с вашими данными уедет ему.

Правильно — указывать конкретный источник. Тогда браузер доставит сообщение, только если окно действительно на нём находится:

// уедет, только если popup сейчас на нашем origin
popup.postMessage(payload, "https://example.com");
// на своём же сайте удобно так:
popup.postMessage(payload, window.location.origin);

Симметрично — на приёме. Событие message может прийти откуда угодно, в том числе от постороннего окна, поэтому первым делом проверяем event.origin и игнорируем чужаков:

window.addEventListener("message", (event) => {
  if (event.origin !== window.location.origin) return; // чужой источник
  handle(event.data);
});

Помимо origin и data, у события есть event.source — ссылка на окно-отправитель. Через неё удобно ответить, не заводя отдельную переменную: event.source.postMessage(...).

Вкладки одного сайта: BroadcastChannel

Подход с opener хорош, когда одно окно открыло другое и держит на него ссылку. Но если у пользователя просто открыто три вкладки вашего магазина, прямых ссылок между ними нет — и передать «в корзину добавили товар» сразу во все становится неудобно.

Для этого случая есть BroadcastChannel — общая «радиостанция» для всех вкладок, окон и даже воркеров одного источника. Кто подписался на канал с тем же именем — тот и слышит сообщения. Никаких ссылок на конкретные окна не нужно.

const channel = new BroadcastChannel("cart");

// одна вкладка положила товар и вещает об этом
channel.postMessage({ type: "add", id: 42 });

// все остальные вкладки слушают
channel.onmessage = (event) => {
  if (event.data.type === "add") {
    updateCartBadge(event.data.id);
  }
};

// когда канал больше не нужен
channel.close();

Важная деталь: отправитель своё сообщение не получает — onmessage срабатывает только у других подписчиков канала. Это ровно то, что нужно для синхронизации: вкладка, которая уже обновила своё состояние, повторно его не обрабатывает.

Поддержка браузерами
chrome
Chrome
54
firefox
Firefox
38
edge
Edge
79
safari
Safari
15.4
opera
Opera
41

Классика: событие storage

Ещё до BroadcastChannel вкладки синхронизировали через localStorage. Приём такой: одна вкладка что-то пишет в хранилище, а во всех остальных вкладках того же источника срабатывает событие storage.

// в любой вкладке меняем значение
localStorage.setItem("theme", "dark");

// во всех прочих вкладках прилетит событие
window.addEventListener("storage", (event) => {
  if (event.key === "theme") {
    applyTheme(event.newValue); // "dark"
  }
});

Здесь та же логика «не услышу сам себя»: событие storage не срабатывает в той вкладке, которая сделала запись, — только в соседних. На этом часто спотыкаются, когда пытаются проверить обработчик в одной вкладке и удивляются, что он молчит.

У этого способа есть плюс, которого нет у остальных: раз данные лежат в localStorage, они переживают перезагрузку и доступны вкладке, которую откроют потом. Минус — передавать так можно только то, что сериализуется в строку, и ради чистого обмена сообщениями (без сохранения) тащить данные в хранилище уже незачем — для этого есть BroadcastChannel. О том, какие вообще бывают хранилища в браузере и чем они отличаются, есть отдельный разбор.

Что выбрать

Короткая шпаргалка по четырём каналам:

Способ Когда подходит Разные источники?
postMessage + opener Окно открыло попап или встроило iframe и держит на него ссылку Да, для того и создан
BroadcastChannel Несколько вкладок/воркеров одного сайта, прямых ссылок между ними нет Нет, только один источник
Событие storage Синхронизация вкладок + нужно, чтобы данные пережили перезагрузку Нет, только один источник

Практическое правило: если общаются окна разных сайтов (попап авторизации, сторонний iframe) — это только postMessage с обязательной проверкой origin. Если это вкладки одного вашего сайта — берите BroadcastChannel, а storage оставьте на случай, когда обмен нужно совместить с сохранением состояния.

Итог

Изоляция окон — это защита, а не преграда: браузер даёт штатные каналы, чтобы пробить её осознанно. Между окном и его попапом/iframe работает postMessage — главное не лениться и указывать конкретный targetOrigin, а на приёме проверять event.origin. Для вкладок одного сайта проще и чище BroadcastChannel, а старое доброе событие storage выручает, когда обмен нужно совместить с сохранением данных.