Скрипт на странице всегда выполняется «внутри» какого-то окна: вкладки, попапа или iframe. Это окно в JavaScript представлено объектом window, и у него две роли одновременно. Во-первых, это глобальный объект: именно в нём оказываются глобальные переменные и функции вроде parseInt() или setTimeout(). Во-вторых, через него доступна BOM (Browser Object Model, объектная модель браузера) — набор объектов для всего, что находится за пределами самой страницы: адресной строки, истории переходов, экрана, самого браузера.

Официального стандарта под названием «BOM» нет: это собирательное имя. Но почти всё, о чём пойдёт речь, давно описано в спецификации HTML и одинаково работает во всех браузерах. Разберём по порядку: как устроена глобальная область, что лежит в location, history, navigator и screen, как открывать окна и показывать диалоги — и какими API эти вещи сегодня лучше заменять. События окна (load, resize, scroll и другие) здесь не повторяем: для них есть отдельный гид по событиям объекта window.

window как глобальный объект

У каждой среды выполнения JavaScript есть свой глобальный объект. В браузере это window, в Node.js — global, в веб-воркере — self. Встроенные функции — его методы, поэтому две строки ниже делают одно и то же:

window.setTimeout(() => console.log('Прошла секунда'), 1000);
setTimeout(() => console.log('Прошла секунда'), 1000);

Обычно префикс window. опускают: писать короче, и код легче переносится в другую среду, где такого объекта нет.

Какие переменные попадают в window, а какие нет

Часто говорят, что «все глобальные переменные — это свойства window». На деле это правда только наполовину и зависит от того, как переменная объявлена:

var visits = 1;
let total = 2;
const LIMIT = 3;
function greet() {}

console.log(window.visits); // 1
console.log(window.greet);  // ƒ greet() {}
console.log(window.total);  // undefined
console.log(window.LIMIT);  // undefined

console.log(total); // 2 — переменная глобальная, просто не свойство window

В window оказываются только var и объявления функций. Глобальные let, const и class видны во всех обычных скриптах страницы, но хранятся отдельно и свойствами окна не становятся. А в скриптах с type="module" на верхнем уровне объявляется переменная модуля, а не глобальная.

Отдельная история — присваивание без объявления вообще:

function setScore() {
  score = 10; // опечатка или забытый let
}

setScore();
console.log(window.score); // 10 — утекло в глобальную область

В нестрогом режиме такая строка молча создаёт свойство на window. В строгом режиме (а также внутри модулей и классов, где он включён автоматически) она бросает ReferenceError, и ошибка сразу видна. Почему это одна из главных причин существования 'use strict' — в разборе строгого режима.

Как проверить, что глобальная переменная существует

Обращение к необъявленному имени бросает ошибку, поэтому такая проверка сама роняет скрипт:

if (analytics) { // ReferenceError: analytics is not defined
  analytics.track('open');
}

Раньше вместо этого писали if (window.analytics): чтение несуществующего свойства не бросает ошибку, а возвращает undefined. Приём рабочий, но с двумя дырами: он не видит глобальные let/const и считает «несуществующей» переменную со значением 0, '' или false. Надёжнее оператор typeof: для необъявленного имени он возвращает строку 'undefined' без исключения.

if (typeof analytics !== 'undefined') {
  analytics.track('open');
}

// если нужно именно свойство окна (например, глобал от сторонней библиотеки)
if ('dataLayer' in window) {
  window.dataLayer.push({ event: 'open' });
}

globalThis: одно имя для любой среды

Если код должен работать и в браузере, и в Node.js, и в воркере, привязываться к window нельзя. Старый трюк — взять this на верхнем уровне скрипта:

const root = this; // window — но только в обычном скрипте

Этот способ ломается в модулях: там this на верхнем уровне равен undefined. Внутри функций в строгом режиме тоже. Для этой задачи в язык добавили globalThis — ссылку на глобальный объект, которая одинаково называется в любой среде:

globalThis === window; // true в браузере
globalThis === global; // true в Node.js
globalThis === self;   // true в веб-воркере

globalThis.appConfig = { theme: 'dark' }; // доступно отовсюду

location: адрес текущей страницы

Свойство window.location хранит объект с URL (адресом) страницы, разложенным на части. Удобнее всего увидеть их на одном адресе, где есть всё сразу:

// открыта страница https://shop.example.com:8080/catalog/shoes?color=red&size=42#reviews
console.log(location.pathname); // '/catalog/shoes'
Свойство Значение Что это
href весь адрес целиком полный URL строкой
protocol 'https:' протокол, с двоеточием на конце
host 'shop.example.com:8080' домен вместе с портом
hostname 'shop.example.com' только домен
port '8080' порт; пустая строка, если он стандартный и не указан
pathname '/catalog/shoes' путь после домена
search '?color=red&size=42' строка запроса с вопросительным знаком; пустая, если параметров нет
hash '#reviews' якорь с решёткой; пустой, если его нет
origin 'https://shop.example.com:8080' протокол + домен + порт; только для чтения

Почти все свойства, кроме origin, можно не только читать, но и записывать. Браузер сразу применит изменение: присвоение href или pathname уведёт на новую страницу, а смена hash лишь прокрутит к якорю, без перезагрузки.

location.hash = '#reviews';          // прокрутка к блоку с id="reviews"
location.href = 'https://example.com/'; // переход на другой сайт

Методы location

  • assign(url) — переходит по адресу, как присвоение href. Текущая страница остаётся в истории, кнопка «Назад» вернёт на неё.
  • replace(url) — тоже переходит, но подменяет текущую запись в истории. Вернуться кнопкой «Назад» не получится. Это то, что нужно после логина или для редиректа со старого адреса.
  • reload() — перезагружает страницу. В старых статьях встречается reload(true) «в обход кэша», но этот параметр нестандартный: его понимал только Firefox, остальные браузеры аргумент игнорируют.
  • toString() — возвращает тот же полный адрес, что и href.
// после успешного входа: со страницы логина на кабинет, без возврата назад
location.replace('/account');

Параметры запроса удобнее разбирать через URL

Свойство location.search отдаёт параметры одной сырой строкой — '?color=red&size=42'. Резать её вручную через split('&') неудобно и чревато ошибками с кодированием символов. Для этого есть объекты URL и URLSearchParams:

const url = new URL(location.href);

url.searchParams.get('color'); // 'red'
url.searchParams.get('size');  // '42' — всегда строка
url.searchParams.has('sort');  // false

// меняем параметр и обновляем адресную строку без перезагрузки
url.searchParams.set('size', '43');
history.replaceState(null, '', url);

URL разбирает на те же части любой адрес, а не только текущий, — например, ссылку из пользовательского ввода. Попробовать можно в демо ниже.

history: переходы вперёд и назад

Объект window.history даёт управлять историей переходов текущей вкладки — той самой, по которой ходят кнопки «Назад» и «Вперёд». Сами адреса посещённых страниц из него не прочитать: это было бы утечкой данных о пользователе.

history.back();    // как кнопка «Назад»
history.forward(); // как кнопка «Вперёд»

history.go(-2); // на две страницы назад
history.go(1);  // на одну вперёд
history.go(0);  // перезагрузить текущую

Свойство history.length возвращает число записей в истории вкладки включая текущую, так что на только что открытой вкладке оно равно 1, а не 0.

Методы pushState() и replaceState() меняют адрес в строке браузера без загрузки новой страницы. На них годами держалась маршрутизация в SPA (одностраничных приложениях): приложение само рисует нужный экран, а pushState() добавляет запись в историю, чтобы работала кнопка «Назад».

function openTab(name) {
  renderTab(name);
  history.pushState({ tab: name }, '', `?tab=${name}`);
}

// пользователь нажал «Назад» или «Вперёд»
window.addEventListener('popstate', (event) => {
  renderTab(event.state?.tab ?? 'overview');
});

Navigation API: современная замена history

У связки pushState() + popstate есть известные неудобства: событие не срабатывает на клики по обычным ссылкам и на сам pushState(), а историю целиком не прочитать. Поэтому в браузерах появился объект window.navigation. Он сообщает о любом переходе в пределах документа одним событием navigate и позволяет перехватить его, чтобы отрисовать страницу самостоятельно:

navigation.addEventListener('navigate', (event) => {
  // переходы на другие сайты, загрузки файлов и т.п. перехватить нельзя
  if (!event.canIntercept) return;

  const url = new URL(event.destination.url);
  if (!url.pathname.startsWith('/docs/')) return;

  event.intercept({
    async handler() {
      const html = await loadDocsPage(url.pathname);
      renderPage(html);
    },
  });
});

Здесь одним обработчиком обслуживаются и клики по ссылкам, и отправка форм, и кнопки «Назад»/«Вперёд» — никаких ручных pushState(). Заодно есть navigation.entries() со списком записей истории этого сайта, navigation.currentEntry и промис-версии navigation.back()/navigation.navigate(url). API недавно доехал до всех основных движков, так что в коде, который должен работать и на старых версиях браузеров, пока оставляем проверку и запасной путь через history:

if ('navigation' in window) {
  // маршрутизация через navigate
} else {
  // pushState + popstate
}
Поддержка браузерами
chrome
Chrome
102
firefox
Firefox
147
edge
Edge
102
safari
Safari
26.2
opera
Opera
88

navigator и screen: что известно о браузере и экране

navigator.userAgent и почему по нему не стоит определять браузер

Объект window.navigator описывает сам браузер. Самое известное его свойство — userAgent, строка, которой браузер представляется сайтам:

navigator.userAgent;
// 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36'

Строка выглядит так, будто браузер одновременно Mozilla, Safari и Chrome, — это наследие десятилетий, когда каждый новый браузер притворялся популярным, чтобы сайты не отдавали ему урезанную версию. Пользователь может подменить её в настройках или расширением. А Chrome к тому же урезал строку ради приватности: младшие цифры версии в ней заменены нулями, версия Windows заморожена на 10.0 даже в Windows 11.

Структурированная замена — navigator.userAgentData (User-Agent Client Hints): бренд, мобильность и платформа уже разобраны по полям. Но это экспериментальный API, который есть не во всех движках, поэтому без проверки его не трогаем:

if (navigator.userAgentData) {
  console.log(navigator.userAgentData.mobile);   // false
  console.log(navigator.userAgentData.platform); // 'Windows'
}
Поддержка браузерами
chrome
Chrome
90
firefox
Firefox
 
edge
Edge
90
safari
Safari
 
opera
Opera
76

В большинстве задач браузер определять вообще не нужно. Правильный вопрос не «это Safari?», а «умеет ли браузер вот это?». Отвечает на него проверка возможностей (feature detection):

// есть системное меню «Поделиться» — показываем кнопку
if ('share' in navigator) {
  shareButton.hidden = false;
}

// основное устройство ввода — палец, а не мышь
if (matchMedia('(pointer: coarse)').matches) {
  document.body.classList.add('touch');
}

Из остального в navigator в реальных проектах чаще всего нужны language (предпочитаемый язык интерфейса, например 'uk-UA'), onLine (есть ли сеть) и clipboard для работы с буфером обмена.

screen: размеры монитора, а не окна

Объект window.screen описывает экран, на котором открыт браузер:

screen.width;       // 1920 — ширина экрана в CSS-пикселях
screen.height;      // 1080
screen.availWidth;  // 1920 — без панелей операционной системы
screen.availHeight; // 1032 — например, минус панель задач
screen.colorDepth;  // 24 — глубина цвета в битах

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

window.innerWidth;                        // ширина вьюпорта вместе с полосой прокрутки
document.documentElement.clientWidth;     // ширина вьюпорта без полосы прокрутки
window.devicePixelRatio;                  // 2 на Retina — сколько физических пикселей в одном CSS-пикселе

Разницу между screen.width и innerWidth удобно пощупать в демо: меняйте ширину окна и смотрите, какие числа двигаются, а какие нет.

А решения вида «экран меньше 768 пикселей — прячем меню» вообще лучше отдавать CSS-медиазапросам или matchMedia() в JS, а не читать screen: медиазапрос сам реагирует на изменение размера окна.

Ориентация экрана

На мобильных устройствах полезен screen.orientation: текущая ориентация и событие её смены.

console.log(screen.orientation.type);  // 'portrait-primary' или 'landscape-primary'
console.log(screen.orientation.angle); // 0, 90, 180 или 270

screen.orientation.addEventListener('change', () => {
  player.classList.toggle('wide', screen.orientation.type.startsWith('landscape'));
});
Поддержка браузерами
chrome
Chrome
38
firefox
Firefox
43
edge
Edge
79
safari
Safari
16.4
opera
Opera
25

Метод screen.orientation.lock('landscape') фиксирует ориентацию, например для игры или видео. Но работает он обычно только в полноэкранном режиме (как его включить — в статье про Fullscreen API), и поддерживается заметно уже, чем само свойство. Поэтому вызов всегда оборачиваем в try/catch:

async function playFullscreen() {
  await video.requestFullscreen();
  try {
    await screen.orientation.lock('landscape');
  } catch {
    // не получилось — просто играем в текущей ориентации
  }
}

Окна и попапы

Метод window.open() открывает новое окно или вкладку и возвращает ссылку на объект window этого окна:

const helpWindow = window.open('/help.html', 'help', 'popup,width=480,height=600');

Аргументы часто путают, поэтому по порядку:

  • Адрес — что открыть.
  • Имя окна (target), а вовсе не его заголовок. Заголовок берётся из <title> открытой страницы. Имя работает как у атрибута target ссылки: '_blank' — всегда новое окно, а повторный вызов с тем же именем ('help') загрузит адрес в уже открытое окно, не плодя новые.
  • Строка параметров через запятую без пробелов. Ключевое слово popup просит отдельное маленькое окно вместо вкладки, width/height задают размер, left/top — положение. Если строку не передать, браузер откроет обычную вкладку.

Закрыть окно можно методом close() через сохранённую ссылку. Скрипту разрешено закрывать только те окна, которые открыл он сам, — вкладку, которую открыл пользователь, window.close() не закроет.

helpWindow.close();
console.log(helpWindow.closed); // true

Как два окна после этого обмениваются данными (postMessage, opener, BroadcastChannel), разобрано в статье о том, как передавать данные между окнами и вкладками.

Блокировщик попапов и null вместо окна

Попапы были главным инструментом навязчивой рекламы, поэтому все браузеры их ограничивают. window.open() обычно срабатывает только как прямая реакция на действие пользователя — клик или нажатие клавиши. Вызов из таймера, после долгого await или при загрузке страницы блокировщик остановит. В этом случае метод вернёт null, и это стоит обработать:

helpButton.addEventListener('click', () => {
  const win = window.open('/help.html', 'help', 'popup,width=480,height=600');

  if (!win) {
    // попап заблокирован — показываем справку прямо на странице
    helpPanel.hidden = false;
  }
});

Одна ловушка: с параметром noopener (он отрезает новому окну доступ к window.opener и нужен при открытии чужих сайтов) open() возвращает null всегда, даже если окно успешно открылось. Так что проверка на null имеет смысл только без этого параметра.

У окна есть ещё методы moveTo(x, y) и resizeTo(width, height), которые двигают окно и меняют его размер. Сегодня они работают только для окон, открытых через window.open(), и не работают, если в окне больше одной вкладки. Двигать и растягивать окно пользователя — плохая идея с точки зрения удобства, так что почти всегда есть решение лучше.

Диалоги alert, confirm, prompt и их замена

Три метода окна показывают системные диалоговые окна. Они не входят в стандарт ECMAScript (сам язык), это часть браузерного окружения:

alert('Файл сохранён');                   // undefined — просто сообщение с кнопкой OK

const ok = confirm('Удалить черновик?');  // true — OK, false — Отмена

const name = prompt('Как вас зовут?', 'Мария'); // введённая строка или null при Отмене

Второй аргумент prompt() — значение, которое заранее подставится в поле ввода. Если пользователь нажмёт OK, ничего не введя, вернётся пустая строка, а не null, — эти два случая стоит различать.

Главная особенность всех трёх — они блокирующие. Пока диалог открыт, JavaScript на странице стоит на этой строке: не срабатывают таймеры, не обрабатываются ответы сервера, не перерисовывается интерфейс. Иногда это даже удобно: confirm() гарантирует, что удаление не начнётся, пока пользователь думает. Но оформить такие окна нельзя, выглядят они в каждом браузере по-своему, а после нескольких подряд браузер предлагает пользователю запретить их для сайта — и дальше они просто перестают появляться. Полагаться на них в рабочем интерфейсе нельзя.

Современная замена — HTML-элемент <dialog>. Его можно стилизовать, он не останавливает скрипты, а модальный режим, фокус и закрытие по Esc браузер берёт на себя:

<dialog id="delete-dialog">
  <form method="dialog">
    <p>Удалить черновик?</p>
    <button value="cancel">Отмена</button>
    <button value="delete">Удалить</button>
  </form>
</dialog>
const dialog = document.querySelector('#delete-dialog');

deleteButton.addEventListener('click', () => dialog.showModal());

dialog.addEventListener('close', () => {
  // returnValue — value нажатой кнопки внутри form method="dialog"
  if (dialog.returnValue === 'delete') {
    deleteDraft();
  }
});

Разница в подходе: confirm() возвращает ответ сразу, а у <dialog> ответ приходит позже, в событии close. Код становится асинхронным, зато страница продолжает жить.

document.write() — наследие, которое лучше не трогать

Через window.document доступна сама страница — DOM. Почти всё в нём давно стандартно и полезно, но один метод стоит упомянуть как антипример. document.write() вставляет строку прямо в поток HTML, пока браузер разбирает страницу:

<h1><script>document.write('Привет, мир!')</script></h1>

Пока страница грузится, текст окажется внутри заголовка. Но если вызвать тот же метод после загрузки (например, по клику), он сотрёт весь документ и оставит на странице только переданную строку. Вдобавок каждый вызов заставляет браузер останавливать разбор HTML, а Chrome на медленных соединениях и вовсе блокирует сторонние скрипты, вставленные через document.write(). MDN помечает метод как нерекомендуемый. Вместо него — обычные DOM-методы:

const title = document.querySelector('h1');

title.textContent = 'Привет, мир!';                       // просто текст
title.insertAdjacentHTML('afterend', '<p>Подзаголовок</p>'); // кусок разметки после элемента

Шпаргалка

Объект / метод Для чего Что взять вместо или в дополнение
window как глобал глобальные переменные, встроенные функции globalThis для кода под любую среду
location части адреса, переходы, перезагрузка URL и URLSearchParams для разбора
history назад/вперёд, pushState() в SPA navigation и событие navigate
navigator.userAgent строка-представление браузера проверка возможностей, matchMedia()
screen размер монитора, ориентация innerWidth и медиазапросы для вёрстки
open() / close() попапы и новые вкладки проверка на null, запасной вариант на странице
alert() / confirm() / prompt() быстрые системные диалоги <dialog>
document.write() вставка HTML при разборе страницы textContent, insertAdjacentHTML(), append()

Если коротко: window — это одновременно глобальная область и дверь во всё, что окружает страницу. Большая часть этих объектов старше многих фронтендеров, и у многих уже есть более удобные и безопасные преемники. Старые API никуда не денутся и в чужом коде встретятся ещё не раз, но в новом коде стоит тянуться к преемникам.