1. Mở đầu

Trước khi học hiểu về this bạn nên học trước về scope và closure

Trong js, từ khóa this là thứ rất hay nhưng cũng là thứ gây ra bao rắc rối cho nhiều người, nhất là đối với những người đi từ ngôn ngữ lập trình khác sang js.

Lý do lớn nhất khiến this gây hiểu nhầm cho bao nhiêu người chính là vì ý nghĩa từ điển của chính từ this 

Khi bạn bắt gặp từ this trong lập trình, gần như chắc chắn bạn sẽ nghĩ tới nó chính là tham chiếu tới instance hiện tại hoặc nội hàm hiện tại, và đó cũng là lý do khiến nhiều người hiểu nhầm từ this trong js.

Có 2 chú ý khi bạn bắt gặp this mà cần phải nhớ đó là:

  • this chính là bối cảnh(context) của nơi mà hàm chứa từ this được gọi. Bạn hãy nhớ từ this tham chiếu tới cái vùng không gian mà hàm chứa từ this được gọi.
  • Chỉ có 2 loại context đối với this là object chứa method được gọi hoặc global , ngoài ra không có loại khác.
  • Khi gặp từ this , chỉ quan tâm tới cái nơi gọi hàm chứa nó chứ không được dịch this là nội hàm hiện tại.
Chính vì thế, nếu bạn gặp this , đừng có dịch nó là cái này mà hãy dịch nó thành bối cảnh hay nơi gọi tao (context)

2. this không phải là tham chiếu tới chính function.

function foo(num) {
  console.log("foo: " + num);
  //keep track of how many times `foo` is called
  this.count++;
}

foo.count = 0;
var i;
for (i=0; i<5; i++) {
  foo(i);
}

console.log(foo.count);

Nếu dịch this là nội hàm thì nhiều người sẽ nghĩ đoạn code trên cho ra kết quả:

0
1
2
3
4
5 <= result of console.log(foo.count)

Nhưng không phải, kết quả in ra là:

0
1
2
3
4
0 <= result of console.log(foo.count)

Như nói ở trên, ta cần quan tâm tới việc hàm foo() được gọi ở đâu. Trong TH này foo() được gọi tại câu lệnh số 10 bằng cách gọi hàm trực tiếp(xem Function Invocation phía dưới) nên context ở đây chính là global. Vì là global nên this.count ở dòng 4 sẽ là undefined dẫn tới this.count++ trả về NaN . Câu lệnh 13 in ra giá trị 0 vì foo.count ở scope hiện tại được khai báo bằng 0.

Nếu bạn muốn tham chiếu tới chính object foo trong function thì sửa thành như sau:

function foo(num) {
  console.log("foo: " + num);
  //keep track of how many times `foo` is called
  foo.count++;
}

foo.count = 0;
var i;
for (i=0; i<5; i++) {
  foo(i);
}

console.log(foo.count);​

Nó sẽ in ra kết quả đúng cho bạn.

3. this trong Function Invocation

Xét ví dụ:

var value = 500; //Global variable
var obj = {
    value: 0,
    increment: function() {
        this.value++;
        var innerFunction = function() {
            console.log(this.value);
        }
        innerFunction(); //Function invocation pattern
    }
}

obj.increment(); //Method invocation pattern

Gọi hàm kiểu Function invocation pattern (gọi trực tiếp bằng cách thêm dấu () ) thì từ khóa this trong hàm đó luôn là global object (window)

Vì vậy đoạn code trên sẽ in ra 500 chứ không phải 1.

4. this trong callback của hàm setTimeout

this trong hàm ẩn danh(anonymous function) luôn là global 

  • this trong callback của setTimeout luôn là global object
  • Không thể tham chiếu tới chính function trong callback của hàm setTimeout vì nó là anonymous function.
var a = 10;
setTimeout( function(){
  // anonymous function (no name), cannot
  // refer to itself
  var a = 20;
  console.log(this.a); // 10
}, 1000);

5. this trong Method Invocation.

this trong Method Invocation chính là context của object gọi tới method đó.

var value = 500; //Global variable
var obj = {
    value: 0,
    increment: function() {
        this.value++;
        console.log(this.value)
    }
}

obj.increment(); //Method invocation pattern

Kết quả trả về là 1 

Nhưng nếu là gọi trong setTimeout thì this luôn là global:

function foo() {
  console.log( this.a );
}

var obj = {
  a: 2,
  foo: foo
};

var a = 100;
setTimeout( obj.foo, 1000 ); // 100

6. this khi gọi với từ khóa new

function foo(a) {
  this.a = a;
}

var a = 10;
var bar = new foo( 2 );
console.log( bar.a ); // 2

Khác với Function invocation ,khi khai báo bằng từ khóa new phía trước, một object sẽ được khởi tạo và trả về object đó nên context ở đây sẽ chính là object được khởi tạo.

7. this trong eval()

  • this trong direct eval là context của nơi gọi hàm
  • this trong indirect eval là context của global

Đọc thêm vền direct eval và indirect eval tại đây

x = 10;
(function foo() {
  var x = 20;

  (function bar(){
    var x = 30;
    eval("test()"); // 10
    var indirectEval = eval;
    indirectEval("test()"); // 10

    var obj = {
      x: 40,
      test: test
    };
    eval("obj.test()"); //  40
    indirectEval("obj.test()"); // lỗi vì ở global ko có biến obj
  })();
})();

function test() {
  var x = 100;
  console.log(this.x)
}

Kết quả in ra là:

10
10
40
error

8. this trong các hàm đặc biệt của js

Như nói ở trên, khi gọi hàm bằng method invocation thì this là context của object. Nhưng trừ 8 hàm đặc biệt sau:

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

Đối với trường hợp Function.prototype: context sẽ là thisArg chứ không phải là object 

Đối với trường hợp Array.prototypecontext sẽ là thisArg nếu được truyền vào, nếu không thì là global 

9. this trong DOM event handler

Xem thêm về cách gọi hàm trong event handler tại đây

<button onclick=console.log(this)>Click me</button>
<!-- Hoặc <button onclick="console.log(this)">Click me</button> -->

this khi event được trigger chính là button chứa event đó.

Nhưng nếu bạn khai báo một hàm trong event handler thì this sẽ là global(window trong browser) vì this đã nằm trong hàm ẩn danh.

<button onclick="console.log(myFunction())">Click me</button>

<script>
function myFunction() {
  return this;
}
</script>

hoặc

<button onclick="console.log((function(){return this})());">Click me</button>

10. this trong ES6

arrow function

Chỉ nên dùng arrow function với các hàm không phải là method của object

var obj = {
  i: 10,
  b: () => console.log(this.i, this),
// hoặc b: () => {console.log(this.i, this)},
  c: function() {
    console.log(this.i, this);
  }
}

obj.b(); // prints undefined, Window {...} (or the global object)
obj.c(); // prints 10, Object {...}

Bản thân arrow function không tự tạo ra this rồi truyền vào cho lệnh thực thi mà thực chất arrow function không được khai báo theo kiểu truyền thống function(){} nên sẽ không có this cho arrow function, this được sử dụng ở đây chính là this của context mà nơi chứa arrow function được gọi.

Nên ở trên obj.b() mới có this là global(Window)

Cũng chính vì lý do trên mà các hàm đặc biệt như bind, call sẽ không hoạt động với arrow function 

var globalObject = this;
var foo = (() => this);
// hoặc var foo = (() => {return this});

console.log(foo() === globalObject); // true
// Call as a method of an object
var obj = {func: foo};
console.log(obj.func() === globalObject); // true

// Attempt to set this using call
console.log(foo.call(obj) === globalObject); // true

// Attempt to set this using bind
foo = foo.bind(obj);
console.log(foo() === globalObject); // true

class trong ES6

Xét ví dụ một đoạn tutorial trong document của react:

class Toggle extends React.Component {
  constructor(props) {
    super(props);
    console.log("1: ", this)
    this.state = {isToggleOn: true};
  }

  handleClick() {
    console.log("2: ", this)
    this.setState(prevState => ({
      isToggleOn: !prevState.isToggleOn
    }));
  }

  render() {
    console.log("3: ", this)
    return (
      <div>
        <button onClick={this.handleClick}>
          {this.state.isToggleOn ? 'ON' : 'OFF'}
        </button>
      </div>
    );
  }
}

ReactDOM.render(
  <Toggle />,
  document.getElementById('root')
);

Các bước thực thi như sau:

  • Đầu tiên là tìm tới nơi bắt đầu gọi hàm ở đâu? Nó là đoạn ReactDOM.render(... 
  • Khi gọi <Toggle /> trong đó, nó sẽ khởi tạo một object Toggle 
  • Khi object Toggle được khởi tạo, hàm constructor được thực thi, this trong hàm này chính là object của Toggle (xem với từ khóa new ở phía trên)
  • Sau khi constructor được gọi xong, hàm render được gọi thực thi, this ở đây vẫn là object của class Toggle 

Bạn để ý trong hàm render có đoạn <button onClick={this.handleClick}> , khác với inline event handler giải thích ở phía trên thì syntax này có vẻ giống nhưng lại không thêm dâu () vào sau handleClick. Khi nào trigger onClick được gọi thì this.handleClick mới được thực thi. Nhưng lúc biên dịch thì this ở đây vẫn là object của Toggle nên không có lỗi gì xảy ra.

Khi render xong và bạn click vào button thì chuyện gì xảy ra?

Khi click, câu lệnh this.handleClick sẽ thực thi, nhảy vào hàm handleClick() như giải thích ở phần DOM event handler this ở đây sẽ là global(window) chứ không phải object của Toggle nên nó sẽ báo lỗi không biết hàm setState của window .

Để sửa lỗi này, có một số cách như sau:

  • Cách 1: Luôn set this thành trigger element trong constructor của class bằng cách thêm dòng như sau:
constructor(props) {
  super(props);
  console.log("1: ", this)
  this.state = {isToggleOn: true};
  this.handleClick = this.handleClick.bind(this);  // thêm dòng này vào
}
  • Cách 2: Biến hàm handleClick thành arrow function để cho chính hàm đó không có this mà sẽ sử dụng this của context gọi nó.
handleClick = () => {
  console.log("2: ", this)
  this.setState(prevState => ({
    isToggleOn: !prevState.isToggleOn
  }));
}
  • Cách 3: Biến hàm callback thành arrow function nhưng cách này khuyến cáo là không nên sử dụng so với 2 cách trên do vấn đề hiệu năng.
render() {
    return (
      <button onClick={(e) => this.handleClick(e)}>
        Click me
      </button>
    );
  }

Một số ví dụ kiểm tra

VD1:

var obj = {
    someData: "a string"
};

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

obj.staticFunction = myFun;
obj.staticFunction();


//=======result==========//
//     this is obj       //
//=======================//

VD2:

var obj = {
    myMethod : function () {
        console.log(this);
    }
};
var myFun = obj.myMethod;
myFun();

//=========result==========//
//  this is global(window) //
//=========================//

VD3:

function myFun() {
    console.log(this);
}
var obj = {
    myMethod : function () {
        eval("myFun()");
    }
};
obj.myMethod();


//=========result==========//
//  this is global(window) //
//=========================//

VD4:

function myFun() {
    console.log(this);
}
var obj = {
    someData: "a string"
};
myFun.call(obj);


//=========result==========//
//      this is obj        //
//=========================//

VD5:

function Person(){
  var age = 10;

  setTimeout(function(){
    this.age++;
    console.log(this.age);
  }, 1000);
}

var p = Person();

//=========result==========//
//           NaN           //
//=========================//

var q = new Person();

//=========result==========//
//           NaN           //
//=========================//

VD6:

function Person(){
  this.age = 10;

  setTimeout(function(){
    this.age++;
    console.log(this.age);
  }, 1000);
}

var p = Person();

//=========result==========//
//           11            //
//=========================//

nhưng:

function Person(){
  this.age = 10;

  setTimeout(function(){
    this.age++;
    console.log(this.age);
  }, 1000);
}

var p = new Person();

//=========result==========//
//           NaN           //
//=========================//

Link tham khảo:

https://github.com/getify/You-Dont-Know-JS/blob/master/this%20%26%20object%20prototypes/ch1.md

https://github.com/getify/You-Dont-Know-JS/blob/master/this%20%26%20object%20prototypes/ch2.md

https://stackoverflow.com/questions/3127429/how-does-the-this-keyword-work/3127440#3127440

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions

https://reactjs.org/docs/handling-events.html