A horrifying globalThis polyfill in JavaScript
mathiasbynens.be
mathiasbynens.be
The fact that I can use classes, block scopes, const and let, new array methods, decorators, etc. without having to wait for every client to update to JacaScript N+1 is pretty sweet.
JavaScript/ES is just now getting features that other languages have enjoyed for one or more decades because JS people have discovered the features and how to build the tooling. So good for JS for finally getting these basics, just wish it happened about 15 years ago.
Not only is it not true in many cases (I've been using pattern matching for years in other functional languages and I'm anxiously hoping the proposal will get closer to inclusion in JS, but I'm sure if/when it lands I'll be told about how sad it is that I just learned about this feature when others have had it for decades...), but it's also pointlessly "holier than thou".
If JS doesn't have a feature, people say that they want it. Once it gets it, another crowd complains about how they should have had it 15 years ago.
The fact is that there wasn't much of a need for something like globalThis 15 years ago. We had window, and that's all that we needed. Then we got global with node, and then self in workers, and then JS started getting more usage in embedded contexts with none of that. A problem was found, a proposal created and refined, a polyfill made, and finally it's going to be part of the language.
This is progress, this is the maintainers identifying and fixing a deficiency, only to be met with "who cares they should have done it years ago [when nobody was asking for it]".
My point being there's just no need to say that. It doesn't contribute anything, it's just belittling the work of some very talented people who are making things that people are asking for in ways that don't break past code. That may not have been your intention, and I'm sorry for coming into this with a lot of baggage here, but it's just something that really gets under my skin and I've seen multiple TC39 people talk about how they avoid places like HN because of those kinds of attitudes, and I feel that everyone is missing out because of it.
It's worth being aware that JavaScript is (and always has been) a long way behind the language design state of the art. That informs people's choice of language (in cases where non-JS is an option - but in these days of transpilers, non-JS is always an option). It's not a reason for people to stop improving JavaScript if they find it useful to do so, but it may be a reason to switch away from JavaScript.
People are excited to have these features in their language, they may like the JS implementation better than others or vice versa, they may talk about how much it improves the language or how it's a detriment to the language to have it included, but I haven't seen anyone claim that "javascript invented this".
It's fine to want to be aware that JS isn't "state of the art" in language design, but I really don't think anyone believes that it is. (and as an aside, I don't think I'd want to use a language that was state of the art as the base for a large application, because that means that there isn't much testing or prior art to build from)
But that argument also misses the point. I'm happy to talk about the pros and cons of languages, I'm happy to discuss how erlang's message passing makes some things extremely easy to develop, how python's collections make many kinds of programming much easier to read. But bringing up how other languages have had a feature "years ago" once it's arrived in javascript isn't discussing pros and cons or offering reasons to switch, it's turning tools into sports teams where you are "rooting" for your favorite.
If/when JS gets pattern matching, you can be your ass i'll be in that thread comparing it to ocaml's approach, or F#'s approach to see the differences and similarities, to see the benefits of each, to see how they all fit into each languages ecosystem. But I won't be exclaiming how it doesn't matter because "other languages had this years ago!", because it doesn't help anything, it doesn't give anyone information they can act on, it doesn't discuss tradeoffs, it is just downplaying the achievements of the people that helped make it a reality.
I've seen people say javascript invented non-blocking I/O or had the first practical implementation. I've even seen people say it was the first mainstream language with map/reduce/filter.
> But that argument also misses the point. I'm happy to talk about the pros and cons of languages, I'm happy to discuss how erlang's message passing makes some things extremely easy to develop, how python's collections make many kinds of programming much easier to read. But bringing up how other languages have had a feature "years ago" once it's arrived in javascript isn't discussing pros and cons or offering reasons to switch, it's turning tools into sports teams where you are "rooting" for your favorite.
That's a failure mode, sure. But being aware of where a language stands on the innovation spectrum can also help you learn more about what else is out there. If you know JavaScript is getting a feature today that OCaml had 20 years ago, you might start looking around for language features that JavaScript will be getting in 20 years' time that are available in other languages today. That's information you can act on, and might draw attention to a tradeoff you weren't even aware you were making by using JS.
Purely anecdotal, you should not base your argument on anecdotal evidence. (goes for both posts for and against in this case)
I point this out because it is important to contextualize why JS is in the state it is in. The powers that be seem less interested in fixing the issues of the languages and more interested in plastering over them. A language with this many polyfills is broken. I use JS almost every day at work, same as Python. 100% prefer Python.
Please, no. I'm bitten by this in Python every other day, with its otherwise weak typing. It feels like you can do many horrible things with types in Python, but no, you shall explicitly cast those numbers to strings. It is being more pedantic than Java by doing this. This never caught a real error in my code but it did make it needlessly fail at runtime because I did not put "str()". I know that the newer Python 3.x versions provide a nice bash-like way to embed expressions in strings and would let me avoid the problem entirely, but this would prevent my code to run in many places (and "Hello, I'm {}".format(age) is painful, hence the new feature by the way).
Thank you for trying to save my back from unwanted implicit type casts, but provide me with static typing instead if you want to do so, and fail at compile time.
If you want '1' + 2 to fail in JavaScript (You might want to, especially when keys of objects are coerced to strings in Javascript, which is actually crappy, and when you are constantly fetching numbers as strings from the web page), use TypeScript. It's amazing and provides a far better way of handling silly type errors.
edit: and by the way, C++ is a bit surprising in this respect. "Hello" + 2 is equal to... "llo".
If you don't know whether your variables contain strings or numbers you literally don't know what your are doing.
> with its otherwise weak typing.
Python is strongly typed.
> It is being more pedantic than Java by doing this.
That just means that Java is broken in that respect.
> Thank you for trying to save my back from unwanted implicit type casts
Making '"1" + 2 = "1twee"' work does not just require a type cast. You actually have to choose and generate one of the string representations of II.
> This never caught a real error
Yes, it did. You tried to concatenate a string with a number. That cannot be done.
> Thank you for trying to save my back from unwanted implicit type casts, but provide me with static typing instead if you want to do so, and fail at compile time.
Python is too dynamic for that to work. However Mypy works well enough to be useful.
Only surprising if you think "Hello" is an object of a string type. If however you think of it as a pointer, then moving the pointer is entirely unsurprising, especially as a pointer "is" an integer.
The tc39 proposal for globalThis mentions that `global` was preferred, but it broke major websites that were for whatever reason relying on setting their own values for `window.global`.
Don’t C++ and C# do the same thing? Adding something to a string coerces the second thing to a string.
__magic__.globalThis = __magic__; // lolwat
Seems needlessly, well, magic. Keeping it at two lines would make it easier to grok: Object.prototype.__defineGetter__('__getValueOfThis__', function() {
return this;
});
const globalThisValue = __getValueOfThis__; // Invoke getter on "global object"
globalThisValue.globalThis = globalThisValue;
Just because it magical doesn't mean it has to be confusing.`__getValueOfThis__` is good. Something like `__globalThisGetter__` or `__globalThisPolyfillGetter__` might be even clearer given the deletion line. e.g.:
__globalThisPolyfillGetter__.globalThis = __globalThisPolyfillGetter__;
delete Object.prototype.__globalThisPolyfillGetter__;I mean, they intentionally closed access to global object through "this" in strict mode and now they want it back.
let global = typeof window === 'undefined' ? global : window
// do something with global.theGlobalProp
(this is used to refer to currently available globals which are not going away anytime soon)
export default foo(theGlobalProp) {...; return ...;}
Why should a module reach outside its own scope?In fengari we currently use https://github.com/fengari-lua/fengari-interop/blob/8e59efb7...
const global_env = (function() {
/* global WorkerGlobalScope */ /* see https://github.com/sindresorhus/globals/issues/127 */
if (typeof process !== "undefined") {
/* node */
return global;
} else if (typeof window !== "undefined") {
/* browser window */
return window;
} else if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) {
/* web worker */
return self;
} else {
/* unknown global env */
return (0, eval)('this'); /* use non-strict mode to get global env */
}
})();
I created https://github.com/fengari-lua/fengari-interop/issues/45 to track.function luaopen_js(L, global) { ... }
instead of writing this ugly hack?
It's a piece of code that is simple on it's surface but has an incredible amount of depth to it. Every single line has a purpose and a backstory. And it solves a very simple to understand problem that many don't even know is a problem until it bites them (most often when trying to use a library in a web worker the first time).
that's the definition of bad code: looks simple and innocuous. Does something completely out of the world and "unexpected" (from the point of view of a novice/unknowledgable programmer).
It's bad code that has to be bad, it has to be complex, it's inherently difficult and needs to tiptoe around edge cases and avoid pitfalls that 99% of us don't know or have to care about. And yet it's able to do that and still be small, concise, and relatively easy to understand on a basic level, even if you can't quite understand the full reasoning behind why it was created that way on the surface.
I have similar feelings about the fast square root function from Quake III. It's horrible and ugly and confusing on the surface, but incredibly powerful, fast, and humbling when you really look at it. And it served a purpose that enabled the game to work!
Can someone in the JavaScript community please enlighten me and tell me why you would want to be able to obtain a reference to “globalThis” rather than just the global object itself?
Maybe I’m just being presumptuous, but I suspect that’s what this object is going to be used for anyway, for storing global state. And then it might as well just be called global, right?
Unless of course you want to permeate the chaos that is JS “this” into this new global context, but who would deliberately want to do that?
1) In browsers, the actual global (the Window) is _never_ exposed to script. The reasons for that used to do with same-origin policy enforcement, with exposure of the Window to script being considered a security bug in various browsers for a while, but I'm not sure those reasons are still relevant nowadays. At this point, the main reason the WindowProxy is what's exposed is that this is what web authors and web code expect. It's actually quite terrible, because it conflates two objects: "the web page" (corresponding to the non-exposed Window) and "the thing we load web pages in" (corresponding to the WindowProxy). _That_ is a design mistake in the web's object model that dates back to the mid-'90s...
In any case, the upshot is that in browser window contexts you can't get a reference to the global; the only thing you can get is the thing that is globalThis. In other contexts (web workers, Node), the two are the same thing.
2) The property naming is ... well, read https://github.com/tc39/proposal-global/blob/master/NAMING.m... for a (longish) summary. Node uses "global", but browsers couldn't do that because it broke some websites. Browsers use "self", but Node wasn't willing to do that, citing concern over the same sort of breakage. And so on, and so forth.
JavaScript transit gloria mundi.
The real WTF is (0, eval)('this')
wtf is that syntax??!!one!
calling a method or function directly (when in the global scope) will set `this` to the global object. But calling a method or bound function could have a different `this`, and calling a function directly in another context can have a different `this`.
the `(0, funcName)` syntax works by evaluating each item in the parens, but returns the last item. (don't ask me why this is in the syntax, I genuinely don't know and have never seen it used beyond this as far as I know)
So that syntax is abused to basically (simplified here) reassign the funcName to strip out the `this` that is bound to it and reassign it to the global context.
It then evals that `this`, and returns it! Meaning it will always get the global `this`, or `globalThis`!
It would be similar to doing this:
var thing = eval
thing('this')This is just how the comma operator works in other languages such as C and C++: https://en.m.wikipedia.org/wiki/Comma_operator
It just seems so foreign and out of place when used outside of those instances though, and for me at least the purpose of it wasn't intuitive at all!
If somebody rebound eval to redefine the 'this' inside - e.g., eval = (function(){myThis = ... ; return function(a){eval.apply(mythis, [a]);};})();
then calling eval with (0, eval)('this') will still not have rebinded the 'this' inside eval right? So why not just directly call eval('this')? Or am i missing the point?
I believe it is because of a weird distinction between "direct" and "indirect" calls in javascript. The spec says that an indirect call is guaranteed to execute in the global context, but a direct call will execute in whatever context it's in.
The `(0, eval)` makes it an indirect call by evaluating an expression that returns the function.
I'm not quite sure about the details of why indirect calls are treated that way though...
> (...) any invocation of eval that is not a direct call uses the global environment as its variable environment rather than the caller’s variable environment.
The `(0, eval)` makes it an indirect call by evaluating an expression that returns the function.
This is really timely. I just ran into an issue this weekend where I tried to generalize a function call like this (unaware of the direct vs. indirect semantics), and TypeScript yelled at me about it. Looks like it was trying to evaluate it in the global context, so the error makes sense now.
import 'foo'
import 'bar'
(function (module) {
console.info(localThis[module]);
})('foo')The article includes a working polyfill that includes old IE support, so I’m not sure what you’re complaining about.
Also, saying “jsvu is the justification for the polyfill” doesn’t make much sense. jsvu is just how I happen to test in various standalone JavaScript engines. There are other, non-jsvu ways of getting such binaries, e.g. compiling one yourself, and there are JS engine binaries that jsvu doesn’t provide (Rhino, Ringo, Nashorn). jsvu usage does not correspond to anything you seem to be talking about, and comparing its download count with IE10 usage is one of the more extreme apples-to-oranges comparison I’ve seen. ️
You wouldn’t have to have to bolt on .meta onto import after the fact. You wouldn’t have to create horrible polyfills for the horribly named globalthis. And so on and so on and so on.
Hint: just spend actual time actually designing the language.
Hint: Introduce System namespace/module with System.globals. There. Solved. Trivially polyfilled. The BS about “we can’t easily introduce new global objects/modules” is BS after WeakMaps and SharedBuffers and god knows what else.
So, my other point still stands: instead of any long-term planning and robust design, TC39 keeps introducing features that are badly specified are underspecified, and that require patches and workarounds in the language itself (yup, .meta, globalThis and some others are nothing but poorly thought-out patches) and conplex workarounds for the various JS environments that may or may not support these features.
I don't think that that can actually happen, so we just keep trying to cake more lipstick on the pig; maybe in five years we can put it out to pasture and use WebAssembly instead.
The problem is that different host environment have different names for the global object. That’s what make it hard to write code that works independent of the host environment and that’s what globalThis is aiming to fix.