Как настроить полифилл 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К показов



