11/08/2018, 20:34

this trong javascript

Đọc bài viết gốc tại đây 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 ...

alt text
Đọc bài viết gốc tại đây

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, 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à instance 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 hàm chứa tao (context).

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à instance hiện tại 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 trường hợp này foo() được gọi ở trong điều kiện for() 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.

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.

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);

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
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.

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

Đọc thêm vền direct evalindirect 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

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.prototype: context sẽ là thisArg nếu được truyền vào, nếu không thì là global.

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

<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>

arrow function

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 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>
    );
  }

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

0