I would like to open a discussion on whether upcoming proposals in TC39 read too much into the existence of new.target and super.* and (in my opinion) abuse the language with extensions and additions that could potentially be solved in a different manner.
I'm copying some of the discussion over from es-discuss for greater visibility and as a possibility for other people to voice their views (not just those subscribed to the mailing list).
My original concern is with:
import being extended to become import() and then having import.metafunction.sentThe summary is as follows:
In Javascript, we now have:
typeof, case, break, etc.)enum, public, private, await etc.). May never be used and may possibly be removed, as some keywords have been (int, byte, char etc.)null, true, false)eval, arguments)new with new.target)import and import())import and import() and import.meta)It gets even worse. Because “metaproperties” are not just attached to keywords. They are attached to keywords which have fundamentally different semantics in the language:
new is an operator, it gets new.targetfunction.sentCallExpression and then it gets an import.meta on top of that (as a hardcoded “metaproperty”).The ECMAScript spec lays down no guidelines for when "metaproperties" should be used and for when keywords can or should be extended with additional behaviour. My personal feeling that this is used to great detriment to the language, as vastly different and often highly context-dependent "things" are "just added" to the language. From a developer experience point of view, I believe, this makes the language increasingly more complex, unstructured, unpredictable, and chaotic.
@dmitriid I'll start from the top:
I would like to open a discussion on whether upcoming proposals in TC39 read too much into the existence of
new.targetandsuper.*and (in my opinion) abuse the language with extensions and additions that could potentially be solved in a different manner.I'm copying some of the discussion over from es-discuss for greater visibility and as a possibility for other people to voice their views (not just those subscribed to the mailing list).
I do agree that there is reason to have concern with the way meta properties are being pretty liberally considered at times. new.target does feel a little weird from a language design perspective, but it does generally fit - it's literally the target of a new call.
My original concern is with:
importbeing extended to becomeimport()and then havingimport.meta
First, plenty of other things have been considered for import(...) and import.meta both.
Second, I'd say this is closer to super(...) (long reserved for that purpose) and new.target.
super(...) performs a context-dependent action, just like import(...).
super(...)'s scope is derived from that of the enclosing thisimport(...)'s scope is derived from that of the enclosing modulenew.target gets context-dependent metadata, just like import.meta.
new.target's scope is derived from that of the enclosing thisimport.meta's scope is derived from that of the enclosing moduleIt would've been nice if super was somehow bound to this (like this@super or something), so it would be as clean as import.meta.
Oh, and fun fact: super(...) is actually an expression, not a statement, and it returns the super call's result (as in, the resolved this). In addition, it's available in nested arrow functions. So in theory, super could be implemented as a callable proxy of the inherited prototype, like this, just exposed as a special keyword. The only reason it can't be spec'd that way is because super itself, just the keyword, isn't an expression.
So if you want to know of an object that isn't, super(...) is about as close as you're going to get. Not even import compares to this one.
function.sent
I do agree that name is probably among the problematic ones, and IMHO it should probably be yield.sent or similar instead (just filed a bug about it). It really looks wrong for that usage.
My bigger issue really boils down to this: why didn't they address that potential issue when they designed generators, rather than a year later.
In Javascript, we now have:
- keywords that are just keywords, really (
typeof,case,break, etc.)- keywords that are just keywords, but don’t even exist in a language. They are reserved for future use in various contexts: always reserved, only in strict mode, only in module code etc. (
enum,public,private,awaitetc.). May never be used and may possibly be removed, as some keywords have been (int,byte,charetc.)
I'd like to correct a couple of your examples:
await has since been used for async functions.public and private were reserved for controlling method privacy, but I see these as likely to be dropped, considering their original intended future use case uses a sigil instead.
- literals that are basically keywords (
null,true,false)- non-keywords that are for all intents and purposes keywords (
eval,arguments)
I presume the main reason they weren't properly reserved was probably because @BrendanEich (please correct me if I'm wrong) didn't have enough time to consider and address that in his design.
- keywords that look like objects (because they have additional properties) which are not objects (
newwithnew.target)
Not really. Unlike import() vs import.meta and especially super() vs super.foo,
- keywords that look like functions (because they are invoked like functions and return values like functions) which are not functions (
importandimport())- keywords that look like objects and functions but are neither (
importandimport()and import.meta)
See my previous notes on super(...).
It gets even worse. Because “metaproperties” are not just attached to keywords. They are attached to keywords which have fundamentally different semantics in the language:
newis an operator, it getsnew.target
Arguably, this is the ideal distinction - operators aren't properties, so there is literally zero risk for confusion here.
- A function is a callable object, and it gets a
function.sent
function.sent is unique to generators, not just any callable object. See my previous notes on it, for my personal objections, but they're purely on naming and lack of foresight, not the choice of a meta property at all.
importis … I don’t know whatimportis. It gets transformed into a separate, made-just-for-import CallExpression and then it gets animport.metaon top of that (as a hardcoded “metaproperty”).
Consider it this way:
import ... from "foo" statically imports values from "foo". It's a top-level statement to reflect its static nature.import(mod) dynamically imports values from mod. It's a call-like expression to reflect its dynamic nature.import.meta contains the import metadata for this module. It's a meta property to reflect its property-like nature.The ECMAScript spec lays down no guidelines for when "metaproperties" should be used and for when keywords can or should be extended with additional behaviour. My personal feeling that this is used to great detriment to the language, as vastly different and often highly context-dependent "things" are "just added" to the language.
In general, here's the patterns I've observed with the choice of what is used:
var, export, etc., it's almost always a highly declarative construct.break, continueyield or await, it's almost always space-separated.if/else and forsuper(...) or import(...), it's almost always call-like.typeofargumentsFrom a developer experience point of view, I believe, this makes the language increasingly more complex, unstructured, unpredictable, and chaotic.
In my experience, it's been adding much needed structure and rigor.
new existed without a way to declaratively make a constructor.takeWhile for Java-like iterators? It's trivial using ES6 generators and for ... of.Also, here's a rundown of all the existing Stage 4 proposals with significant added syntax:
await to become a contextual keyword in order to avoid ambiguity.And stage 1-3 with significant added syntax (ignoring RegExp stuff):
import(...) has already been discussed above.#), not a keyword.function.sent has already been discussed above.import.meta has already been discussed above.do expressions have bodies.@isiahmeadows first of all, thanks :clap: for the comprehensive write! It is way easier to digest this, no matter if the reader agrees or not with the argumentation.
I personally need to digest this in peace and quiet, but there are three things that I actually miss before starting the digestion.
Something that @dmitriid left out of this github issue, but exists in the es-discuss thread that triggered this issue, is the suggested alternative to his complaints (isn't that constructive criticism? :) ).
That suggestion goes about introducing new globals, in the line of how Map, Set, Symbol, Reflect and Proxy came up. He mentions those globals could tackle introspection at specific levels i.e. Module, or in the mindset of "introduce as few things", at a general level i.e. System which can then have reference introspection at specific levels. Those are irrelevant details - the core is "introduce a new global for introspection".
My question on this topic is whether you or somebody else listening in has any idea of whether this/similar was suggested and why was it rejected?
While new.target, function.sent and import.meta are not keywords, they are still additional syntax since new, function and import are not objects, but keywords. That is why now parsers need to be aware of such things - random example from babylon https://github.com/babel/babylon/pull/402/files#diff-94eebbd7c72a61803345c949037690d8 .
The only detail that makes them not keywords, is that the dot that follows carries meaning (separator) allowing a parser to detect the keyword e.g. new then look forward if a dot follows (maybe after some whitespace) and decide whether the code want new or new.target.
From 30.000 ft though, new.target could be easily added to the keywords list since it behaves, smells and quacks like a keyword. "new.foo" doesn't make sense and throws. Put differently, if we do call and treat new.target as a keyword (i.e. the dot carries no meaning; treat it as if it's "newtarget" or "new_target"), parsers couldn't care less. We could even have "new.meta.target" but if "new.meta" isn't an object with a "target" property, then "new.meta.target" is also a ~keyword.
This is also why you cannot have this.super (you gave the this@super alternative) - the dot carries semantics, but this.super cannot be turned into a standalone "keyword" (i.e. super as a metaproperty of this).
What stands out is that a whole new world opens up which adds syntax. For instance, a proposal could add metaproperties to a simplified for loop syntax e.g. for collection console.log(\${for.key}=${for.value}`). Another proposal could add a counter to thewhileloopwhile (true) console.log(while.counter). Or we could promote metaproperties to operators, and create even more operators e.g.let a = 5 /.mod 2` (modulo) Etc. This is the world that we opened. And all of these are additional syntax. An old parser just cannot parse. The language will grow, the parsers will grow.
What I'm missing on this "Not keywords, but still new syntax" is to hear agreement or disagreement. Because keywords or not (see my silly example with an operator) is simply irrelevant detail in my view. What I prioritize is simplicity at cognitive and parser level, sometimes linearly connected but not always.
Correct me if I'm wrong but the first in this league was new.target, and followed by function.sent. The rest is history - I'm personally interested in seeing how meta-properties came to be, not in how a new meta-properties is aligned with another one. The inflection point is at the first or second occurrence (in case the first was an involuntary mistake). If the discussions around those are not substantial, then we have no induction so to speak.
All I could find about new.target are these notes https://github.com/rwaldron/tc39-notes/blob/master/es6/2015-01/jan-27.md#44-subclass-instantiation-reformation-status-and-open-issues where @allenwb says at some point:
Sees options in the future to add things like function.callee
Maybe it could be class.target but new.target is more accurate. These are called MetaProperties in the spec.
Before that there's nothing I can find, the previous suggestion being noted by @wycats :
Early strawman was arguments.constructor.
Same month, but a bit earlier there a reference here https://github.com/tc39/ecma262/blob/master/workingdocs/ES6-super-construct%3Dproposal.md also talking about new.target .
What follows is a document from February 2015 advocating for more metaproperties, describing the problem domain and the opportunity (the new world mentioned above) https://github.com/allenwb/ESideas/blob/master/ES7MetaProps.md#the-problem-and-opportunity . That was good to read, as I think anyone agrees with the problem domain that go like "can't do X", but it still doesn't clarify the addition of meta properties in general, and of the first one "new.target".
The discussion on the function.sent property (just a few months later) doesn't clarify that much either:
Can someone clear the clouds and point to other online resources - proposal, notes? Otherwise, if they want and can, input from the driver of metaproperties on how they came to be would definitely go a long way (I'm guessing you @allenwb drove this ?!).
new.target first appeared (tentatively) in draft 31 of ES6 (on 2015-01-15). See https://esdiscuss.org/topic/a-new-es6-draft-is-available#content-14 and following.
What annoys and scares me the most is that the discussion starts of as valid:
Assume a developer who has never seen this new.target construct before. They will first think that this is an invalid expression, as new is an operator. Then, upon seeing this code execute, the natural question is "What is new? Is it an identifier injected into Environment Records created by [[Call]] and [[Construct]]? Does this identifier resolve to an object (so that the MemberExpression would make sense)?"]
And it is immediately dismissed with
In general, ES6 has new syntax, so this is a "learn it and use it" bump, one of many.
And most discussions regarding new syntax, especially the extension of keywords, is usually immediately brushed off with the same argument
I also love this proposal:
For the long term, I'd like to see a new identifier injected into function scopes which exposes the Lexical Environment/Environment Record internals. Then we can use
__scope__.new.targetorReflect.isNewed(__scope__)(orisConstructed, which may make more sense seeing as there will beReflect.construct). Of course, this__scope__binding should only be injected in the Environment Record if the binding does not exist yet after registering the function body's declarations, for back-compat reasons. And obviously,__scopeis just a placeholder name for this suggestion, I don't really mind how it will be called.
Note how __scope__(or whatever name it may be) solves both new.target, and function.sent, and import.meta
It would not be feasible to "inject" a new non-syntactical identifier into function scopes, because no matter what identifier was chosen, it would break code that was relying on that identifier being a global variable. Only syntax can be used in this manner.
@ljharb I want to clarify in my head what you wrote with an example.
You're saying that "before" you could have some code like:
let __scope__ = {new: {target: 123}};
let fun = function() {
console.log(__scope__.new.target); // forget that i'm not in a constructor for a second
}
but "after" this could lead to a different value being logged, that is the actual new.target ?
You say feasible, not possible. So what's the inconvenience with detecting whether __scope__ is used or not in the function?
@andreineculau
So what's the inconvenience with detecting whether
__scope__is used or not in the function?
eval, for example.
But even then, how would detecting it help? How would you know if this was old code intending to refer to the global, or new code intending to refer to the magic identifier being introduced?
eval I saw it coming the second I pressed Comment, but.. eval...
How would you know if this was old code intending to refer to the global, or new code intending to refer to the magic identifier being introduced?
Do you need to know exactly that? You don't know that even with the new Map, Set, Symbol, Proxy.
What I'm thinking is that at runtime, you can see if __scope__ is defined in the local/closure/global scope. Return that if defined, return the new definition if not.
Same problems as with the new globals i.e. anyone can window.Symbol = function(){} đź’Ł đź’Ą , except __scope__ would be localized i.e. traverse the scopes, if undefined, return a localized value. The proposal could come with immediately marking __scope__ as a future keyword, and giving warning whenever it is defined.
PS: do not do window.Symbol = function(){} on a github page, before you press Comment. It will frighten you.
@andreineculau when adding a new global, someone relying on their own global of that name will end up shadowing the new global, so their code won't likely break. It's only a concern if someone is detecting the presence of the new global, and behaving differently based on that - and that's exactly why global can't be added under that name.
Regardless, anything that's context-sensitive has to be syntax.
Do you need to know exactly that? You don't know that even with the new Map, Set, Symbol, Proxy.
Those are on the global object, not in every function scope. As such, there's no concern with those about the new name shadowing something; they're at the top of the scope chain. This would not be the case if we started injecting identifiers into function scopes.
What I'm thinking is that at runtime, you can see if
__scope__is defined in the local/closure/global scope.
With this proposal, would you be able to close over it? If not, it really is a keyword, not just an identifier; if so, you could never use it in an inner function, because it would always be defined in the outer one.
Anyway, separately, I have to say I can't really imagine how you could find "inject a new identifier into some function scopes depending on, at runtime, whether those functions already have visibility of an identifier of that name" to be a more reasonable design than a new keyword or metaproperty.
This is a circular argument. It contains the same arguments over and over again.
We can't blindly add global objects
Yes, we can. See, Reflect, Proxy, Symbol, temporal proposal. As one comment in temporal issues stated, "Each major browser release adds 4-5 new global objects".
This will break user code
See new global objects above.
Objects/globals/whatnot cannot have access to local context
Yes, they can. It all boils down to how you define them in grammar, and how you describe their behaviour. new.target is basically a hardcoded value in the current spec. import.meta is hardcoded in the proposal. Their behaviour is explicitly specced out for their particular purpose.
They cannot be too global. They cannot be too context-sensitive. Or other arguments between these two extremes
Yes, they can. super.* is context-dependent. function.sent is extremely context-dependent. import's behaviour changes completely depending on the scope it's in. await is only defined for "module scope" (if I'm not mistaken).
This will make the parser more complex. This will make the VM more complex. This will make property access more complex.
Handling of super requires changes to parser/VM: this cannot appear before super call etc.
Handling of metaproperties requires multiple specific changes to both the parser and the VM: function.sent can only be encountered in generator functions. import is top-level only, import() is kinda anywhere else, import.meta has many other specific behaviours associated with it (the algorithm for import.meta is a whole screen of instructions).
This will break expectations from code. This will make magical auto-variables. This will make it harder to reason about code. etc.
.<metaproperty> is applied to very different parts of the language: operators, objects, functions, separate entities only recently introduced into the language. If anything, the whole "BigInt" proposal could be solved with "metaproperties": 1 +.big 2 and 14 **.big 29 (or, better still, we could allow prefix and postfix operators in JS: +.pre 1 2 3 4 and 2 5 6 7 *.post).
import.meta is just as automagical as __scope__.context.module. However, the user of the language can predict and expect __scope__ (or Introspect) to contain just that: scope info or introspection API.
And that's just metaproperties. The whole "let's change import into import()" is another can of worms altogether.
@dmitriid please stay focused.
Objects/globals/whatnot cannot have access to local context
Yes, they can. It all boils down to how you define them in grammar, and how you describe their behaviour. new.target is basically a hardcoded value in the current spec. import.meta is hardcoded in the proposal. Their behaviour is explicitly specced out for their particular purpose.
You're conflating keywords/metaproperties into "Objects/globals/whatnot".
And it was already opinionated that syntax may be the only way to have context-sensitive information... Thus everything that you wrote from there on is just ready to get a "Yeah, syntax is context-sensitive, correct. We just told you that. Your point is?"
@ljharb
It's only a concern if someone is detecting the presence of the new global, and behaving differently based on that
So you're saying that someone might have in their code if (window.__scope__)... ?
I do not see how this is different from introducing window.Symbol ? Couldn't someone have had if (window.Symbol)... before Symbol was defined?
Can I kindly ask you to shed some light?
and that's exactly why
globalcan't be added under that name.
Care to clarify? Which global? Under what name? I simplify do not follow. Thanks.
Regardless, anything that's context-sensitive has to be syntax.
I digested a bit, and there's at least Error which is context-sensitive.
So to play along, if I'm allowed to change a bit the pattern of __scope__, what's off with
class Foo {
constructor() {
let scope = new Scope();
if (!scope.new.target) {
throw new Error('Please use new Foo().');
}
}
}
Thanks a lot for your time.
Yes, someone could have. And if they had, Symbol probably couldn't have been introduced without breaking sites.
For global, I'm referring to https://github.com/tc39/proposal-global
Error isn't context-sensitive; new Error is via error stacks which are not in the spec and it's been highly problematic for security reasons that that's the case for a long time. See https://github.com/tc39/proposal-error-stacks for an attempt to begin to specify stacks.
re: global - now I'm with you, though I tripped once again reading Further research has determined that using global will not break existing code., when in fact this is the relevant issue. So to rephrase, without skipping any relevant context:
and that's exactly why
globalcan't be added under ~that name~window.global. It might go asSystem.global, granted we don't find problems withSystem.
But now that I'm with you, we can agree at least that globals can be introduced (yes, iff they don't break the web). Maybe we were never in disagreement, but that's how I've read your and Kevin's replies. Maybe my bad.
Anyways, why do you say Error is not context-sensitive? Or how come you consider an error's stack (not spec-ed, but maybe spec-ed after your proposal) is not context-sensitive? Context as in literal context. When a stack gets created it gets information about the parent function name, location, etc. It also gets runtime information - the stack frames.
So on which level does new Error() get different to this invented new Scope() that would carry different information based on who is the caller?
Off-topic: System.global, System.getStack, System.WeakRef --- so there's a convergence towards a global System, but is there a proposal/decision overall towards System ?
@bakkot
Anyway, separately, I have to say I can't really imagine how you could find "inject a new identifier into some function scopes depending on, at runtime, whether those functions already have visibility of an identifier of that name" to be a more reasonable design than a new keyword or metaproperty.
Those are two very separate things imho. One is design, one is implementation. The design part is that we want to have __scope__ in the language. The implementation part is the because
if so, you could never use it in an inner function, because it would always be defined in the outer one.
Correct. The constraint is not to break current code. So if someone would window.__scope__ = undefined; then nobody would get to do introspection. Same applies to all the globals, so I must be missing the point you want to make into a counter-argument.
@andreineculau
A new global "__scope__" would not break the following code, but a new function-injected var "__scope__" would:
function foo() {
var __scope__ = false;
function bar() {
if (__scope__) {
return 3;
}
return 4;
}
return bar();
}
@ljharb this was an answer to which of my questions? I can only confirm that's correct, but I don't see where I said otherwise.
We're playing with the idea that since __scope__ cannot be a keyword (TC39 doesn't want to introduce keywords, though I'm telling you that new.target still quacks like a keyword), then it can be a global that returns contextualized information. Error returns stacks. Date returns system time. __scope__ or Scope or new Scope() or System.Scope or new System.Scope() would return introspection information.
This is not the repository for discussing proposals. If you have feedback on a particular proposal, use that proposal's repository. More general feedback should stay in es-discuss.
Most helpful comment
@dmitriid I'll start from the top:
I do agree that there is reason to have concern with the way meta properties are being pretty liberally considered at times.
new.targetdoes feel a little weird from a language design perspective, but it does generally fit - it's literally the target of anewcall.First, plenty of other things have been considered for
import(...)andimport.metaboth.Second, I'd say this is closer to
super(...)(long reserved for that purpose) andnew.target.super(...)performs a context-dependent action, just likeimport(...).super(...)'s scope is derived from that of the enclosingthisimport(...)'s scope is derived from that of the enclosing modulenew.targetgets context-dependent metadata, just likeimport.meta.new.target's scope is derived from that of the enclosingthisimport.meta's scope is derived from that of the enclosing moduleIt would've been nice if
superwas somehow bound tothis(likethis@superor something), so it would be as clean asimport.meta.Oh, and fun fact:
super(...)is actually an expression, not a statement, and it returns the super call's result (as in, the resolvedthis). In addition, it's available in nested arrow functions. So in theory,supercould be implemented as a callable proxy of the inherited prototype, likethis, just exposed as a special keyword. The only reason it can't be spec'd that way is becausesuperitself, just the keyword, isn't an expression.So if you want to know of an object that isn't,
super(...)is about as close as you're going to get. Not evenimportcompares to this one.I do agree that name is probably among the problematic ones, and IMHO it should probably be
yield.sentor similar instead (just filed a bug about it). It really looks wrong for that usage.My bigger issue really boils down to this: why didn't they address that potential issue when they designed generators, rather than a year later.
I'd like to correct a couple of your examples:
awaithas since been used for async functions.publicandprivatewere reserved for controlling method privacy, but I see these as likely to be dropped, considering their original intended future use case uses a sigil instead.I presume the main reason they weren't properly reserved was probably because @BrendanEich (please correct me if I'm wrong) didn't have enough time to consider and address that in his design.
Not really. Unlike
import()vsimport.metaand especiallysuper()vssuper.foo,See my previous notes on
super(...).Arguably, this is the ideal distinction - operators aren't properties, so there is literally zero risk for confusion here.
function.sentis unique to generators, not just any callable object. See my previous notes on it, for my personal objections, but they're purely on naming and lack of foresight, not the choice of a meta property at all.Consider it this way:
import ... from "foo"statically imports values from"foo". It's a top-level statement to reflect its static nature.import(mod)dynamically imports values frommod. It's a call-like expression to reflect its dynamic nature.import.metacontains the import metadata for this module. It's a meta property to reflect its property-like nature.In general, here's the patterns I've observed with the choice of what is used:
var,export, etc., it's almost always a highly declarative construct.break,continueyieldorawait, it's almost always space-separated.if/elseandforsuper(...)orimport(...), it's almost always call-like.typeofargumentsIn my experience, it's been adding much needed structure and rigor.
newexisted without a way to declaratively make a constructor.takeWhilefor Java-like iterators? It's trivial using ES6 generators andfor ... of.Also, here's a rundown of all the existing Stage 4 proposals with significant added syntax:
awaitto become a contextual keyword in order to avoid ambiguity.And stage 1-3 with significant added syntax (ignoring RegExp stuff):
import(...)has already been discussed above.#), not a keyword.function.senthas already been discussed above.import.metahas already been discussed above.doexpressions have bodies.