Description: Encapsulated destructure should return an object composed of the destructured property/value pairs. Instead, leaks global variables and returns the original unprocessed object.
eshost Output: x should unreferenced, u should be { x: 5 }
$ eshost -s -x "const u = ({x} = {x: 5, y: 10}); print(JSON.stringify(u)); print(x)"
#### ch, jsc, sm, v8, xs
{"x":5,"y":10}
5
eshost Output: x should be { x: 5 }, encapsulating the destructure in brackets should convey clear intent not to implicitly create variables but instead return them as part of the assignment expression
$ eshost -s -x "const x = ({x} = {x: 5, y: 10}); print(JSON.stringify(x))"
#### ch
ReferenceError: Use before declaration
#### jsc
ReferenceError: Cannot access uninitialized variable.
#### sm
ReferenceError: can't access lexical declaration `x' before initialization
#### v8
ReferenceError: Cannot access 'x' before initialization
#### xs
ReferenceError: ?: set x: not initialized yet
x should be { x: 5 }
assignment always returns the right hand side, not the left hand side.
leaks global variables
which variables are being leaked?
assignment always returns the right hand side, not the left hand side.
This is the case, however, considering the code I have presented in isolation - I would expect ({ x } = { x: 5 }) to evaluate to the result, the destructure operator (which looks like equals =) is an operation which has a result, as opposed to relying on side-effects.
which variables are being leaked?
Any on LHS of the destructure expression, when not in 'use strict' (being in strict mode is even more clear in the intent, ({x} = { x:5 }) should not create any variables in its evaluation, only when const/let/var is before it should the variables be assigned (but only to the scoped variable, not globally).
(side note: I've been working with JS since fairly early days, this is the one feature whose implementation strikes me as incomplete)
What you’re describing is how assignment has always worked in the language; a = b = 3 works the same (chaining assignments doesn’t encapsulate anything)
Given your example a = b = 3, were you to enclose it as such: const a = (b = 3), I would expect b to be an assignment to an existing variable (throwing an error if b is not defined), and a to be the result of the assignment.
Yet, that’s not how chained assignment has ever worked, nor at this point can ever work.
parenthesis are grouping, they don't change the value as it is evaluated. (in fact, (a) = 5 is valid)
Does this seem fully thought out? Things being always as they have been goes so far, this feature fails to make sense when brought to the fullest conclusion (being able to refer collectively to the variables destructured as an object, otherwise I am forced to restate the variables I have destructured).
Consider the following:
const handler = { set(obj, prop, value) { return obj[prop] = 5 } }
const proxy = new Proxy({}, x)
console.log(proxy.foo = "bar"); // 'bar'
console.log(proxy.foo); // 5
But (proxy.foo = "bar") returning 'bar' is a lie, and unhelpful, I know already what the RHS is.
Personally I’d have made assignments be a statement, so you couldn’t ever use them in expression position. Unfortunately for me, though, they’ve returned the RHS since JS’ inception afaik, and it would have been problematic for destructuring to create inconsistency around how assignment works.
Sure, thanks for that, my argument is that returning the RHS is unreliable/misleading - assignment is an operation which has a return value, which I should be able to rely upon to tell me the result of the assignment, I can receive an error if the assignment fails, but I cannot be informed (without subsequent query of the object) for the result of the assignment. In which case, perhaps the current definition of assignment needs consideration. Or, the return value should be dropped entirely and assignment be made a void operation with side-effects only.
regardless of the virtues of the feature, it can't be changed, as js maintains full backward compatibility.
Perhaps strict mode could be enhanced, given that at the moment it throws non-standard errors across the different engines, there seems to be no consensus on how to treat the second eshost Output case from my first post. Especially since there can be no implicit variable creation in strict mode.
eshost Output:
xshould be{ x: 5 }, encapsulating the destructure in brackets should convey clear intent not to implicitly create variables but instead return them as part of the assignment expression$ eshost -s -x "const x = ({x} = {x: 5, y: 10}); print(JSON.stringify(x))" #### ch ReferenceError: Use before declaration #### jsc ReferenceError: Cannot access uninitialized variable. #### sm ReferenceError: can't access lexical declaration `x' before initialization #### v8 ReferenceError: Cannot access 'x' before initialization #### xs ReferenceError: ?: set x: not initialized yet
They all throw a ReferenceError. Error messages have never been standardized. This is the same case as const x = x;.
I argue that error messages should be standardised going forward, does it provide the developer value for them not to be? I largely expect (having discovered eshost) all implementations to return the same output.
I still maintain that const x = x; has an entirely different intent to const x = ({x} = RHS), especially in strict mode where there is no implicit creation of x, and since it is widely known to be inadvisable to write control flow around errors (especially where the error message cannot be relied upon), this provides space for backward compatible enhancement of the destructure and even assignment operations.
I am new here, but would like to see this feature becoming more solidified in its reasoning, and to become dependable, at the moment I cannot rely on assignment to return the assigned value, nor can I use destructure and then reference the set of my destructured variables. Is there a way this can be implemented, without forcing developers into a common lisp situation of creating dialects whereby expectations can be reasonably fulfilled? Or avoid having to wrap such basic operations.
I argue that error messages should be standardised going forward, does it provide the developer value for them not to be?
There are a few reasons I can think of that messages aren't standardized:
I am new here, but would like to see this feature becoming more solidified in its reasoning, and to become dependable
Like i said above, we can't change how this part of the language works, as it would break existing websites and codebases.
Is there a way this can be implemented, without forcing developers into a common lisp situation of creating dialects whereby expectations can be reasonably fulfilled?
Given that JS has such extremely strong backwards compatibility guarantees, most of its warts are here to stay, forever.
So your best option is to use a compile-to-JS language, or a compile-to-WebAssembly language. There are many of those available, so you should be able to find one which suits your taste.
Or you could create a Babel plugin which would automatically transform all assignment expressions so that they have the behavior you want. Making a Babel plugin is a lot easier than making a whole new language.
I argue that error messages should be standardised going forward, does it provide the developer value for them not to be? I largely expect (having discovered eshost) all implementations to return the same output.
It turns out that sometimes even changing error messages can have a web compatibility cost if it turns out that a popular library was string-matching on them. :scream: Still, you're not the only one that feels this way—you might be interested in getting involved in @codehag's nascent Better Errors initiative!
Allowing hosts to provide messages in additional languages and formats
Could this be achieved by augmenting the error object with a property (string key or symbol) containing the localised error messages? I wish to be able to retrieve information from the error message (such as the offending variable name) without having to strip its text, being mindful of different messages on a per host basis.
Allowing hosts with limited memory to not include them at all
I'm not convinced that by failing to standardise errors we provide benefits such as this, a standard approach might include the ability for a host to indicate that the error is intentionally omitted, or even provide a link to a more comprehensive trace.
Allowing hosts to provide error messages that include information the spec doesn't implicitly provide in the context of throwing the error.
Supposing there is not yet a standard way of augmenting the error object to include such information, there could be a standard way of accessing these host specific adaptations.
Given that JS has such extremely strong backwards compatibility guarantees, most of its warts are here to stay, forever.
I understand this, though I'm seeking a solution which will not change current expectations (though I do not comprehend why anyone would need to be returned a value which is guaranteed to be the same as they just passed, in both assignment and destructure cases). Am I wrong in my belief that by standardising how errors provide localised error messages, and access to underlying error context (e.g., variable name), in combination with 'use strict' (whose behaviour limits and disables such implicit variable creation), that by implementing proper return behaviour it will not constitute a backward incompatible feature?
As a side note, like the introduction of 'use strict' did not break backward behaviour, can other string statements (or, introduce an alternative to 'use strict', like (['strict', 'truthfulAssignment', 'destructureIntoForm'];, bad example for a good inspiration) be used to enable certain edge features, like introducing truthful return values for the assignment operator (as demonstrated by the proxy example).
I am writing a library to transform such code, as babel does, and am using hacks to split on the error message strings for some other enhancements (which I already do not like), but in the last few months I have been teaching JavaScript to fellow students, and every time I feel somewhat ashamed not being able to demonstrate destructuring in a way that allows reference to the destructured variables as a whole (this is a core and fundamental expectation of mine), I can only assume it was glossed over to not instigate this sort of debate.
destructuring in a way that allows reference to the destructured variables as a whole
That's just creating an object with some properties. Destructuring is about the exact opposite, pulling properties from an object.
though I do not comprehend why anyone would need to be returned a value which is guaranteed to be the same as they just passed, in both assignment and destructure cases
That doesn't matter. What matters is that somebody, somewhere is depending on it, and their website will break if the behavior is changed.
Whether their code is sensible or not doesn't matter. What matters is that JS does not break people's existing code. This is a promise that JS makes to programmers, and this promise is necessary in order to not break the web.
As a side note, like the introduction of 'use strict' did not break backward behaviour, can other string statements be used to enable certain edge features, like introducing truthful return values for the assignment operator (as demonstrated by the proxy example).
Yes, it's theoretically possible, but it's extremely unlikely that a new JS mode will be accepted by the TC39 committee.
That's just creating an object with some properties. Destructuring is about the exact opposite, pulling properties from an object.
Precisely, but for what ends? Pulling properties from an object, into where? At the moment it's either the defined scoped variables, implicit global creation, or a non-standard error in case of strict. I pull properties from an object, I then wish to pass along the pulled properties, I cannot do this without reiterating all the properties I just stated I wished to pull, or wrapping it - a dull solution, other developers will face this and be left to design a fix, whereas it could be standard behaviour that assignment and destructure when in specific usage return the value that was assigned or return the values that were destructured (at the moment the return value of both is inconsequential or just incorrect, I must still query the object in a subsequent operation to see whether assignment actually assigned the value I was returned - only to find out that because a Proxy object set a hook that changes the assignment value, I have been deceived).
I'm running out of strip here, I can concede at this point, but I feel that this reveals an oversight that could be addressed without breaking prior expectations, perhaps I will see something in the future, but otherwise suggestions of a standard solution would be appreciated.
Yes, it's theoretically possible, but it's extremely unlikely that a new JS mode will be accepted by the TC39 committee.
Perhaps, though it might address other issues elsewhere, where features/fixes are held back due to backward compatability concerns. I won't hold my breath on that approach though, cheers.
Going back to your original question, it seems that you might be expecting destructuring to help you do what Lodash's pick function does? But it's not actually intended for this purpose.
let { x } = { x: 5, y: 10 }; is strictly a way of defining a variable x using a value from an existing object. It _destructures_ but it doesn't _restructure_—i.e., it never creates a smaller object containing just x. The very same behavior applies even if we split the declaration from the destructuring assignment:
let x;
({ x } = { x: 5, y: 10 });
Since this is fundamental to the destructuring feature, users have the right to expect that the following pair of snippets are equivalent:
let x = 5;
let u = { x, y: 10 };
let x;
let u = { x } = { x: 5, y: 10 };
So I'm afraid the semantics you seek would be those of a different language, as others have suggested. :sweat:
Although this can’t change because people and minifiers make use of how it works today, in case you want to explore it further by some means (proposing new syntax, etc), here’s some spots that left me confused about what behavior is desired:
{ foo } = bar is property shorthand, sugar for { foo: foo } = bar. I assume we would get { foo }, not { baz }, for { foo: baz } = bar. Otherwise e.g. { foo: something.bar } = bar and the get-5-back-from-the-proxy example wouldn’t make sense.for example, what would you expect the evaluatedValues to look like in each case here?
let a, b, c, d, e, f, g = {};
const evaluatedValues = [
{ a, a: b } = { a: true },
{ x: { c=true }={} } = {},
{ y: [ d, ...e ] } = { y: '123' },
{ f, ...g.rest } = { f: true, z: true }
];
Possibly of interest: a proposal that I think was exploring related ideas: https://github.com/rtm/js-pick-notation
I still maintain that
const x = x;has an entirely different intent toconst x = ({x} = RHS), especially in
Who know what the intent of somebody who wrote such code might have been, but in either case they seem to not understand the semantics of the const declaration. Similarly, I get the impression that you probably don't either. You really should or at least be able to analyze such statements using the actual language specification before opening an issue here.
Here are a few things that the language spec could tell you:
The left most = in both of the above const statements is not an _assignment operator_ but syntaxicaly marks the start of a declaration _initializer_. _initializers_ and the _assignment operator_ have different semantics.
The const statement introduce a lexically scoped declaration of x within the current scope.
All references to the identifier x within a scope bind to the same variable or constant.
Lexically scoped bindings for an identifier such as x are in their "temporal dead zone" (TDZ) from the point the scope is entered until they are initialized via their declared _initializer_.
Evaluating a reference (either a read or write) to an identifier that is in its TDZ produces a Reference Error.
The expression to the right of the = in an _initializer_ is evaluated while the identifier to the left of the = is still in its TDZ.
So,
const x = x; always produces a ReferenceError because because the _intitializer_ expression attempts to access the value of x which is in its TDZ.
const x = ({x} =RHS]; also produces a ReferenceError for essentially the same reason. this is easier to see if you understand that the semantics of ({x}=RHS) is essentially the same as (x=(RHS).x). So it attempts to assign to x which is in its TDZ.
@rkirsling Thanks for the clarification, especially with the last snippet's functionality being intentional, I had always thought that assignments flowed from the right most side, such that let u = { x } = { x: 5, y: 10 } would destructure into { x }, then (I suppose) restructure into u, rather than u being assigned a seemingly redundant initial value). Incidentally, I was hoping that by grouping the expression with brackets let u = ({ x } = { x: 5, y: 10 }) the engine would realise that it is the result of the operation (the sum result of destructuring) that interests me.
The proxy example with assignment just unsettles me, I don't know why we would expect the set trap to trigger and the value be modified but then the returned value be not reflective of the alteration, we have operators like ++number vs. number++, yet assignment returns the value to be assigned but not the resultant assigned value. This is something that'll sit in the back of my mind from now.
@bathos - thanks for those.
{ a, a: b } = { a: true }
This is a destructure of { a: true } onto a, with a subsequent rename afterwards (fortunately order seems guaranteed here), b can then be given a default value if a was undefined.
{ x: { c=true }={} } = {}
An empty object is destructured onto x, I initially saw x: {} as a nested destructure but it's actually another assignment of a destructure, this time c is destructured from the empty object and assigned a default value of true.
{ y: [ d, ...e ] } = { y: '123' }
Thanks for introducing me to this, I didn't realise destructuring was quite expressive enough to destructure the string (being treated as an arraylike) and assigning those parts d and the rest e.
{ f, ...g.rest } = { f: true, z: true }
Brilliant, it didn't occur to me that by destructuring some properties, the rest can be unfolded (it's a bit of a shame this isn't happening on the return value) onto an object g, .'rest' I thought to be erroneous but it seems this is how to specify a property on g to destructure into, so my original query can -sort of- be resolved as long as the remaining properties are that which you want in the resultant object.
That proposal is what I was unable to find in searching, I had only chanced upon posts of people effectively finding out that you cannot 'destructure onto an object' ("restructuring" was the phrase missed, except how specified in your final example).
@bathos Something like { u, [y, z]: outbox = [-10, -15], ...unused } = { x: 5, y: 10, z: 15, u: 3 } would probably satisfy my requirements whilst fitting into the general style and not being a separate 'restructure'. Thanks for your illumination.
I haven't yet found an ideal solution for avoiding { [symbol]: symbol } because { [symbol] } is treated as an expression returning a single array, but var p = ({ [x] }) throws a SyntaxError which perhaps could be implemented.
@allenwb tone aside, thanks. My hope was that ({x}=RHS) in a strict setting would be enough to indicate that (because no implicit variable can be created) that it not a reference to a variable in the "TDZ". I am aware that declarations and assignments differ, my hope was also that assignment (as demonstrated in the proxy example) would return the actually assigned value - not the initial value to assign. Is there any particular logic to why the returned value from the assignment f.bar =~3 (when the value to be set is intercepted by a proxy trap and modified) is not the finally set value? (And perhaps why this is intentional design?)
@Rob-pw glad if that helped clarify some of the facets of binding patterns to consider. The g.rest was to include an example that showed that outside of a declaration/arguments, the assignment targets can be any expression that works as a reference, not just an identifier.
I’m not sure I understand what’s intended by [y, z]: outbox = [-10, -15]. This is valid syntax, but [y, z] would be a computed property name and the comma here would be the comma operator (so it’d be the same as just [z], unless referencing y had side effects, e.g. by being an accessor on globalThis).
May I suggest visiting https://es.discourse.group/ if you’d like to discuss your ideas further? This repo is primarily for ‘boring’ issues and PRs like those addressing specification errors, editorial nits, and integrating proposals as they close in on stage 4, while the discourse group is appropriate for general questions, exploring ideas, and getting feedback on potential proposals.
I've closed this issue as the issues seem to be known. Some clarification on the unexpected nature of assignment would be appreciated, I may open another issue if this is warrants a separate discussion.
@bathos - I am unable to get the below example to work, which is desired, if the syntax does not exist (I do not believe the comma operator is valid here), then perhaps this is a space in the destructuring syntax where restructuring can be specified (restructure y, and z into 'outbox' where the default values are -10 and -15 respectively, that way no new operator needs to be introduced and we could continue in the spirit of f, ...g.rest. It should not be confused with computed properties, because a missing p value would give way to q by use of the or operator u = { [p || q] = ['tim'] }
var p = "bob"
undefined
> var q = "jeff"
undefined
> u = { [p, q] = ['tim', 'martin'] }
Thrown:
u = { [p, q] = ['tim', 'martin'] }
^
SyntaxError: Unexpected token ,
@Rob-pw You’re correct, I was mistaken that comma was permitted there without parentheses. It does indeed appear to be open syntactic space.
@bathos In that case I'll try my hand at putting a proposal together, thanks for the review and your wider insight.
Most helpful comment
Going back to your original question, it seems that you might be expecting destructuring to help you do what Lodash's pick function does? But it's not actually intended for this purpose.
let { x } = { x: 5, y: 10 };is strictly a way of defining a variablexusing a value from an existing object. It _destructures_ but it doesn't _restructure_—i.e., it never creates a smaller object containing justx. The very same behavior applies even if we split the declaration from the destructuring assignment:Since this is fundamental to the destructuring feature, users have the right to expect that the following pair of snippets are equivalent:
So I'm afraid the semantics you seek would be those of a different language, as others have suggested. :sweat: