Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When you pass 'this' as an argument? [duplicate]

I'm trying to learn about this, and it's confusing me a bit here:

var randomFunction = function(callback) {
    var data = 10;
    callback(data);
};

var obj = {
    initialData:  20,
    sumData: function(data) {
        var sum = this.initialData + data;
        console.log(sum);
    },
    prepareRandomFunction: function() {
        randomFunction(this.sumData.bind(this));
    }
};

obj.prepareRandomFunction();

Is this designed to set itself where it is first rendered in code?

For instance, in my example I'm successfully using it to refer to obj and also binding the function to obj, but since this is being passed as a callback function, what is stopping it from being set as randomFunction (i.e. what's stopping it from literally passing "this.sumData.bind(this)" so that this is set to randomFunction when it gets called from there)?

Updated

I'm not exactly asking how this works generally (I don't think). I'm mainly curious to know why this gets set where I define it as the argument of my randomFunction call, and not where callback gets called within randomFunction.

I could be wrong, but if I were to swap this.sumData.bind(this) with the callback(data) that I currently have I think I would get a different result. Is that because callback is a reference to this.sumData.bind(this) when it was first defined (and where this is obj)?


I think I've learned through this scenario that this is set when it's executed. It's not passed as a argument to be set later when the argument is called down the line.

like image 545
noob-in-need Avatar asked Jan 19 '15 01:01

noob-in-need


2 Answers

this inside a function call gets set according to how a function is called. There are six main ways that this gets set.

  1. Normal Function Call: In a normal function call such as foo(), this is set to either the global object (which is window in a browser or global in nodejs) or to undefined (in JavaScript's strict mode).

  2. Method Call: If a method is called such as obj.foo() where the method foo is a normal method declaration using the function keyword or using the regular method declaration syntax for a class, then this is set to obj inside the function.

  3. .apply() or .call(): If .apply() or .call() is used, then this is set according to what is passed to .apply() or .call(). For example, you could do foo.call(myObj) and cause this to be set to myObj inside of foo() for that particular function call.

  4. Using new: If you call a function with new such as new foo(), then a new object is created and the constructor function foo is called with this set to the newly created object.

  5. Using .bind(): When using .bind() a new stub function is returned from that call that internally uses .apply() to set the this pointer as was passed to .bind(). FYI, this isn't really a different case because .bind() can be implemented with .apply().

  6. Using ES6 Fat Arrow Function Defining a function via the arrow syntax in ES6+ will bind the current lexical value of this to it. So, no matter how the function is called elsewhere (with any of the previous ways to call it), the this value will be set by the interpreter to the value that this has when the function was defined. This is completely different than all other function calls.

There's sort of a seventh method, via a callback function, but it isn't really its own scheme, but rather the function calling the callback uses one of the above schemes and that determines what the value of this will be when the callback is called. You have to consult either the documentation or the code for the calling function or test it yourself to determine what this will be set to in a callback.


What is important to understand in JavaScript is that every single function or method call in JavaScript sets a new value for this. And, which value is set is determined by how the function is called.

So, if you pass a method as a plain callback, that method will not, by default, get called as obj.method() and thus will not have the right value of this set for it. You can use .bind() to work around that issue.

It's also useful to know that some callback functions (such as DOM event handlers) are called with a specific value of this as set by the infrastructure that calls the callback function. Internally, they all use .call() or .apply() so this isn't a new rule, but is something to be aware of. The "contract" for a callback function may include how it sets the value of this. If it does not explicitly set the value of this, then it will be set according to rule #1.

In ES6, calling a function via an arrow function, maintains the current lexical value of this. Here's an example of the array function maintaining the lexical this from MDN:

function Person(){
  this.age = 0;

  setInterval(() => {
    this.age++; // |this| properly refers to the person object
  }, 1000);
}

var p = new Person();

Your example of obj.prepareRandomFunction(); is rule #2 above so this will be set to obj.

Your example of randomFunction(this.sumData.bind(this)) is rule #1 above so this inside of randomFunction will be set to the global object or undefined (if in strict mode).

Since randomFunction is calling a callback function which itself used .bind(), then the value of this inside the callback function when it is called will be set to the value of this that was passed to .bind() in this.sumData.bind(this) as via rule #5 above. .bind() actually creates a new function who's job it is to call the original function AFTER setting a custom value of this.


Here are a couple other references on the topic:

How to avoid "this" refering to the DOM element, and refer to the object

A better understanding of this

How does the "this" keyword work?


Note, that with the use of .apply() or .call() or .bind(), you can create all sorts of somewhat odd things and sometimes quite useful things that could never be done in something like C++. You can take any function or method in the world and call it as if it were a method of some other object.

For example, this is often used to make a copy of the items in the arguments object into an array:

var args = Array.prototype.slice.call(arguments, 0);

or similarly:

var args = [].slice.call(arguments, 0);

This takes the array's .slice() method and calls it, but supplies it with an arguments object as the this pointer. The arguments object (though not an actual array), has just enough array-like functionality that the .slice() method can operate on it and it ends up making a copy of the arguments items into an actual array which can then be operated on directly with real array operations. This type of chicanery can't be done willy-nilly. If the array .slice() method relied on other array methods that are not present on the arguments object, then this trick would not work, but since it only relies on [] and .length, both of which the arguments object has, it does actually work.

So, this trick can be used to "borrow" methods from any object and apply them to another object as long as the object you are applying them to supports whatever methods or properties that the method actually uses. This can't be done in C++ because methods and properties are "hard bound" at compile time (even virtual methods in C++ are bound to a specific v-table location established at compile time), but can be easily done in JavaScript because properties and methods are looked up live at runtime via their actual name so any object that contains the right properties and methods will work with any method that operates on those.

like image 87
jfriend00 Avatar answered Oct 31 '22 08:10

jfriend00


Step by step.

  1. this inside prepareRandomFunction is obj

     obj.prepareRandomFunction()
    
  2. randomFunction takes a function:

     randomFunction(this.sumData);
    
  3. That function gets called:

     callback(data);
    

    Notice callback is called without a dot, which means it has no value for this, which means this is the global object (or undefined in strict mode).

  4. sumData gets called:

     var sum = this.initialData + data;
    

    this is the global object, initialData doesn't exist, you add undefined to data. Unexpected results.

    Solution: bind this permanently:

     randomFunction(this.sumData.bind(this));
    
  5. sumData runs, this is obj, obj.initialData is 20. It works.

like image 40
elclanrs Avatar answered Oct 31 '22 09:10

elclanrs