-
-
Save getify/5253319 to your computer and use it in GitHub Desktop.
// This is a theoretical exploration of something that could | |
// be added to JS. It borrows from JS events which have both a | |
// `target` and `currentTarget` element, which distinguish between | |
// the direct target of an event and the delegated parent handling | |
// the event, respectively. | |
// The idea is to distinguish between `this` and `currentThis`, | |
// respectively, as... | |
// | |
// `this`: the initial containing object for a call that initiates a | |
// traversal of the [[Prototype]] chain (just as it currently does) | |
// | |
// `currentThis`: the actual link in the [[Prototype]] chain where the | |
// traversal resolution is completed. | |
// Notice below that with `bar1` for example, `this` will point to | |
// `bar1` even when using the delegated `identify()` function found | |
// on `Foo`, but `currentThis` will point to `Foo`, since that's | |
// where `identify()` was found in the chain. | |
// So... what if: | |
var Foo = Object.create(null); | |
Foo.me = "Foo"; | |
Foo.identify = function() { | |
// whenever Foo#identify() is called, `currentThis` will | |
// always be `Foo`, but `this` will continue to follow the | |
// established rules for determining a `this` binding. | |
console.log("Me: " + this.me + "; Current: " + currentThis.me); | |
}; | |
var Bar = Object.create(Foo); | |
Bar.me = "Bar"; | |
Bar.another = function() { | |
console.log("Current: " + currentThis.me); | |
} | |
var bar1 = Object.create(Bar); | |
bar1.me = "bar1"; | |
var bar2 = Object.create(Bar); | |
bar2.me = "bar2"; | |
Foo.identify(); // "Me: Foo; Current: Foo" | |
Bar.identify(); // "Me: Bar; Current: Foo" | |
Bar.another(); // "Current: Bar" | |
bar1.identify(); // "Me: bar1; Current: Foo" | |
bar2.identify(); // "Me: bar2; Current: Foo" | |
bar1.another(); // "Current: Bar" | |
bar2.another(); // "Current: Bar" |
@allenwb one other thought:
Your lazy relookup to determine currentThis only when it's actually referenced breaks if properties are added/removed or the [[Prototype]] chain is dynamically modified after the call but before the reference.
If I am understanding you correctly, you're suggesting that currentThis
would have been statically "fixed" at compile time, and that my lazy-lookup at runtime could give wrong answers?
Actually, I think that's specifically what I intended for currentThis
, which is that the author-time decisions are irrelevant, and the actual run-time lookup on the [[Prototype]]
chain is what will determine where currentThis
ends up pointing to.
Indeed, if you were to dynamically re-arrange the [[Prototype]]
chain, I would want currentThis
to reflect the "situation on the ground" rather than from a compile-time assumption.
Similarly, imagine if you were to use the "mixin" pattern on the example above, and were to put a reference to the identify()
function (as he currently exists only on Foo
) directly onto the Bar
object, and then do the exact same bar1.identify()
call. NOW the function would be found in the lookup at an earlier step in the lookup chain than before, at Bar
rather than at Foo
, and so I'd want for that invocation of identify()
to bind currentThis
to Bar
, not Foo
.
In other words, currentThis
is intended to be a call-time binding, not a compile-time/lexical binding.
In that sense, then, isn't the lazy-lookup I showed (perf concerns aside) the correct semantic for currentThis
?
EDIT: I just realized what you probably meant. You're probably talking about if you change properties or re-wire the [[Prototype]]
chain from inside the function itself, right? Yeah, that's tricky, tricky. Not sure whether I'd want the currentThis
to be modifiable at that point or not. I can see arguments for both. :)
EDIT #2: I updated my little sorta-polyfill to match a better semantic, where the lookup is done once, as the first line of the function, so shouldn't be susceptible to weirdness about changes in properties or [[Prototype]]
chain.
Whether a particular function uses super
(or currentThis
) can be statically determined when the function is parsed and that fact can be record somewhere. Most likely in some sort of descriptor that is accessible relative to the address of the function.
However, at a call-site we generally don't know what particular function is being called because the function is obtained via a property lookup or a parameter value, etc. So if the call site wants to only pass the currentThis``value to functions that actually use it, it needs to inspect the target function's descriptor to see if it uses
currentThisand then conditionally pass the
currentThis` value based upon what was recorded in the descriptor.
There are a lot of degrees of variability at this "close to the metal" level of design, but a reasonable simplification of the implementation alternatives are:
- Always unconditionally pass the
currentThis
value. This has an idealized cost of 1 memory store on every call. - Only pass
currentThis
if the target function is flagged as needing it. This has an idealized cost of 1 memory read + 1 test + 1 conditional branch on every call. If the branch is taken because the target function actually needscurrentThis
then there will be a store and a branch back to the fast path.
Stores are expensive, but so are reads and branches, so at this level of abstraction we might as well say that the two alternatives add approximately the same base overhead to every function call and that overhead is approximately that of one additional argument.
Yes, I was referring to __proto__
tampering that might take place after the call but before the reference to super
. An arbitrary large amount of code might execute between those two points include code that does loading, code-generation, dynamic mix-ins, etc.
It's not always obvious what the best semantics are in situations like this. However, in general JS property accesses are defined to reflect the state of objects at the point when the access is made. This allows for the possibility that object structure is dynamically highly mutable. It is probably best to minimize exceptions to that principle.
@allenwb Thanks again for the wonderful detailed explanation. Sheds a lot of light on things.
One last point of curiosity: you've mentioned a couple of times that the decision of if a function needs a
super
orcurrentThis
passed to it is a dynamic one. I'm trying to figure out why that would be the case?It seems from my naivety that a static analysis of the function at compile time (basically, the function has to already have been analyzed before execution, right?) could flag it as "needingCurrent" or not. If that were possible once, at compile time, it seems like it would be "cheaper" than having to dynamically inspect (or just always pass
currentThis
). And if that were the case, would perf be roughly similar to the static lexicalsuper
binding you mentioned was decided for ES6?What circumstances or code constructs (besides eval/with) could cause the engine to have to dynamically determine the need for
currentThis
?In any case, I'll just trust your expertise that it was looked at and couldn't be made to work. Bummer though. I think a
currentThis
would be helpful in several cases.