4 ms·
Because I said left to the dot at "call time". More succintly: > When you call a function, whatever is left of the dot is passed as the `this` argument The an
by spion 9y ago
Because I said left to the dot at "call time". More succintly:
> When you call a function, whatever is left of the dot is passed as the `this` argument
The analogy is pretty good - you can't pass an argument to a function until you actually call it. In your example, the function hasn't been called yet, so no argument has been passed.
foo.doStuff // <-- syntax to access a field member
foo.doStuff() // syntax to call a field member with a `this` argument passed left of the dot.
Furthermore, on the receiving side, the setTimeout function is seeing:
function setTimeout(fn, delay) {
// just fn, but not the argument.
}
Which makes it clear that you can no longer call that function with the proper `this` argument.
My point was that `this` is only as confusing as you make it, and there is a simple and straightforward way to teach it (function argument). If you do that, the dozens of seemingly special cases go down to just a couple.
Its unfortunate we don't yet have the bind operator https://github.com/tc39/proposal-bind-operator https://github.com/tc39/proposal-bind-operator
If we had it, the explanation could be made even clearer. You would be able to call free-standing functions with a `this` argument left of the double colon, i.e. `thisArgument::freeStandingFunction()` - then the dot method call syntax `thisArgument.method()` could be explained as a syntax sugar for `thisArgument::(thisArgument.method)()` - a syntax sugar to at the same time access a function attached as a member field of an object and call it with that object as the `this` argument.
The bind operator would make all those other `this` cases seem less special too. Of course you can call various bits of code with whatever `this` argument you want. The event handler code? `someElement::eventHandlerCode(event)`. A constructor? `Object.create(constructor.prototype)::constructor()`. Nothing special here, just a function argument, can have different values in different functions, depending what was passed as `this`.
Its bizarre that this operator got stuck in stage 0 and is being subsumed by the pipeline operator (a terrible match for JavaScript). Looks like TC39 isn't interested in untangling the mess.
- jdmichal 9y agoThe thing is, I don't think anyone is really getting confused about `this` in your simplistic call-site left-of-the-dot model. They get confused when they do exactly the code I put, or similarly in a DOM event handler. And then there's weird cases, like calling bind on a bound function does nothing... If we what to talk about what we wish it looked like, I wish it was more like Rust, where left-of-the-dot is an implicit argument to an explicit parameter within the function.
- spion 9y agoThat example is still explained by the simplistic model. You have a function function setTimeout(callback, delay) { do the set timeout thing, at the right time, call callback(); } You pass a function argument to it, but you're not calling that function, so you haven't passed the `this` argument: setTimeout(someObject.someFn, delay) From within setTimeout, the code gets that function and calls it without anything left of the dot callback(); So the `this` argument gets the default value in that environment (undefined/global/window) Calling bind on a bound function is also explained by the simplistic model: function bind(f, o) { return function(...args) { return o::f(...args) } } Once you partially apply the `this` argument you get a new function that calls the old function with the specified object as the `this` argument; that newly created function has its own implicit `this` argument, but it doesn't use it for anything, so it doesn't matter if you bind it. Prototypical inheritance is also explained by the simplistic model. You call a method: o.f() The method is not on the object, so its looked up in its prototype chain. Lets say its found after the 2nd prototype: [o.__proto__.f, o.__proto__.__proto__.f] Once found its called, but left of the dot is still the original object `o` so that is what is going to become the `this` argument. o::(o.__proto__.__proto__.f)() To me this is simpler than any other late binding explanation I've read, for any language, ever.
- jdmichal 9y agoSo it's simplistic as long as you are capable of programming all these built ins with lower-level primitives... Which is not simple at all from the perspective of a beginner. I would think that once you get that far, `this` is probably not confusing anymore.
- nybble41 9y agoThe confusing part for non-experts is that there are two "callable" types in JavaScript, functions and methods, but the language pretends that these two types are equivalent. Functions are self-contained and can be called without an object; methods depend on an implicit 'this' parameter. The fact that you can define and call a method using the syntax for a normal function call, without generating a type error, is a key part of the problem. The sequence "g = x.f; g()" should either be equivalent to "x.f()" or result in an error. Accepting both forms, but with subtly different behavior regarding implicit parameters, is bound to cause confusion. Note that in C++ there is a type-level distinction between pointers to ordinary functions (without 'this') and pointers to member functions, so the concepts expressed above are not without precedent in "real" languages. > To me this is simpler than any other late binding explanation I've read, for any language, ever. The object system in Lua achieves the same effect with much less complexity, IMHO. Methods in Lua are just ordinary functions which take an object as an explicit first parameter; there is no implicit 'this' argument. Lua objects are tables (associative arrays), with metatables providing a fallback for missing keys much like JavaScript's prototype chain. The dot syntax (x.f) is nothing more than syntactic sugar for the table lookup x['f']. To call methods idiomatically you use the colon operator x:f(...), which is equivalent to x.f(x, ...) except that x is only evaluated once. Like JavaScript, Lua is dynamically typed, with missing arguments defaulting to nil, so you won't get a clean type error if you forget to pass the first parameter. However, there is no hidden, context-sensitive 'this' to worry about, and if you pass an argument fn=x.f to a function which calls it as fn(...) then the arguments will be exactly the same as if you had written x.f(...) directly—most likely not what you intended, but it is at least apparent why it failed.
- carussell 9y ago> You would be able to call free-standing functions with a `this` argument left of the double colon, i.e. `thisArgument::freeStandingFunction()` If you're going to do that, you might as well do it the right way and make it `verb::theThing` instead. Alternatively, `with` is an existing reserved word that already kind of deals in this realm, as language semantics go. So... finishWork().then(placeOrder with deliveryService)
- spion 9y agoThat would muddy the water even further, since `with` normally brings object members into scope. This is specifically about calling a non-member that is already in scope, as if it were a member. Its exactly the same as `freeStandingFunction.call(thisArgument)` It should be a primitive operator, not just a method on functions, because that allows you to explain x.method() without the explanation being self-referential i.e. fn.call(thisArgument) is a method call itself.
- carussell 9y ago> This is specifically about calling a non-member that is already in scope, as if it were a member. I understand what you wrote. `with` is reserved, but disused, however. And it wouldn't be the first reserved word that has different behavior based on the actual construct it appears in. Loops and switch statements both use `break`, for example. The language does this because the semantics are similar enough, and there's no point in polluting the list of reserved words when pulling double-duty with the one would do.
- spion 9y agoIn the case of 'break', it breaks out of the block of the current construct (loop, switch). with brings an object in scope, or calls something already in scope with the object as a this argument - the semantics will be exactly opposite in this case. Then it breaks chaining syntax (one nice thing we get), since everything will have to nest again: x::fn()::otherFn() vs (otherFn() with (fn() with x)) It brings more problems with ASI: fn() with (x); // is this fn() with (x), or fn(); with (x); do nothing