/ JavaScript Series

JavaScript Series: Context in JavaScript Functions

Have you ever noticed this keyword in JavaScript and wondered how it works? Especially in a context of a function - where does its value come from? How can one find it useful?

Calling a simple function

First, let’s bring back our favourite fullName function:


  /**
   * Sample Usage:
   *
   *   fullName({ firstName: 'Jason', lastName: 'Bourne' });
   *   // => 'Jason Bourne'
   *
   */
  function fullName(person) {
    return person.firstName + ' ' + person.lastName;
  }
 

We’ve learned how to execute this function with some parameters already:


  fullName({ firstName: 'Jason', lastName: 'Bourne' });
  // => 'Jason Bourne'

Not scary, right? Is there any other way to call functions? Turns out there is!

Functions can operate with context

Before we move to the more advanced parts, let’s learn how we could get the same results back by calling this function differently:


  fullName.call(null, { firstName: 'Jason', lastName: 'Bourne' });
  // => 'Jason Bourne'


  fullName.apply(null, [{ firstName: 'Jason', lastName: 'Bourne' }]);
  // => 'Jason Bourne'


  fullName.bind(null)({ firstName: 'Jason', lastName: 'Bourne' });
  // => 'Jason Bourne'

Three different ways and same results. Why bother?

Turns out functions can operate with context. We refer to the context via this keyword. In our example, we intentionally ignored the context parameter and used null.

By context, we usually mean that a function is part of something bigger. In such cases, this refers to that bigger thing. Let’s bring an example:


  var person = {
    firstName: 'Jason',
    lastName: 'Bourne',
    fullName: function () {
      return this.firstName + ' ' + this.lastName;
    }
  };

What you see above is a simple object with personal data. You might have noticed already we didn’t use any parameters in the fullName function and I can assure you it still works!


  person.fullName();
  // => 'Jason Bourne' 

How come? Our function knows exactly it is part of the object and easily recognised this refers to the whole person.

Be careful!

While this example works perfectly fine, if we wanted to borrow that function and use it somewhere else, we might lose the context! See:


  // What if I wanted to save a reference to the function
  // and refer it by the other name somewhere else?
  var borrowedFullName = person.fullName;


  borrowedFullName();
  // => 'undefined undefined'
  

Ooops! Something bad happened. Turns out your function is smart enough and looks for something it could attach to. It totally forgot about the person and tried to check what the current context has to offer. Since your global object, usually* window, does not have fullName nor lastName assigned, fullName returned undefined undefined.

Make sure you don’t harm yourself

First of all, functions operating on context should be used carefully. It is a good practice to trigger strict mode in your functions to make sure you don’t accidentally change global variables. here’s how you do it:


  var strictPerson = {
    firstName: 'Jason',
    lastName: 'Bourne',
    fullName: function () {
      'use strict';
      return this.firstName + ' ' + this.lastName;
    }
  };

Besides the variable name, you may notice ‘use strict’ string in your function. This is all it takes to trigger strict mode in your browser. Now the browser will complain when executing borrowed function:

  
  // Borrowed function with strict mode
  var borrowedStrictFullName = strictPerson.fullName;

 
  borrowedStrictFullName();
  // =>
  // TypeError: Cannot read property 'firstName' of undefined
  // at fullName (...)

I can assure you this is good! You’re not going to hurt yourself by accidentally modifying global variables.

Force a context when calling a function

Good, we learned how to make our code a little bit safer. Now it’s time to move on to contexts! Remember how borrowedStrictFullName call threw an error in strict mode? Right, strict mode prevented us from modifying global variables. Does it mean this function is useless? No, it doesn’t! We can use it. Let’s call it with some context!


  borrowedStrictFullName.call({
    firstName: 'Marie',
    lastName: 'Kreutz'
  });
  // => 'Marie Kreutz'

It worked! Our parameter-less function worked with a context we enforced! Are there any alternatives?

  
  borrowedStrictFullName.apply({
    firstName: 'Marie',
    lastName: 'Kreutz'
  });
  // => 'Marie Kreutz'

"I’m confused" you may say. Same results, what’s the point of having two functions that do the very same thing?

The difference is subtle and it does not necessarily relate to context binding. It is the way these functions treat parameters! I’ll give you an example:


  /**
   * Function that adds two numbers:
   * 
   *   add(1, 2); // => 3
   *   add(5, 1); // => 6
   * 
   * No context involved
   */
  function add(a, b) {
    return a + b;
  }

Below you can find the difference between the way call and apply pass parameters to the function:

  
  add(1, 2);
  // => 3
  
  add.call(null, 1, 2);
  // => 3

  add.apply(null, [1, 2]);
  // => 3

As you can see, call takes an undefined number of parameters and invokes the function as you would do it manually. It just takes one extra context parameter that wasn’t really useful in our case.

The difference is that apply takes only two parameters. The second one is an array of parameters that you would normally pass to the function one by one.

"Why does it matter?" you may wonder.

To put it simply, you’re going to use call mostly to invoke a function with a context when you know exactly how many parameters the target function expects and you’ve got these parameters ready to be used.

On the other hand, apply comes in handy when your function accepts any number of parameters or the number of parameters you pass to the function may vary. Here’s a real world use case:


  /**
   * Math.max takes any number of integers
   * and returns the biggest one
   * 
   * You cannot pass an array of integers to it though!
   * It would return a NaN!
   */
  Math.max(1,2,3,5,4); 
  // => 5

  Math.max([1,2,3,5,4]);
  // => NaN

  Math.max.apply(null, [1,2,3,5,4]);
  // => 5
  //
  // It worked!
  // Apply took our array and applied
  // the numbers individually to Math.max for us
  // as if we applied them manually:
  //
  //         [1,2,3,5,4]
  // Math.max(1,2,3,5,4)

Or just assign a context

When does bind come in handy then?

Bind allows us to keep a function as a function. It does not invoke anything. It just takes care of binding either context or parameters or both. It is useful sometimes.

We could use bind to make sure functions we borrowed keep the original context:


  // Keep the context of `person` we borrowed function from 
  var borrowedStrictFullNameWithContext =
      person.fullName.bind(person);


  borrowedStrictFullNameWithContext();
  // => 'Jason Bourne'

See? We just picked a single method from an object and made sure it remembers where it came from by binding a context to it!

Wrapping up

To wrap everything up, here’s how you use call, bind and apply:


  someFunction.call(context, arg1, arg2, ..., argN);
  // => invokes function and returns results

  someFunction.apply(context, [arg1, arg2, ..., argN]);
  // => invokes function and returns results
                               
  someFunction.bind(context, arg1, arg2, ..., argN);
  // => returns a function with context / parameters bound

Further learnings

What’s next? Bind operator can do much more than setting the context. It can produce functions with [parameters bound]https://devdocs.io/javascript/global_objects/function/bind) as well. I’ll leave more advanced exercises with bind as a homework for you.

Good luck experimenting!

* global object - when writing JavaScript in your browser, the context referred by this operator is window. On the other hand, if you’re writing code in Node.js environment, your global object refers to global. What’s more, in strict mode, this refers to undefined if no context is bound. Make sure your functions using this operator always have context bound in strict mode. Otherwise your code might throw type errors, e.g. “TypeError: Cannot read property 'firstName' of undefined".