Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Каждый Frontend-разработчик знает, что такое объект Window. С самими объектом, вроде бы, все понятно. Но при детальном рассмотрении оказывается, что браузер никогда не отдает этот важнейший глобальный объект напрямую. В этой статье я предлагаю разобраться в спецификации HTML и в том, как именно ведет себя браузер в части глобального контекста.
Начать стоит с того, что в документе может быть несколько глобальных объектов Window (например, iframe на странице). Более того, самих документов тоже может быть несколько (например, открытые табы браузера). Между документами и их объектами Window возможна некоторая связанность посредством frame.contentWindow
, self.opener
, window.open()
и пр. Согласно спецификации, каждый браузерный документ, будь то таб, iframe или что-то еще, является так называемым navigable. В свою очередь, кажды navigable в спецификации HTML называется browsing context. Сам же browsing context имеет ассоциированные объекты WindowProxy и Window. Когда мы переключаем контекст, объект Window меняется на соответствующий, а вот WindowProxy всегда один и тот же.
Дело в том, что WindowProxy является как бы универсальной оберткой-прокси для множества Window. Он прозрачно отдает все доступные свойства Window, однако, сам Window может время от времени меняться. Так же, в отличие от Window, являющимся обычным объектом (ordinary object), WindowProxy является необычным (exotic object). Напомню, обычными считаются все классические объекты в JavaScript, которые мы можем свободно создавать. Необычными считаются те объекты, поведение которых может иметь некую скрытую логику, к таким объектам относят, например Array, String, Arguments и др.
WindowProxy, будучи exotic, имеет слот [[Window]]
, а так же ряд скрытых методов: GetPrototypeOf, SetPrototypeOf, IsExtensible, PreventExtensions, GetOwnProperty, которые призваны обеспечить работу объекта, как прокси.
Зачем же нужны такие сложности, не проще ли просто отдавать Window как есть? Ответ прост, основная причина - политика безопасности. Мы знаем, что политика same-origin
запрещает доступ к коду одного origin из другого. Т.е. нельзя с домена a.com получить доступ к коду b.com. Однако, например, если b.com был размещен внутри a.com через iframe, есть некоторые послабления в политике и доступ к коду все получить можно, что потенциально создает дыры в системе безопасности.
Представим следующий код
Что делает этот код
// вешаем onload колбэк на iframe, который сработает при
// загрузке/перезагрузке фрейма
frame.onload = function onFrameLoad() {
// после загрузки фрейма сохраним ссылку на contentWindow
cWindow = frame.contentWindow;
// а так же, создадим глобальную переменную a в контексте Window
cWindow.a = "a";
// Выведем результат на экран.
//
// Функция ниже сравнивает ссылку на сохраненный contentWindow
// c реальным contentWindow:
//
// cWindow === frame.contentWindow
//
// а так же, выводит значение глобальной переменной a из Window
//
// const printResult = (id) => {
// const isEqual = cWindow === frame.contentWindow;
// const value = cWindow.a;
//
// document.getElementById(id).innerHTML = contentWindow is equal: ${isEqual};\nvalue: ${value};
// }
printResult("result-1");
// далее повесим новый колбэк на onload этого же iframe
frame.onload = function () {
// и еще раз выведем результат после перезгрузки фрейма
printResult("result-2");
}
}
Мы видим, что после первой загрузки фрейма ссылка cWindow
идентична frame.contentWindow
, а глобальная переменная window.a = 'a'
, как и ожидалось.
После второй загрузки фрейма видим, что cWindow
по прежнему идентична frame.contentWindow
, но window.a
уже не существует в этом контексте. С точки зрения политики безопасности - это правильно, iframe не должен иметь возможности мутировать родительский глобальный объект. Эта и другие проблемы безопасности в дочерних контекстах были предметом головной боли и детального обсуждения Бобби Холлей, Борисом Жбарски, Яном Хиксона, Адамом Варта и Эн ван Кестерен на протяжении многих лет, которое завершилось пул-реквестом "Define security around Window, WindowProxy, and Location properly".
Но как такое оказалось возможным технически? Ведь ссылка на контекст не изменилась? Как в одном и том же контексте переменная может существовать и не существовать одновременно?
Всему виной WindowProxy. Как я уже говорил, работая в объектом Window, мы, на самом деле, всегда обращаемся к WindowProxy, который обеспечивает изоляцию Window внутри browsing context. При этом ссылка на сам WindowProxy остается перманентной. Именно поэтому cWindow === frame.contentWindow
всегда возвращает true
, обе ссылки - ссылки на WindowProxy. Однако, перезагрузив фрейм мы создали второй контекст Window, который будет существовать внутри WindowProxyManager. Обращаясь к глобальной переменной, WindowProxy определяет, из какого контекста происходит обращение и выставляет нужный Windowв слот [[Window]]
.
Эту и другие мои статьи, так же, читайте в моем канале
RU: https://t.me/frontend_almanac_ru
EN: https://t.me/frontend_almanac