The ‘this’ Keyword in JavaScript

· 11 min read Đọc bài này bằng tiếng Việt

JavaScript (JS) is a fairly flexible and interesting programming language. But that flexibility also brings plenty of confusion, especially for newcomers. People new to JS often assume it works like other languages such as Java or C#. But many things in JS are quite different from those languages, causing misunderstandings. One of the most confusing points is the this keyword, because in JS it’s not simply a reference to the current object as in other OOP languages. Let’s explore this in detail.

// app.js
// Fun question: guess the output of (1) and (2)
var obj = {
    mMethod: function() {
        console.log(this)
    }
}

obj.mMethod(); // (1)

var _mMethod = obj.mMethod;
_mMethod();  // (2)

By now, some of you familiar with this might chuckle at me calling it a “variable.” And you’d be right — this in JS is a keyword, not a variable. You can’t directly assign a value to this, nor can you delete it. So what makes this keyword so tricky?

The essence of the this keyword

JavaScript code executes in a specific execution context. These contexts are arranged to execute the program sequentially. Imagine each context contains a certain set of code, and the whole program arranges these contexts in a stack. Contexts are then popped and executed one by one until completion — the context at the top of the stack contains the code ready to run.

Each execution context has a corresponding ThisBinding with a constant value representing that execution context. And the keyword this equals the ThisBinding value in the currently executing context. Thus, this represents the current execution context and must be re-evaluated when the execution context changes.

There are 3 types of execution context: global, eval, and function. Global is the top-level context of the entire program, containing code not inside any function or called by eval — it’s the default execution context. Eval is the context containing code called by the eval function. Function is code inside a function. We’ll look at each in detail below.

Execution contexts

Global

This is the execution context at the top of the context stack — the first context when the program starts. For example, in client-side code on a web page, the global context starts right after the <script> tag. In the global context, ThisBinding is set to the Global Object. In Node.js, this is the Node.js global object (initially an empty object); in browsers, it’s the window object. However, in strict mode, the global object is undefined.

console.log(this); // global object in global context

this.mX = 'I love JavaScript'; // using the global object

console.log(this.mX); // prints the value of property mX

var obj = {
  mMethod: function () {
    console.log(this); // prints current this
  },
};

obj.mMethod(); // no longer the global object

Eval

With eval, we have two cases:

Direct eval call

Calling eval directly means calling it as shown below. In this case, ThisBinding is set to the enclosing context of that code.

function callMe() {
  console.log(this);
}

var obj = {
  callMe: function () {
    console.log(this);
  },
};

eval('callMe()'); // global object

eval('obj.callMe()'); // obj object
Indirect eval call

Indirect eval means calling eval through a variable — e.g., passing eval as a function parameter or assigning it to a variable. In this case, ThisBinding is set to the global object.

this.callMe = function () {
  console.log('callMe in Global Object');
};

var obj = {
  callMe: function () {
    console.log(this);
  },
  _callMe: function (_eval) {
    _eval('console.log(this)');
    _eval('callMe()');
  },
};

obj._callMe(eval); // indirect eval via function parameter

var mEval = eval; // assign eval to a variable

mEval('console.log(this)'); // indirect eval via variable mEval

Function

When a function is called, its execution context depends on the input parameters and the calling context. Suppose our function is F, with arguments and calling context corresponding to thisValue. The thisBinding is determined as follows:

  1. If the function is in strict mode, ThisBinding is set to thisValue.
  2. Else if thisValue is null or undefined, ThisBinding is set to the global object.
  3. Else if Type(thisValue) is not Object, ThisBinding is set to ToObject(thisValue).
  4. Else ThisBinding is set to thisValue

See more about thisBinding assignment here.

Let’s look at some specific function call scenarios:

Calling through the global context

Here, this references the global object.

function mMethod() {
  console.log(this); // global object
}

mMethod();

var obj = {
  myMethod: function () {
    return (function () {
      console.log(this); // global object
    })();
  },
};

obj.myMethod();
Calling through an object

Here, this references the thisValue object — the object containing the function.

var obj = {
  mMethod: function () {
    console.log(this);
  },
  oMethod: function () {
    console.log('▼ oMethod');
    console.log(this);
    console.log('▲  oMethod');
  },
};

obj.mMethod(); // this corresponds to obj
obj['oMethod'](); // this corresponds to obj

// Assign mMethod to another object
var obj1 = {
  mVal: "I'm obj1",
};
obj1.mMethod = obj.mMethod;

obj1.mMethod(); // this corresponds to obj1

// Calling via object construction with new
function MyObject(val) {
  this.mVal = val || 'I xxx JS';

  this.mMethod = function () {
    console.log(this);
  };
}

var mObj1 = new MyObject();
var mObj2 = new MyObject("I'm object 2");

mObj1.mMethod(); // this corresponds to mObj1
mObj2.mMethod(); // this corresponds to mObj2
Calling through special functions

JavaScript provides built-in functions that let us control this via an input object:

  • Function.prototype.apply(thisArg, argArray)
  • Function.prototype.call(thisArg[, arg1[, arg2, …]])
  • Function.prototype.bind(thisArg[, arg1[, arg2, …]])
  • Array.prototype.every(callback[, thisArg])
  • Array.prototype.some(callback[, thisArg])
  • Array.prototype.forEach(callback[, thisArg])
  • Array.prototype.map(callback[, thisArg])
  • Array.prototype.filter(callback[, thisArg])

Using these functions, this becomes the value of the thisArg object. This is very handy for actively changing thisBinding:

var obj = {
  mMethod: function (firstName, lastName) {
    var firstName = firstName || '';
    var lastName = lastName || 'Danh';
    console.log('Hello ' + firstName + ' ' + lastName);
    console.log(this);
  },
};

var obj1 = {
  mVal: "I'm obj1",
};

obj.mMethod.apply(obj1); // obj1 object

obj.mMethod.apply(obj1, ['Chí', 'Phèo']); // obj1 object

obj.mMethod.call(obj1, 'Thị', 'Nở'); // obj1 object

The code above prints this as the obj1 object, not obj, because call and apply directly pass this via the first parameter.

Common confusion cases

Calling through a different context

Consider this case:

var obj = {
  mVal: 'Vietnam',

  mMethod: function () {
    console.log('Hello ' + this.mVal);
  },
};

var oMethod = obj.mMethod; // oMethod is in the global context

oMethod();

When this code runs, it prints Hello undefined because we’ve pushed this to the global object. How do we get the correct result Hello Vietnam? We use bind to force this to be the obj object:

var obj = {
  mVal: 'Vietnam',

  mMethod: function () {
    console.log('Hello ' + this.mVal);
  },
};

var oMethod = obj.mMethod.bind(obj); // this in oMethod is forced to obj

oMethod();

Why bind and not call or apply? Because bind retains the obj value for multiple calls, while call and apply only apply for a single invocation. Read more in the call, apply, and bind article.

Callbacks

Calling through a callback is essentially calling through a different context, since the callback executes in a different context:

var obj = {
  mVal: 'Vietnam',

  mMethod: function () {
    console.log('Hello ' + this.mVal);
  },
};

var obj1 = {
  oMethod: function (callback) {
    return callback();
  },
};

obj1.oMethod(obj.mMethod);

Because it’s called in obj1’s context, mMethod now takes this as obj1, not obj. Again, use bind:

obj1.oMethod(obj.mMethod.bind(obj));

Nested functions

Let’s examine ambiguity with nested functions:

var obj = {
  mVal: 'Vietnam',

  oVal: {
    oMethod: function (callMe) {
      callMe();
    },
  },

  mMethod: function () {
    this.oVal.oMethod(function () {
      console.log('Hello ' + this.mVal);
    });
  },
};

obj.mMethod(); // prints: Hello undefined

We expected it to print mVal from obj, but it prints Hello undefined. The reason: the execution context at console.log is now oVal. To access mVal from obj, we need to capture the execution context via an intermediate variable:

var obj = {
  mVal: 'Vietnam',

  oVal: {
    oMethod: function (callMe) {
      callMe();
    },
  },

  mMethod: function () {
    var _this = this; // remember obj's execution context

    this.oVal.oMethod(function () {
      console.log('Hello ' + _this.mVal); // reference obj's context
    });
  },
};

obj.mMethod();

Conclusion

The this keyword is a bit tricky, so when programming, we need to pay attention to the execution context to use it correctly and effectively, based on the calling context and execution context type. Be especially careful with callbacks and nested functions. We can also actively change the execution context using call, apply, or bind as described above.

For more on using Call, Apply, and Bind, read this article.

Additionally, you can refer to the ECMAScript 5.1 standard.

Comments

  1. Loading comments…