Browsing Context, WindowProxy, Window
Каждый 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]]
.
EN - https://t.me/frontend_almanac
RU - https://t.me/frontend_almanac_ru
English version: https://blog.frontend-almanac.com/ErCH3Qsu6L6