Understanding JavaScript function calls and this

Keywords: Javascript ECMAScript

Links to the original text: Original link

Over the years, I've seen a lot of confusion about JavaScript function calls. In particular, many people complain about the confusing semantics of this in function calls.

In my opinion, many of these confusions can be eliminated by understanding the core function call statement and then looking at all other ways of invoking functions with sugar as an example on top of that primitive.

Core primitive function

First, let's look at the core function call primitive, the call function call method. Calling methods is relatively simple.

  1. Create a parameter list (argList) from parameter 1 to the end
  2. The first parameter is thisValue
  3. Set this function to this thisValue and call it with argList as its parameter list.

For example:

function hello(thing){
    console.log(this + " says hello " + thing)
}
hello.call("Mandy", "world") // Mandy says hello world

We call the hello method, and he
Its this is set to Mandy with a parameter world. This is the core primitive of JavaScript function calls. You can think of all other function calls as substitutes for the core primitive ("substitution" is a convenient grammar and is described in a more basic core primitive).

Simple function call

Obviously, calling functions all the time with call can be annoying. JavaScript allows us to call functions directly using the grammar (hello("world"). When we do this, the call will be replaced.

function hello(thing){
    console.log("Hello" + thing)
}

// this: 
hello("world")

// desugars to:
hello.call(window, "world");

When using strict mode, it is equivalent to

// this: 
hello("world")

// desugars to:
hello.call(undefined, "world");

function call
fn(...args) is equivalent to
fn.call(window[ES5-strict:undefined], ...args).

Note that the same is true for inline declared functions:
(function() {} () is equivalent to
(function() {}).call(window [ES5-strict: undefined).

Member method

Another very common way of calling method calls is to call a method as a member of an object (person.hello()).

var person = {
  name: "Brendan Eich",
  hello: function(thing) {
    console.log(this + " says hello " + thing);
  }
}

// this:
person.hello("world")
// Amount to
// desugars to this:
person.hello.call(person, "world");

Note that it does not matter how the Hello method is attached to an object in this form. Remember, we previously defined hello as a stand-alone function. Let's see what happens if we dynamically attach Hello methods to objects:

function hello(thing) {
  console.log(this + " says hello " + thing);
}

person = { name: "Brendan Eich" }
person.hello = hello;

person.hello("world") // still desugars to person.hello.call(person, "world")
// [object Object] says hello world

hello("world") // "[object DOMWindow]world"
// [object Window] says hello world

Note that this function does not have its "this" persistence concept. It is always set when invoked according to the way the caller invokes it.

Using Function.prototype.bind

Because it is sometimes convenient to use persistent this value to refer to functions, people have been using simple closure techniques to transform functions into functions without changing this:

var person = {
  name: "Brendan Eich",
  hello: function(thing) {
    console.log(this.name + " says hello " + thing);
  }
}

var boundHello = function(thing) { 
  return person.hello.call(person, thing); 
}

boundHello("world"); // Brendan Eich says hello world

Even though our boundHello("world") call is still equivalent to boundHello.call(window, "world"), we turn around and use our original call method to change this value back to the value we want.

We can make this technique universal through some adjustments:

var bind = function(func, thisValue) {
  return function() {
    return func.apply(thisValue, arguments);
  }
}

var boundHello = bind(person.hello, person);
boundHello("world") // "Brendan Eich says hello world"

To understand this, you need only two other pieces of information. First, arguments are an Array-like object that represents all the parameters passed to the function. Second, the application method works exactly the same way as call, except that it uses an Array-like object instead of listing a parameter at a time.

Our bind method returns a new function. When it is called, our new function simply calls the incoming original function and sets the original value to this. It also passes parameters.

Because this is a somewhat common idiom, ES5 introduces a new bind method on all Function objects that implement this behavior:

var boundHello = person.hello.bind(person);
boundHello("world") // "Brendan Eich says hello world"

This is most useful when you need a function to pass as a callback:

var person = {
  name: "Alex Russell",
  hello: function() { console.log(this.name + " says hello world"); }
}

$("#some-div").click(person.hello.bind(person));

// when the div is clicked, "Alex Russell says hello world" is printed

Of course, this is a bit clumsy, and TC39 (the committee responsible for the next version of ECMAScript) continues to work on developing a more elegant, still backward-compatible solution.

Posted by morphius on Sat, 12 Oct 2019 01:24:36 -0700