3

I'm currently studying javascript by following the book "you dont know js" series.

In the "this & object prototype" section, when discussing "indirect references to functions", the author states

function foo() {
  console.log( this.a );
}

var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };

o.foo(); // 3
(p.foo = o.foo)(); // 2

The result value of the assignment expression p.foo = o.foo is a reference to just the underlying function object. As such, the effective call-site is just foo(), not p.foo() or o.foo() as you might expect. Per the rules above, the default binding rule applies.

So apparently, (p.foo = o.foo) return a reference to the function foo. But what is the mechanism/rules that allow (p.foo = o.foo) return a reference to the function foo? In other words, why a simple assignment return a reference to foo function?

Thor
  • 9,638
  • 15
  • 62
  • 137
  • 1
    Because an assignment evaluates to the *value* of the right hand side. And not to a property reference, as `o.foo` in the method call `o.foo()` does. – Bergi Jul 11 '17 at 02:28

2 Answers2

2

When I want to understand something like this, I find it helpful to break it down step by step.

  1. o.foo looks at the o object and finds a property named foo. It returns a reference that property, whatever it might be. In this case, the o.foo property is a reference to the function foo.
  2. p.foo = o.foo takes the result from above (a reference to the function foo), creates a property in the p object which is also named foo. So now p.foo is also a reference to the foo function, exactly the same thing as o.foo.
  3. That expression is wrapped in parentheses, so now you have whatever was on the left side of the = sign, or p.foo, which is (as a reminder) still a reference to the foo function.
  4. Now we find the () at the end. This calls whatever function we have on hand at this moment. That is the foo function. Note in particular that we are not calling p.foo(). That would be a method call to the function that p.foo is a reference to, so inside that function, this would be set to p. But we're not doing that. We're just calling whatever function was returned by ( p.foo = o.foo ). As before, this is the same foo function, but we've now lost any connection it may have ever had to the o object or the p object.
  5. So, when we make that call at the end, we are merely calling the foo function without setting this to any particular object. Because of that, when we make the call, this is set to undefined.
  6. But we're not running in strict mode, so JavaScript "helpfully" doesn't want to give us an undefined this, so it sets this to the window object in a browser or the global object in Node.
  7. Previously we did var a = 2;. So the window or global object actually now has a property named a, and the value of that property is 2.
  8. So now when we do the console.log(this.a), we pick up the a property from the window or global object. That value is 2.

What if all this code was not running at the global level, but instead it was inside a function? What would happen then?

function test() {
  function foo() {
    console.log( this.a );
  }
  
  var a = 2;
  var o = { a: 3, foo: foo };
  var p = { a: 4 };

  o.foo(); // 3
  (p.foo = o.foo)(); // was 2, but now is undefined
}

test();

Now when we call console.log( this.a ); inside foo, this still refers to the window or global object. But when we set var a = 2;, we aren't setting a global property any more. We're just creating a local variable. window.a or global.a is undefined (unless some other code previously set it).

Strict mode avoids some this weirdness. If we put a 'use strict'; at the top of the code, it will compile in strict mode. And now when we make that last function call at the end, where we're calling the foo function (again not as a method!), this is now set to undefined instead of window or global. Therefore, when we try to call console.log(this.a), the code fails because this is the same as undefined, and undefined does not (and couldn't) have an a property.

Let's try it:

'use strict';

function foo() {
  console.log( this.a );
}

var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };

o.foo(); // 3
(p.foo = o.foo)(); // was 2 in the original, but now throws an exception

The bottom line, at least for this particular example: always use strict mode! It is your friend.

Michael Geary
  • 28,450
  • 9
  • 65
  • 75
  • 1
    I was just about to post an incomplete answer with basically step 8 from yours missing. Thanks for making sense of the bit I couldn't quite work out. I knew `this` was the `window` but didn't know why. Upvoted :-) – Fred Gandt Jul 11 '17 at 02:28
  • 2
    Actually, in step 5, `this` is specifically passed as `undefined`. Step 8 is irrelevant - it has nothing to do with the kind of call. It's step 6 where the sloppy mode function casts the `this` value to an object, and replaces `null` or `undefined` with the global object. – Bergi Jul 11 '17 at 02:33
  • Thanks! That's what I get for doing a "stream of consciousness" overview. :-) I updated it with your corrections, hopefully got it closer this time! (And for anyone reading the comments, I took out step 8, so 9 is the new 8...) – Michael Geary Jul 11 '17 at 02:44
  • @FredGandt thanks for your answer. Correct me if I'm wrong, but is it correct to assume that '(p.foo = o.foo)();` is the same as 'var q = (p.foo = o.foo)' and then 'q()'? I tried the code in chrome console and they seem to be behaving as I expected. – Thor Jul 11 '17 at 02:50
  • @Bergi do you mind if I ask, how did you got to know such intrinsic details of how javascript work? A bit of wisdom from you would help beginners like me to go a long way. :) – Thor Jul 11 '17 at 02:51
  • 2
    @CaptainAmerica Yes, those are the same. In both cases you've gotten a reference to the `foo` function and then called it directly as a function, not as a method. – Michael Geary Jul 11 '17 at 02:53
  • @MichaelGeary now it makes perfect sense! thanks again for helping out! – Thor Jul 11 '17 at 02:54
  • 1
    @CaptainAmerica Years of StackOverflow experience :-) – Bergi Jul 11 '17 at 02:54
  • 1
    @CaptainAmerica - As Michael has explained, the result of the assignment is whatever the value being assigned is, so yes `q = (foo = bar)` will result in `q` having the value of `foo` and `bar`. It equates to `q = foo = bar`. – Fred Gandt Jul 11 '17 at 02:55
  • 1
    @CaptainAmerica - Just my opinion, but if the book your reading doesn't explain the examples it gives, you might want to consider using it as firelighters instead of reading it. Over the years, I've encountered a lot of "experts" who love to _show off_, but rarely say anything helpful. Consider the real world practicality of that last line in your excerpt code; would you ever really want to do that? – Fred Gandt Jul 11 '17 at 03:02
  • @FredGandt yeah, I definitely agree with parts of your comment. The author tends to go into the nooks and crannies when explaining things, and I agree that the last line of code are unlikely to have much real world implications, but I guess it doesn't hurt to know the details of javascript, just in case it is needed (however unlikely) down the track. – Thor Jul 11 '17 at 03:10
  • 2
    I'll rephrase: would it have not been simpler to understand if the last line(s) was `var q = o.foo; q(); // 2`? This imparts that `o.foo` returns `foo()` and turns `q` into a proxy, without the baffling parenthesized assignment call. If the code is minified production code, it can be as alien as you like as long as it works, but an educator should make every effort to provide simple, clear walkthroughs. The book should also explain the reason for the return of `2` immediately after showing alien code that returns it. Anyhoo; was interesting :-) – Fred Gandt Jul 11 '17 at 03:24
1

The assignment operator = is an expression in JavaScript that produces (returns) the assigned value. Because it is an expression it can be used anywhere an expression is allowed, such as inside parenthesis.

For example:

let test = (a = b = c = { name: 'test' })

The code above would first evaluate the expression in the parenthesis and point the variables c, b, and a to the test object (in that order), then it would point test to the produced value from this expression. After that line is executed, a, b, c, and test will all point to the same object.

Similarly,

(p.foo = o.foo)

Would produce o.foo back (technically it would produce whatever o.foo is pointing to, which is the function foo).

As far as

(p.foo = o.foo)()

By adding the additional () after the parenths, we are telling the engine that we want to invoke whatever the expression (p.foo = o.foo) ends up producing. Thus we end up invoking the function foo. Similar patterns is used in IIFEs.

A helpful rewrite would be to think of the line above as doing this:

let produced = (p.foo = o.foo)
produced()

Further reading about statements vs expressions.

nem035
  • 34,790
  • 6
  • 87
  • 99
  • This does not explain why `(p.foo = o.foo)()` `log`s `2` – Fred Gandt Jul 11 '17 at 02:33
  • @FredGandt but that wasn't the question? Q: "But what is the mechanism/rules that allow (p.foo = o.foo) return a reference to the function foo?" – nem035 Jul 11 '17 at 02:34
  • Fair point (on review); perhaps it should have been though ;-) – Fred Gandt Jul 11 '17 at 02:36
  • -1 for the first two paragraphs that are complete nonsense. One doesn't "declare statements", there are no "statement expressions", and assignments don't evaluate to the left-most variable. – Bergi Jul 11 '17 at 02:37
  • …one cannot wrap statements in parenthesis, and you can try out that `p.foo` is not read back by making it a getter/setter property. – Bergi Jul 11 '17 at 02:40
  • If you introduce a `temp` variable for the righthandside value in your final snippet, it might almost be correct – Bergi Jul 11 '17 at 02:47
  • @nem035 thanks for all the links in the answer! reading them right now :) – Thor Jul 11 '17 at 02:59
  • 1
    @CaptainAmerica no problem mate, glad to help. Happy JavaScripting :) – nem035 Jul 11 '17 at 03:01