Как настроить полифилл globalThis в универсальном JavaScript
Перевод статьи: «A horrifying globalThis polyfill in universal JavaScript»
Предложенное свойство 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
.
globalThis === window; // → true globalThis === frames; // → true globalThis === self; // → true
Node.js не работает со всем вышеперечисленным, в его случае нужно использовать global
.
globalThis === global; // → true
Ключевое слово this
может использоваться в функциях, запущенных в грязном режиме, но в модулях и функциях, запущенных в строгом режиме, для него будет возвращаться значение undefined
.
globalThis === this; // → true
Проблема выше решается использованием Function(‘return this’)()
, но в средах с отключенной функцией eval()
вроде CSP нельзя использовать Function
подобным образом.
globalThis === (function() { return this; })(); // → true globalThis === Function('return this')(); // → true globalThis === (0, eval)('this'); // → true
Примечание setTimeout(‘globalThis = this’, 0)
не следует использовать по тем же причинам, что и eval()
и Function
. Кроме того, функция setTimeout
не является частью ECMAScript, а следовательно, не будет доступна во всех средах выполнения JavaScript. Вдобавок к этому, setTimeout
асинхронна, и даже если бы она везде поддерживалась, использовать её в полифилле, от которого зависит другой код, было бы неразумно.
Примитивный полифилл
Похоже, вышеперечисленные методы можно было бы объединить в один полифилл, как например этот:
// Примитивный костыль для globalThis. Не используйте его! const getGlobalThis = () => { if (typeof globalThis !== 'undefined') return globalThis; if (typeof self !== 'undefined') return self; if (typeof window !== 'undefined') return window; if (typeof global !== 'undefined') return global; if (typeof this !== 'undefined') return this; throw new Error('Unable to locate global `this`'); }; // Примечание: var используется вместо const, чтобы убедиться, что globalThis // становится глобальной переменной (в отличие от переменной в // лексической области верхнего уровня) при запуске кода в глобальной области видимости. var globalThis = getGlobalThis();
Но, к сожалению, такой полифилл не будет работать в функциях во время исполнения кода в строгом режиме, в модулях JavaScript и в небраузерных средах (кроме поддерживающих GlobalThis
).
Надёжный полифилл
А можно ли вообще написать надёжный полифилл globalThis
? Возьмём в качестве примера среду, в которой:
- Нельзя полагаться на значение
globalThis
,window
,self
,global
илиthis
. - Нельзя использовать конструктор
Function
илиeval()
. - Можно полагаться на целостность остальной встроенной функциональности JavaScript.
В таком случае есть решение, но оно не идеально.
Если установить значение функции на globalThis
и вызвать его как метод, то можно получить доступ к this
используя следующую функцию:
globalThis.foo = function() { return this; }; var globalThisPolyfilled = globalThis.foo();
Как можно сделать что-то подобное, не полагаясь на globalThis
или на специфичную связанную сущность, которая на него ссылается? Нельзя же просто сделать следующее:
function foo() { return this; } var globalThisPolyfilled = foo();
Функция foo()
больше не является методом, а поэтому в строгом режиме или в модулях JavaScript у ключевого слова this
будет значение undefined
. Однако, это не относится к геттерам и сеттерам.
Object.defineProperty(globalThis, '__magic__', { get: function() { return this; }, configurable: true // Благодаря этому впоследствии можно удалить геттер. }); // Примечание: var используется вместо const, чтобы убедиться, что globalThis // становится глобальной переменной (в отличие от переменной в // лексической области верхнего уровня) при запуске кода в глобальной области видимости. var globalThisPolyfilled = __magic__; delete globalThis.__magic__;
Скрипт выше устанавливает геттер на полифилл globalThis
, получает к нему доступ, чтобы в итоге ссылаться на globalThis
, затем очищает полифилл, удаляя геттер. Таким образом у нас появляется доступ к globalThis
при любых обстоятельствах, но этот метод опирается на глобальный this
в первой строке (где написано globalThis
). Можно ли как-то избавиться от этой зависимости? Как можно установить глобально доступный геттер без прямого доступа к globalThis
?
Аналитик-тестировщик
«МойОфис», Санкт-Петербург
tproger.ru Вакансии на tproger.ru
Вместо установки геттера на globalThis
, нужно установить его на то, что глобально наследует объект — Object.prototype
.
Object.defineProperty(Object.prototype, '__magic__', { get: function() { return this; }, configurable: true // Это позволит
позже
избавиться от геттера. }); // Примечание: var используется вместо const, чтобы убедиться, что globalThis; // становится глобальной переменной (в отличие от переменной в // лексической области верхнего уровня). var globalThis = __magic__; delete Object.prototype.__magic__;
Примечание В спецификации ECMAScript не указано, что глобальное this
наследует именно Object.prototype
— только указано, что это строго должен быть объект. Функция Object.create(null)
создаёт объект, который не наследуется от Object.prototype
. Движок JavaScript мог бы использовать такой объект как глобальное this
, не нарушая требования спецификации, но в этом случае фрагмент кода выше всё равно не сработал бы. Однако, в современных движках разработчики, похоже, согласны с тем, что глобальное this
должно включать Object.prototype
в своей цепочке прототипов.
Во избежание проблем с Object.prototype
в современных средах JavaScript, где полифилл globalThis
уже доступен, изменим его следующим образом
(function() { if (typeof globalThis === 'object') return; Object.defineProperty(Object.prototype, '__magic__', { get: function() { return this; }, configurable: true // Это позволит избавиться от геттера позже. }); __magic__.globalThis = __magic__; delete Object.prototype.__magic__; }()); // В коде теперь может использоваться globalThis. console.log(globalThis);
Или можно использовать __defineGetter__
:
(function() { if (typeof globalThis === 'object') return; Object.prototype.__defineGetter__('__magic__', function() { return this; }); __magic__.globalThis = __magic__; delete Object.prototype.__magic__; }()); // В коде теперь можно использовать globalThis. console.log(globalThis);
Как вам такое? Перед вами самый ужасающий полифилл из когда-либо существовавших. Такой подход полностью противоречит общепринятой практике, согласно которой нельзя изменять объекты, которыми вы не владеете.
Не стоит играться с встроенными прототипами вообще — это объясняется в 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.
# Загрузите полифилл + демо-код как модуль. curl https://mathiasbynens.be/demo/globalthis.mjs > globalthis.mjs # Создайте копию (или символьную ссылку) файла, который должен будет использоваться как обычный скрипт. ln -s globalthis.mjs globalthis.js
Теперь можно тестировать в Node.js.
$ node --experimental-modules --no-warnings globalthis.mjs Testing the polyfill in a module [object global] $ node globalthis.js Testing the polyfill in a classic script [object global]
Для тестирования полифилла в отдельной оболочке JavaScript-движка используйте jsvu для установки любого желаемого движка, а затем запустите скрипты напрямую. Например, протестируем в V8, v7.0 (без поддержки globalThis
) и v7.1 (с поддержкой globalThis
):
$ jsvu v8@7.0 # Install the `v8-7.0.276` binary. $ v8-7.0.276 globalthis.mjs Testing the polyfill in a module [object global] $ v8-7.0.276 globalthis.js Testing the polyfill in a classic script [object global] $ jsvu v8@7.1 # Install the `v8-7.1.302` binary. $ v8-7.1.302 globalthis.js Testing the polyfill in a classic script [object global] $ v8-7.1.302 globalthis.mjs Testing the polyfill in a module [object global]
Таким же образом можно тестировать JavaScriptCore, SpiderMonkey, Chakra и другие JavaScript-движки. Ниже приведён пример использования JavaScriptCore:
$ jsvu # Install the `javascriptcore` binary. $ javascriptcore globalthis.mjs Testing the polyfill in a module [object global] $ javascriptcore globalthis.js Testing the polyfill in a classic script [object global]
Заключение
Написание универсального JavaScript может быть непростым делом и часто требует творческих решений. Новая функция globalThis
облегчает написание универсального JavaScript, которому нужен доступ к глобальному значению this
. Повсеместное использование globalThis
сложнее, чем кажется, но есть работающее решение.
Используйте полифилл только тогда, когда это действительно необходимо. Модули JavaScript облегчают импорт и экспорт функциональности без изменения глобального состояния, и большинству современных JavaScript-кодов не нужен доступ к глобальному this
.
12 концепций, которые прокачают ваш JavaScripttproger.ru
Хинт для программистов: если зарегистрируетесь на соревнования Huawei Honor Cup, бесплатно получите доступ к онлайн-школе для участников. Можно прокачаться по разным навыкам и выиграть призы в самом соревновании.
Перейти к регистрации
- 3 views
- 0 Comment
Свежие комментарии