Как настроить полифилл globalThis в универсальном JavaScript
В статье рассказано, как с помощью свойства globalThis реализовать стандартный способ доступа к глобальному значению this в разных средах.
5К открытий6К показов
Предложенное свойство globalThis
предполагает введение единого механизма доступа к глобальному значению this
в любой среде JavaScript. Это похоже на обычный полифилл, но всё же немного отличается, и понять, что это на самом деле, довольно сложно.
В статье описаны трудности реализации правильного полифилла globalThis
. Для него существуют следующие требования:
- должен работать в любой среде JavaScript, включая браузеры, воркеры и расширения браузеров. А также в Node.js, Deno и standalone бинарных файлах на движке JavaScript;
- должен поддерживать грязный и строгий (strict) режимы работы, а также модули JavaScript;
- должен работать независимо от контекста, в котором запущен код (т. е. полифилл должен выдавать правильный результат, даже если его на этапе сборки обернут упаковщиком в строгий режим работы).
Обратите внимание, что в модулях JavaScript есть область-посредник между глобальной областью видимости и вашим кодом. Область видимости модуля скрывает значение this
глобальной области видимости. Поэтому ключевое слово this
, которое видно на верхнем уровне в модулях, на самом деле имеет свойство undefined
.
TL;DR globalThis
!= глобальный объект, но globalThis
== this
из глобальной области видимости.
Альтернативы globalThis
Так сложилось, что для доступа к глобальному объекту в разных средах JavaScript требуется разный синтаксис. Например, в веб-среде можно использовать window
, self
или frames
, при этом для веб-воркеров (и сервис-воркеров) работать будет только self
.
Node.js не работает со всем вышеперечисленным, в его случае нужно использовать global
.
Ключевое слово this
может использоваться в функциях, запущенных в грязном режиме, но в модулях и функциях, запущенных в строгом режиме, для него будет возвращаться значение undefined
.
Проблема выше решается использованием Function(‘return this’)()
, но в средах с отключенной функцией eval()
вроде CSP нельзя использовать Function
подобным образом.
Примечание setTimeout(‘globalThis = this’, 0)
не следует использовать по тем же причинам, что и eval()
и Function
. Кроме того, функция setTimeout
не является частью ECMAScript, а следовательно, не будет доступна во всех средах выполнения JavaScript. Вдобавок к этому, setTimeout
асинхронна, и даже если бы она везде поддерживалась, использовать её в полифилле, от которого зависит другой код, было бы неразумно.
Примитивный полифилл
Похоже, вышеперечисленные методы можно было бы объединить в один полифилл, как например этот:
Но, к сожалению, такой полифилл не будет работать в функциях во время исполнения кода в строгом режиме, в модулях JavaScript и в небраузерных средах (кроме поддерживающих GlobalThis
).
Надёжный полифилл
А можно ли вообще написать надёжный полифилл globalThis
? Возьмём в качестве примера среду, в которой:
- Нельзя полагаться на значение
globalThis
,window
,self
,global
илиthis
. - Нельзя использовать конструктор
Function
илиeval()
. - Можно полагаться на целостность остальной встроенной функциональности JavaScript.
В таком случае есть решение, но оно не идеально.
Если установить значение функции на globalThis
и вызвать его как метод, то можно получить доступ к this
используя следующую функцию:
Как можно сделать что-то подобное, не полагаясь на globalThis
или на специфичную связанную сущность, которая на него ссылается? Нельзя же просто сделать следующее:
Функция foo()
больше не является методом, а поэтому в строгом режиме или в модулях JavaScript у ключевого слова this
будет значение undefined
. Однако, это не относится к геттерам и сеттерам.
Скрипт выше устанавливает геттер на полифилл globalThis
, получает к нему доступ, чтобы в итоге ссылаться на globalThis
, затем очищает полифилл, удаляя геттер. Таким образом у нас появляется доступ к globalThis
при любых обстоятельствах, но этот метод опирается на глобальный this
в первой строке (где написано globalThis
). Можно ли как-то избавиться от этой зависимости? Как можно установить глобально доступный геттер без прямого доступа к globalThis
?
Вместо установки геттера на globalThis
, нужно установить его на то, что глобально наследует объект — Object.prototype
.
Примечание В спецификации ECMAScript не указано, что глобальное this
наследует именно Object.prototype
— только указано, что это строго должен быть объект. Функция Object.create(null)
создаёт объект, который не наследуется от Object.prototype
. Движок JavaScript мог бы использовать такой объект как глобальное this
, не нарушая требования спецификации, но в этом случае фрагмент кода выше всё равно не сработал бы. Однако, в современных движках разработчики, похоже, согласны с тем, что глобальное this
должно включать Object.prototype
в своей цепочке прототипов.
Во избежание проблем с Object.prototype
в современных средах JavaScript, где полифилл globalThis
уже доступен, изменим его следующим образом
Или можно использовать __defineGetter__
:
Как вам такое? Перед вами самый ужасающий полифилл из когда-либо существовавших. Такой подход полностью противоречит общепринятой практике, согласно которой нельзя изменять объекты, которыми вы не владеете.
Не стоит играться с встроенными прототипами вообще — это объясняется в JavaScript Engine Fundamentals: optimizing prototypes.
С другой стороны, единственный способ сломать этот полифилл — изменить object
, Object.defineProperty
(или Object.prototype._ _defineGetter
) перед его запуском.
Тестирование полифилла
Этот полифилл — интересный пример универсального JavaScript: чистый и независимый код, который не полагается на какие-либо встроенные компоненты той или иной среды выполнения, поэтому работает везде, где работает ECMAScript. Итак, одна цель достигнута, теперь посмотрим, как он будет работать.
Обратите внимание на пример HTML-страницы для полифилла, который журналирует globalThis
, используя классический скрипт globalThis вместе с globalThis.mjs (с одинаковым исходным кодом). Этот пример может быть использован для проверки работы полифилла в браузерах. globalThis
нативно поддерживается в Chrome 71, Firefox 65, Safari 12.1 (+iOS Safari 12.2). Чтобы протестировать нужные части полифилла, откройте демо-страницу в старых версиях браузеров.
Примечание Полифилл не поддерживается в Internet Explorer 10 и старше. В этих браузерах строка __magic__.globalThis = __magic__ по каким-то причинам не делает globalThis доступным глобально, несмотря на то, что __magic__ служит рабочей ссылкой на глобальное this. В итоге выясняется, что __magic__ !== window, хотя оба относятся к [object Window], что как бы намекает, что браузеры могут запутаться в определении глобального объекта и глобального this. Внесение правок в полифилл для отката к одной из альтернатив позволяет ему работать в IE 10 и 9. Для поддержки в IE 8 нужно обернуть Object.defineProperty в try-catch, подобным образом откатившись в блок catch (это также поможет избежать проблемы в IE 7 с глобальным кодом, который не наследуется от Object.Prototype). Попробуйте поиграть с демо-версией, поддерживающей старые версии IE.
Для тестирования полифилла в Node.js и отдельных движках JavaScript скачайте те же самые файлы с расширением .js/.mjs.
Теперь можно тестировать в Node.js.
Для тестирования полифилла в отдельной оболочке JavaScript-движка используйте jsvu для установки любого желаемого движка, а затем запустите скрипты напрямую. Например, протестируем в V8, v7.0 (без поддержки globalThis
) и v7.1 (с поддержкой globalThis
):
Таким же образом можно тестировать JavaScriptCore, SpiderMonkey, Chakra и другие JavaScript-движки. Ниже приведён пример использования JavaScriptCore:
Заключение
Написание универсального JavaScript может быть непростым делом и часто требует творческих решений. Новая функция globalThis
облегчает написание универсального JavaScript, которому нужен доступ к глобальному значению this
. Повсеместное использование globalThis
сложнее, чем кажется, но есть работающее решение.
Используйте полифилл только тогда, когда это действительно необходимо. Модули JavaScript облегчают импорт и экспорт функциональности без изменения глобального состояния, и большинству современных JavaScript-кодов не нужен доступ к глобальному this
.
5К открытий6К показов