Bàn về this trong JavaScript – làm sao để xác định this?

Ở bài trước mình đã trình bày về this và call-site, ở bài này mình sẽ nói về cách đánh giá this:

Đầu tiên bạn đánh giá call-site, tức là xem hàm được gọi ở đâu chứ không phải được khai báo ở đâu. tiếp theo xem xét xem 4 luật sau đây luật nào được áp dụng cho tình thế của mình.

1. Default Binding: this tham chiếu tới global object

Nhớ rằng nếu tình thế của bạn không ứng với luật nào khác, thì luật này được xài.

Xem xét đoạn vận chuyển tận nhàe:

function foo() {     console.log(this.a) } var a = 2 foo() // 2 

Đầu tiên (nếu bạn chưa biết) là các biến khai báo ở global scope, như kiểu var a = 2, chính là thuộc tính của global object (điều này không đúng với NodeJS hoặc strict mode, với node thì thay vì var a = 2 ta dùng a = 2).
Thứ hai, khi chạy vận chuyển tận nhàe bạn sẽ thấy this.a chính là thuộc tính a của global object.

Default Binding đã được dùng ở đây, this trỏ tới global object.

Ờ, thế vì sao ta lại biết tình thế này là Default Binding ?

Trong đoạn vận chuyển tận nhàe, foo() được gọi một cách thuần túy, không thông qua object, construGiám đốc kỹ thuậtr, hay dùng một phương thức bind nào, thì Default Binding được áp dụng, có nghĩa là this trong hàm sẽ trỏ tới global object.
Với strict mode, sẽ không bao giờ có Default Binding vì global object undefined, nếu không có luật nào áp dụng, this của ta sẽ là undefined

2. Implicit Binding: this tham chiếu tới object ngữ cảnh

Xem xét đoạn vận chuyển tận nhàe:

function foo() {     console.log(this.a) } var obj = {     a: 2,     foo: foo } obj.foo() // 2 

Đầu tiên, quan tâm cái cách hàm foo() được khai báo và tiếp theo được thêm vào obj như kiểu một thuộc tính tham chiếu. Ta cần nắm được rằng, dù foo có khai báo bên trong obj hay được tham chiếu từ bên ngoài vào, thì cũng đều không có ý nghĩa rằng hàm này "thuộc sở hữu" obj.

mặc dù vậy, call-site (nơi hàm được gọi) dùng obj làm ngữ cảnh để tham chiếu tới hàm, nên ta "có thể" nói rằng obj "sở hữu" hàm foo() tại lúc hàm được gọi.

Khi một object được chỉ định làm ngữ cảnh cho hàm, luật Implicit Binding sẽ được áp dụng, khi đó this sẽ trỏ tới object ngữ cảnh. Do đó, this.a đồng nghĩa với obj.a và giá trị in ra là 2.

Implicitly lost:

Đây là một lỗi có thể sẽ gây đau đầu với những người chưa biết, khi mà Implicit Binding bị trôi về thành Default Binding.

Xem xét:

function foo() {     console.log(this.a) } var obj = {     a: 2,     foo: foo } var bar = obj.foo // function reference/alias! var a = "oops, global" // 'a' also property on global object bar() // "oops, global" 

Mặc dù bar trông có vẻ nhưng là được tham chiếu từ obj.foo, nhưng thực tế, nó chỉ đơn thuần là một tham chiếu khác của foo.
Call-site ở đây chính là chuyện, call-site là bar(), một cách gọi hàm thuần túy, không thêm thắt linh tinh, và Default Binding được dùng.

Trong thực tế, lỗi này thường diễn ra khi ta truyền vào hàm một callback function:

function foo() {     console.log(this.a) } function doFoo(fn) {     // 'fn' is just another reference to 'foo'     fn() // <-- call-site! } var obj = {     a: 2,     foo: foo } var a = "oops, global" // 'a' also property on global object doFoo(obj.foo) // "oops, global" 

3. Explicit Binding: Cha mẹ đặt đâu, con ngồi đấy.

Luật này được áp dụng khi ta gọi các hàm call(..), apply(…). Các hàm này nằm trong prototype của function.

Xem xét đoạn vận chuyển tận nhàe sau:

function foo() {     console.log(this.a) } var obj = {     a: 2 } foo.call(obj) // 2 

Gọi foo thông qua hàm call() cấp quyền ta gán this của foo cho obj. Do đó kết quả in ra 2.

Hard Binding: Chung thủy sắt son

Xem xét:

function foo() {     console.log(this.a); } var obj = {     a: 2 } var bar = function() {     foo.call(obj) } bar() // 2 setTimeout(bar, 100)  // 2 // hard-bound 'bar' can no longer have its 'this' overriden bar.call(window) // 2 

Ở đây, ta tạo một hàm bar() và bên trong nó gọi foo.call(obj), do đó yêu cầu this của foo phải được bind với obj, dù this trong bar có bind với cái gì đi chăng nữa thì foo cũng sẽ bind với obj.

Binding kiểu này vừa rõ ràng vừa vững chắc, nên người ta gọi là Hard Binding.

Vì đây là một pattern phổ biến, nên với ES5 nó được đưa vào thành built-in ultility, nằm trong Function.prototype, và ví dụ được dùng như sau:

function foo(something) {     console.log(this.a, something)     return this.a + something } var obj = {     a: 2 } var bar = foo.bind(obj) var b = bar(3) // 2 3 console.log(b) // 5 

bind(..) trả về một hàm mới mà đã được chỉ định sẵn this.

API call "context"

Còn một kiểu Explicit Binding be bé nữa giới thiệu nốt, đấy là một vài hàm built-in cung ứng một optional paramteter, thường được gọi là "context", để mình truyền vào obj thay vì gọi bind(..), kiểu để đảm bảo callback function của mình gọi đúng theo cái "this" mà mình muốn ấy.

Ví dụ:

function foo(el) {     console.log(el, this.id) } var obj = {     id: "awesome" } // use 'obj' as 'this' for 'foo(..)' calls var arr = [1, 2, 3] arr.forEach(foo, obj) // 1 awesome   2 awesome   3 awesome 

Thực chất bên trong các hàm này cũng dùng Explicit Binding thông qua call(..) hoặc apply(..) ấy mà.

4. new Binding

Luật binding sau cùng này có thể khiến ta nhớ lại một quan niệm sai lầm về function và object trong JS.

Trong các ngôn ngữ hướng đối tượng truyền thống như JAVA, C#, "construGiám đốc kỹ thuậtr" là một hàm đặc sắc được gắn với class, và khi class được khởi tạo với một keyword new thì construGiám đốc kỹ thuậtr của class sẽ được gọi. Kiểu trông thế này:

something = new MyClass(..); 

JS cũng có toán tử new, và cái cách gọi cũng giống như nên nhiều lập trình viên nghĩ rằng cơ chế dùng toán tử new này của JS giống kiểu các ngôn ngữ OOP khác. mặc dù vậy sự thực là không có tí quan hệ nào trong việc dùng giữa 2 bên cả.

Đầu tiên, ta phải được hiểu lại "construGiám đốc kỹ thuậtr" trong JS là gì?

Trong JS, construGiám đốc kỹ thuậtr chỉ đơn thuần là một hàm được gọi kèm với toán tử new đứng trước. Có nghĩa là giờ ông cứ gọi hàm có kèm theo new thì hàm đấy nó thành construGiám đốc kỹ thuậtr. Hàm này không gắn với class hay khởi tạo ra class, không chỉ vậy nó cũng chẳng phải một hàm nào đặc sắc, nó chỉ một function bình thường mà được dùng kèm với new. Khi gọi hàm với keyword new đứng trước, ta đã gọi lên một construGiám đốc kỹ thuậtr call. Đúng ra mà nói, không có cái gì gọi là "construGiám đốc kỹ thuậtr function", mà nó chỉ đơn thuần là construGiám đốc kỹ thuậtr call của function.

Khi có construGiám đốc kỹ thuậtr call, những hành động sau sẽ được thực hiện:

Một object mới được tạo ra (hay constructed) object mới này được gắn prototype object mới được set thành this cho hàm gọi nếu hàm không trả về object nào khác của nó, thì cái object mới tạo ra kia sẽ được trả về.

Xem xét đoạn vận chuyển tận nhàe:

function foo(a) {     this.a = a; } var bar = new foo(2) console.log(bar.a) // 2 

Bằng việc gọi foo(..) với new đứng trước, ta đã tạo một object mới và set nó làm object cho this trong hàm foo.
new là cách sau cùng để bind this. Người ta gọi đó là new Binding.

Thứ tự ưu tiên giữa các luật:

Về cơ bản, bạn chỉ cần nhớ Explicit Binding > Implicit Binding > Default Binding Còn new Binding thường không được dùng phối hợp với các loại binding khác.

Tổng kết:

Tóm tắt lại, nguyên tắc để đánh giá this được bind thành gì thì đầu tiên phải xem xét call-site, tiếp theo đưa ra những câu hỏi sau theo thứ tự, và dừng lại khi có luật tương ứng phù hợp:

Hàm có được gọi với new không ? Hàm có được gọi với call(), apply() hay bind() không ? Hàm có được chỉ định context object không ? Nếu non-strict mode, thì this chính là global object.

Hy vọng với những kiến thức thu được sẽ giúp đỡ ích cho bạn

Dịch và tóm tắt từ cuốn You Don’t Know JS – this & Object Prototypes của Kyle Simpson

Nguồn viblo.asia