0

I have two cases and I am confused with the output I am getting.

Case 1 :

let x = {
  b: 5,
  y: function a() {
    return function() {
      return () => {
        console.log(this);
      }
    }
  }
}
x.y()()();

On running x.y()()() I am getting Window object as output but according to arrow function's definition, the output should have been its parent function.

Case 2 :

let x = {
  b: 5,
  y: function a() {
    return () => {
      console.log(this);
    }
  }
}
x.y()();

If I remove one level of nesting which is a basic example then on running x.y()(), I am getting object x as output.

Can you please explain why exactly am I getting these output?

Alessio Cantarella
  • 5,077
  • 3
  • 27
  • 34
Madhur Bansal
  • 726
  • 2
  • 8
  • 14
  • Arrow functions have no this. Inside an arrow function the this keyword is the this of the upper scope. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions – Kai Lehmann May 21 '20 at 18:13
  • Probably because a() in Case 1 is not an arrow function. – AlexS May 21 '20 at 18:14

4 Answers4

6

The this inside a function is determined by its calling context, and by whether the function is an arrow function or not.

With an arrow function, this is always inherited from the outer scope.

If the function being called is a full function and that function is being called as a property as an object (eg obj.foo()), then the calling context, the this inside the foo function, is obj.

If the function being called is a full function but is standalone - that is, not a property of an object - then there is no calling context, and this inside the function will be undefined or the global object.

With

let x = {
  b: 5,
  y: function a() {
    return function() {
      return () => {
        console.log(this === window);
      }
    }
  }
}
x.y()()();

The this is what the returned function's this is - that is, the calling context for the function invoked here:

x.y()()

The function being invoked is not part of an object - it's a standalone function expression which was created from the invocation of a prior function on an object, equivalent to:

(x.y())()

So, there is no calling context, and this is the global object.

In contrast, in case 2, the closest ancestor function being called is the a function on the y property of the object, and this function is called with a calling context:

x.y()()
^^^^^

Above, y is called with a calling context of x, so this is x inside the a function.

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • Thanks for the explaination. I have one more doubt, can you please clarify for this case also - I am getting Window object in this case. let x = { b: 5, y: () => { console.log(this); return function () { console.log(this); } } } x.y()() – Madhur Bansal May 21 '20 at 18:45
  • The first `this` is in an arrow function, inherited from the top level. The second `this` is in the returned function which, when called, is not called on an object. – CertainPerformance May 21 '20 at 19:20
  • For first 'this' why is it not considering context of object 'x' as it is an arrow function and x is its parent? – Madhur Bansal May 21 '20 at 19:24
  • Inside an arrow function, `this` refers to the `this` of the outer scope. The outer scope there is the top level, and `this` on the top level is the global object or `undefined`. – CertainPerformance May 21 '20 at 19:28
2

The first function call is

x.y()

In that function call, this will refer to x. That function call returns a function, which is used in the second function call:

x.y()()

That function call has no explicit this value, so this is window. That function call returns another function, the innermost arrow function, which upon its definition inherits window as this from its closure. Thus the third function call:

x.y()()()

also has window as the value of this.

Pointy
  • 405,095
  • 59
  • 585
  • 614
0

Your question is similar to this question:-Javascript "this" pointer within nested function

Calling a function inside a parent object that is - in case of x.y(), it has a leading parent x, that's why it returned x - in case of x.y()() & x.y()()(), there the insider function is called which don't have a leading parent object and hence that will return window.

VR7
  • 112
  • 3
  • 16
0

Here's What's Going On...

First, let's modify your code slightly -- but before we even do that, let's talk about what even influences this: "CallSite".

CallSite

Where you call a function from will determine its this value and there is an order of precedent which goes alongside such determination. Basically, there are 4 types of CallSite, however, there 1 or 2 caveats which arguably makes this 5 or 6 types.

The 4 Types of CallSite

  • Global
  • Implicit
  • Explicit
  • New
Global

This is fairly obvious & straightforward... Call a function detached from any object and this will be the Global Scope:

function run() {
  this === window === true;
}
Implicit

Also fairly straightforward. Call any function as a method of an object and this will equal that object's instance.

function run() { return this; }
run();  // Global
({ id: 'test', run: run }).run();  // > { id: 'test', run: f() }
Explicit (See Caveat Below)

Straightforward. Calling a function using Function.prototype.call or Function.prototype.apply will force this to equal the first argument of the prototype method. However, there is one caveat to this rule -- stay tuned.

(function run() { return this; }).call(new Klass());  // this instanceof Klass === true
New

As straightforward as it gets. Call a function and prefix the invocation with new and this will equal a new instance of the implied class. However, this does have one exception.

Caveats to CallSite Precedence

Explicit

Function.prototype.call & Function.prototype.apply cannot force a function's context if the function is the result of calling Function.prototype.bind. Moreover, it is noteworthy to mention that recalling Function.prototype.bind on an already bound function will not re-bind to another context; its this value will remain the same as that of the original bind call.

function run() { return this; }
let o = { };
let x = { };
let r = run.bind(o);
let result = r.call(x);  // result === x === false && result === o === true
New

When calling a function that is prefixed with new, you will not get an object-instance of the implied class if the function returns a different value.

class Class {}
var Klass = function Klass() {
    return new Class;
};
var klass = new Klass();  // klass instanceof Class === true

Your Code (Modified)

In your original code, you get back the value of x because Arrow-Functions always bleed the outermost scope into their own. That is, an arrow-function will always take its parent-scope's this context. This behavior is no different returning the result of calling Function.prototype.bind on a function and providing this as its context.

var x = {
  b: 5,
  y: function a() {
    console.log('a', this);
    return function b() {
      console.log('b', this);  // window
    }
    return () => {
      console.log('=>', this);  // x
    }
  }
}
x.y()();

When we call x.y() we are returning whatever that function returns. I have modified your code to present two return scenarios:

  • Returning function b() {...}: x.y() is a detached function. Therefore, its CallSite will be Global
  • Returning () => {...}: x.y() is a detached Arrow-Function. Its behavior will be as if you invoked bind on a normative function and passed in the [parent] function's this context as the first argument.

Closing

I hope this makes a lot more sense after further understanding CallSite. Knowing this, you realize it is possible to actually use new as an invocation tactic. That is, try to figure out how the following code could run successfully:

Exercise 1 (The "Crockford Dog Balls Problem")

What behavior do the following 2 lines of code exhibit?

var a = new (function Klass() {})();  // a
var b = new (function Klass() {}());  // b

Exercise 2 ("What's New?")

Write a function that remediates the following code to run successfully.

var instance = new new new new Klass();

Hope this helps!

Cody
  • 9,785
  • 4
  • 61
  • 46