Search code examples
javascriptfunctionscopethisbind

How do I know when to use .bind() on a function in JS?


(I'm aware of this question, but the answers don't quite tell me what I need to know.)

I've come across cases where I need to use .bind() on a function in JavaScript in order to pass this or local/class variables to a function. However, I still don't really know when it's needed.

What is the criteria for knowing when this or local/class variables will or will not be available in a function, exactly? How do you reason about this?

For instance:

  • When a new anonymous function() { } is created, wrapped, or passed around?
  • When using a class member function, a class getter/setter function, or an oldschool prototype.function "member function" (of a function acting as a class)?
  • In the global scope?
  • In a for or forEach loop, or any of their variants?
  • In a closure outer or inner function?
  • In various JS operations like Array.prototype.forEach.call() or [].forEach.call()?
  • In various JS libraries and scripts, which may have their own custom implementation of things?

The main reason I ask is to be aware of potential pitfalls and to avoid having to rely on trial and error.


Solution

  • Credit to T.J. Crowder and Zapparatus for their answers, which provided helpful info. Also helpful were these 4 answers/articles: 1 2 3 4

    However, these were either not entirely complete and/or very long-winded. So I have decided to combine all of my findings into one answer, along with code examples.


    There are several considerations to factor in when determining whether this or local/class variables will be available in a function:

    • The function's containing scope
    • The immediate predecessor in the call chain
    • Whether the function is called directly or indirectly

    Note: there is also strict mode (which yields undefined's rather than window objects) and arrow functions (which do not change this from the containing scope).


    Here are the explicit rules:

    • By default, this is the global object, which in browser world is window.
    • Inside a function in the global scope, this will still be window, it does not change.
    • Inside a member function within a class or function-class (new function() { }), inside a function-class's prototype (funcClass.prototype.func = function() { }), inside a function called by a neighboring member function with this, or inside a function mapped in an object ({ key: function() { } }) or stored in an array ([ function() { } ]), if the function is called directly with the class/object/array as the immediate predecessor in the call chain (class.func(), this.func(), obj.func(), or arr[0]()), this refers to the class/object/array instance.
    • Inside any closure's inner function (any function within a function), inside a returned function, inside a function called with a plain variable reference as its immediate predecessor in the call chain (regardless of where it actually lives!), or inside a function called indirectly (i.e. passed to a function (e.g. .forEach(function() { })) or set to handle an event), this reverts back to window or to whatever the caller may have bound it to (e.g. events may bind it to the triggering object instance).
      • There is... however... one exception...: if inside a class's member function (and only a class, not a function-class), if a function within that loses its this context (e.g. by being a closure's inner function), it becomes undefined, rather than window...

    Here is a JSFiddle with a bunch of code examples:

    outputBox = document.getElementById("outputBox");
    
    function print(printMe = "") {
    	outputBox.innerHTML += printMe;
    }
    
    function printLine(printMe = "") {
    	outputBox.innerHTML += printMe + "<br/>";
    }
    
    var someVar = "someVar";
    
    function func(who) {
    	printLine("Outer func (" + who + "): " + this);
      var self = this;
      (function() {
      	printLine("Inner func (" + who + "): " + this);
        printLine("Inner func (" + who + ") self: " + self);
      })();
    }
    func("global");
    printLine();
    func.call(someVar, "someVar");
    
    printLine();
    
    function funcTwo(who) {
    	printLine("Outer funcTwo (" + who + "): " + this);
      var self = this;
      return function funcThree() {
      	printLine("Inner funcThree (" + who + "): " + this);
        printLine("Inner funcThree (" + who + ") self: " + self);
      };
    }
    funcTwo("global")();
    printLine();
    f = funcTwo("global f");
    f();
    printLine();
    funcTwo.call(someVar, "someVar")();
    
    printLine();
    
    object = {
      func: function(who) {
        printLine("Object outer (" + who + "): " + this);
        var self = this;
        (function() {
          printLine("Object inner (" + who + "): " + this);
          printLine("Object inner (" + who + ") self: " + self);
        })();
      }
    }
    object.func("good");
    printLine();
    bad = object.func;
    bad("bad");
    
    printLine();
    
    function funcClass(who) {
    	printLine("funcClass (" + who + "): " + this);
    }
    funcClass.prototype.func = function() {
    	printLine("funcClass.prototype.func: " + this);
      self = this;
      (function() {
        printLine("funcClass.func inner: " + this);
        printLine("funcClass.func inner self: " + self);
      })();
    }
    fc = funcClass("bad");
    printLine();
    fc = new funcClass("good");
    fc.func("good");
    
    printLine();
    
    class classClass {
    	constructor() {
      	printLine("classClass constructor: " + this);
      }
      func() {
      	printLine("classClass.func: " + this);
        self = this;
        (function() {
        	printLine("classClass.func inner: " + this);
          printLine("classClass.func inner self: " + self);
        })();
      }
      funcTwo() {
      	this.func();
      }
    }
    cc = new classClass();
    cc.func();
    printLine();
    printLine("Calling funcTwo:");
    cc.funcTwo();
    
    printLine();
    
    [0].forEach(function(e) {
    	printLine("[0].forEach: " + this);
      printLine("[0].forEach someVar: " + someVar);
    });
    [0].forEach(function(e) {
    	printLine("[0].forEach with [0]: " + this);
    }, [0]);
    
    printLine();
    
    arr = [
    	function(who) {
      	printLine("Array (" + who + "): " + this);
      },
      1,
      10,
      100
    ];
    arr[0]("good");
    arrFunc = arr[0];
    arrFunc("bad");
    
    printLine();
    
    var button = document.getElementById("button");
    button.onclick = function() {
    	printLine("button: " + this);
    }
    button.click();
    button.onclick = func;
    button.click();
    
    setTimeout(function() {
    	printLine();
    	printLine("setTimeout: " + this);
      printLine("setTimeout someVar: " + someVar);
    }, 0);
    
    setTimeout(fc.func, 0);
    setTimeout(cc.func, 0);
    <input id="button" type="button" value="button"/>
    <br/><br/>
    <div id="outputBox" />


    Conclusion: So yeah that's pretty simple.