Unscopeable
Can somebody (private mail is okay) please clarify this for me? It looks me
like there's a proposal to introduce @@
syntax to support something
that's forbidden in most es6 code anyways (with
)? Or does it have other
use cases? Where can I read more about this?
Some answers inline.
On Wed, Aug 21, 2013 at 7:58 AM, Andreas Rossberg <rossberg at google.com> wrote:
I'm still trying to wrap my head around the idea of introducing an @@unscopeable property, as discussed at the last meeting. There seem to be plenty of edge cases:
- How would it interact with inheritance?
with always uses the full prototype chain. @@unscopable is gotten using a [[Get]] so the whole prototype chain is considered.
let p = {a: 1}
let o = {__proto__: p, [@@unscopeable]: ['a']}
let a = 2
with (o) {
a // 1 or 2?
}
2, 'a' is blacklisted from being part of the object environment record.
let p = {[@@unscopeable]: ['a']}
let o = {__proto__: p, a: 1}
let a = 2
with (o) {
a // 1 or 2?
}
2, for the same reason
- How would it affect assignment?
This is one of the details we didn't talk about, but the concept is that 'a' is not part of the object environment record, so free a means the lexical a (if any).
One way to think of it is that the with is acting on a view of the object that does not have the blacklisted properties. This can be implemented using proxies. I'll leave that as an exercise.
let o = {[@@unscopeable]: ['a']}
let a = 2
with (o) {
a = 1
a // 1 or 2?
1
o.a // 1 or undefined?
undefined
}
a // 1 or 2?
1
let p = {a: 1, [@@unscopeable]: ['a']}
let o = {__proto__: p}
let a = 2
with (o) {
a = 3
a // 1 or 2 or 3?
3
o.a // 1 or 3?
1
}
a // 2 or 3?
3
- How would it interact with mutation?
Another hole we didn't cover. Here is my proposal:
The @@unscopable is only accessed once, when the object environment record is created.
let o = {a: 1}
let a = 2
with (o) {
o[@@unscopeable] = ['a']
a // 1 or 2?
1, @@unscopable was set too late.
}
- How would it interact with compound assignment?
this one falls out of the previous one.
let o = {a: 1}
let a = 2
with (o) {
a += (o[@@unscopeable] = ['a'], 2)
a // 2 or 3 or 4?
3
o.a // 1 or 3?
3
}
a // 2 or 3 or 4?
2
- How would it affect the global object? (At least currently, with scopes and the global scope share the same object environment mechanism.)
The idea was that it would be part of with only. We don't want to tax the global property lookups for this.
var a = 1
this[@@unscopeable] = ['a']
a // 1 or undefined?
1
var a
this[@@unscopeable] = ['a']
a = 1
a // 1 or undefined?
1
this[@@unscopeable] = ['a'] </script><script> var a = 1 a // 1 or undefined?
1
- How would it expose side effects?
It does a [[Get]] in the with head.
let s = "" let o = {a: 1} let u = {get '0'() { s+="0"; return 'a' }, get '1'() { s+="1"; return 'b' }} Object.defineProperty(o, @@unscopeable, {get: function() { s += "u"; return u }}) with (o) {} s // "" or "u" or "u01" or ...? with (o) { c; c } s // "u01" or "u01u01" or ...? with (o) { a } s // "u01" or "u0" or ...? with (o) { a+=1 } s // "u0" or "u00" or "u01" or "u0101" or ...?
Some of this looks rather whacky, if not unpleasant.
If you think of it as only doing it once in the with head things become much simpler to understand.
On 21 August 2013 15:18, Peter van der Zee <ecma at qfox.nl> wrote:
Can somebody (private mail is okay) please clarify this for me? It looks me like there's a proposal to introduce
@@
syntax to support something that's forbidden in most es6 code anyways (with
)? Or does it have other use cases? Where can I read more about this?
It's a proposed backwards compatibility thing to work around the problem that uses of 'with' in the wild are not robust against extensions of standard objects, see Section 4.3 of the July 23 meeting notes:
rwaldron/tc39-notes/blob/master/es6/2013-07/july-23.md
Unfortunately, many people still use 'with', and we can't break the web.
(The @@-syntax is just a placeholder we got into the habit of using to denote predefined symbols.)
Here is a proof of concept using Proxy (Spidermonkey only)
It rewrites the with (expr)
with with (createUnscopeable(expr))
where createUnscopeable returns a proxy that filters out the black
listed property names.
On Wed, Aug 21, 2013 at 2:41 PM, Erik Arvidsson <erik.arvidsson at gmail.com> wrote:
The idea was that it would be part of with only. We don't want to tax the global property lookups for this.
with
and www.whatwg.org/specs/web-apps/current-work/multipage/webappapis.html#event-handler-content-attributes
It would be extremely useful on the latter for new methods on node
objects.
On 21 August 2013 15:41, Erik Arvidsson <erik.arvidsson at gmail.com> wrote:
One way to think of it is that the with is acting on a view of the object that does not have the blacklisted properties. This can be implemented using proxies. I'll leave that as an exercise.
I see.
- How would it interact with mutation?
Another hole we didn't cover. Here is my proposal:
The @@unscopable is only accessed once, when the object environment record is created.
Hm, that would seem rather inconsistent with the way adding/removing properties can dynamically change the domain of a 'with'. Consider:
function safeenrich(o) {
o.a = 1
o[@@unscopable] = o[@@unscopable] || []
o[@@unscopable].push('a')
}
let o = {}
let a = 0
with (o) {
a // 0
safeenrich(o)
a // 1
}
On Wed, Aug 21, 2013 at 10:07 AM, Andreas Rossberg <rossberg at google.com> wrote:
Hm, that would seem rather inconsistent with the way adding/removing properties can dynamically change the domain of a 'with'. Consider:
I don't think it is worth covering these kind of new scenarios. We are patching a broken feature here.
Still, it is easy to allow that if we really wanted to. We can make ObjectEnvironment [[HasBinding]] do object.[Get] every time then.
On Wed, Aug 21, 2013 at 10:05 AM, Anne van Kesteren <annevk at annevk.nl> wrote:
with
and www.whatwg.org/specs/web-apps/current-work/multipage/webappapis.html#event-handler-content-attributes It would be extremely useful on the latter for new methods on node objects.
Of course. This is actually even more important than the pure ES scenario.
On Aug 21, 2013, at 7:46 AM, Erik Arvidsson wrote:
I don't think it is worth covering these kind of new scenarios. We are patching a broken feature here.
Still, it is easy to allow that if we really wanted to. We can make ObjectEnvironment [[HasBinding]] do object.[Get] every time then.
@@unscopable and its interaction with the with state is specified in the ES6 draft that I will release this weak
It is specified as Erik describes, @@unscopable is only access when a with is entered. The computability use case was designed to support doesn't involve dynamically adding properties to a with object. Instead, the problem is inherited properties of the with object shadowing out scope bindings.
It's possible to opt-out of the mechanism via:
Object.mixin( withobj, { [@@unscopable]: [] });
with (withobj) {
for (v of values()) ...
}
On 21 August 2013 16:02, Erik Arvidsson <erik.arvidsson at gmail.com> wrote:
Here is a proof of concept using Proxy (Spidermonkey only)
It rewrites the
with (expr)
withwith (createUnscopeable(expr))
where createUnscopeable returns a proxy that filters out the black listed property names.
A small nitpick: that implementation doesn't quite work, because it does not delegate other operations correctly. The wrapping proxy will leak into the object as receiver. Consider:
var o = {
__unscopeable__: ['a'],
a: 1,
f: function() { return 'a' in this }
}
with (createUnscopeable(o)) {
f() // false!
}
A proper implementation would require explicit delegation for all receiver-aware traps. Left as an exercise. :)
On 21 August 2013 16:46, Erik Arvidsson <erik.arvidsson at gmail.com> wrote:
I don't think it is worth covering these kind of new scenarios. We are patching a broken feature here.
I understand, but when the intent is to patch something, then I think that the semantics of the patch should be in sync with the thing it's patching.
Here is a not completely unlikely scenario where it might matter:
-
Some library author thinks it's a good idea to monkey-patch Object.prototype and extends it with a new method. He is aware of the danger of doing so, so wants to play nice(r) by making the extension unscopeable.
-
Some application uses the aforementioned library. In another part it has a 'with' statement that contains a call to some function that can trigger lazy loading of the library under certain circumstances.
With the problem being as dynamic as it is, I think the workaround should be equally dynamic.
Here is another issue with your proposed (and already spec'ed) semantics: applying @@unscopable across the inheritance chain is a breaking change. Consider the following, fairly innocent ES5 code:
var a = []
a.values = "Hello"
with (a) {
console.log(values)
}
Under ES5, this happily prints "Hello", under the latest ES6 spec it would be a ReferenceError.
So it seems to me that it's wrong to apply an @@unscopable black list to arbitrary objects in the inheritance chain. It should only filter properties found on its own object.
(Strictly speaking, of course, @@unscopable will be a breaking change even without that, since somebody might try to monkey-patch Array.prototype itself with a 'values' property. But monkey patching has always been brittle, and deserves to break. The above example is in a different class, since it only concerns a local object.)
Andreas Rossberg wrote:
So it seems to me that it's wrong to apply an @@unscopable black list to arbitrary objects in the inheritance chain. It should only filter properties found on its own object.
Great point.
I agree that this is crucial. I think that tips the balance over to have to do the Get of @@unscopable on every HasBinding.
How so. The timing of the @@unscopable property and the the process of using it to filter identifier binding are completely separable procedures. How does restricting the filtering to own properties change the rationale for getting the blacklist property once per 'with' rather than on every variable reference.
I just felt things got a bit too complicated to spec.
I realize now that the complexity is not very different either way.
I'm ambivalent which behavior I prefer. Either way solves the use case but dynamic lookup has the benefit that will better handle crazy edge cases like changing @@unscopable and [[Prototype]] within the with statement.
At the cost of turning every implicit property access within a with
into two property accesses (which are observable vis proxies or accessors) even in normal cases that are far away from the crazy edges.
The most important thing for crazy edges is that they are well-defined. I don't think we should make things better in the crazy zone at the cost of making them worse in outside the crazy zone.
I would not worry about the cost -- if you want remotely decent performance, you wouldn't use with-scopes anyway (e.g. in V8, they do not get optimized at all, and every variable lookup already is a runtime call). Also, it's not hard to fast-case the normal case for objects that have no @@unscopable.
Conceptual consistency and the principle of least surprise is more important. The potential performance cost is the honest price to pay for having a complicated object model. I don't like trying to cheat our way around that.
I'm still trying to wrap my head around the idea of introducing an @@unscopeable property, as discussed at the last meeting. There seem to be plenty of edge cases:
How would it interact with inheritance?
let p = {a: 1} let o = {__proto__: p, [@@unscopeable]: ['a']} let a = 2 with (o) { a // 1 or 2? } let p = {[@@unscopeable]: ['a']} let o = {__proto__: p, a: 1} let a = 2 with (o) { a // 1 or 2? }
How would it affect assignment?
let o = {[@@unscopeable]: ['a']} let a = 2 with (o) { a = 1 a // 1 or 2? o.a // 1 or undefined? } a // 1 or 2? let p = {a: 1, [@@unscopeable]: ['a']} let o = {__proto__: p} let a = 2 with (o) { a = 3 a // 1 or 2 or 3? o.a // 1 or 3? } a // 2 or 3?
How would it interact with mutation?
let o = {a: 1} let a = 2 with (o) { o[@@unscopeable] = ['a'] a // 1 or 2? }
How would it interact with compound assignment?
let o = {a: 1} let a = 2 with (o) { a += (o[@@unscopeable] = ['a'], 2) a // 2 or 3 or 4? o.a // 1 or 3? } a // 2 or 3 or 4?
How would it affect the global object? (At least currently, with scopes and the global scope share the same object environment mechanism.)
How would it expose side effects?
let s = "" let o = {a: 1} let u = {get '0'() { s+="0"; return 'a' }, get '1'() { s+="1"; return 'b' }} Object.defineProperty(o, @@unscopeable, {get: function() { s += "u"; return u }}) with (o) {} s // "" or "u" or "u01" or ...? with (o) { c; c } s // "u01" or "u01u01" or ...? with (o) { a } s // "u01" or "u0" or ...? with (o) { a+=1 } s // "u0" or "u00" or "u01" or "u0101" or ...?
Some of this looks rather whacky, if not unpleasant.