Hiểu từ khóa this trong JavaScript
Nếu muốn thành lập trình viên, bất kể lĩnh vực cụ thể, bạn gần chắc chắn phải đối mặt JavaScript. Nó tiến hóa từ khởi đầu khiêm tốn thành cường năng thực sự. Bây giờ bao phủ front end, backend, mobile, game, desktop app, thậm chí machine learning!
Trước khi thành thạo JavaScript và mở rộng, bạn phải học cơ bản. Từ khóa this nên là số một danh sách học.
this thực sự là gì?
Đơn giản, this là con trỏ, cách tham chiếu đối tượng nào gọi hàm hoặc phương thức trong mã JavaScript. Theo ngôn ngữ đời thường, this là thứ ở bên trái dấu chấm.
This có hai dùng chính. Hãy khám phá qua ví dụ.
Xây dựng đối tượng với this
function Book(title, author, genre) { this.title = title; this.author = author; this.genre = genre; } const book1 = new Book("Lord Of The Rings", "J.R.R. Tolkien", "Fantasy"); const book2 = new Book("Tools Of Titans", "Tim Ferriss", "Self-help"); console.log(book1); console.log(book2);
Mã trên tạo ra đầu ra:
Ở đây, định nghĩa hàm constructor tạo đối tượng book nhận title, author, genre làm tham số.
Hàm constructor là hàm "bản thiết kế" đối tượng từ đó tạo đối tượng tên cho trước (Book trong trường hợp).
Chúng ta tạo đối tượng myBook, truyền tham số yêu cầu (gọi arguments khi invoke hàm, ngược lại parameters khi định nghĩa).
Cuối cùng log myBook, nhận lại đối tượng với thuộc tính tương ứng. Cách này, mỗi đối tượng book mới tạo được gán giá trị truyền vào.
Truy cập thuộc tính với this:
const girl = { name: "Sarah", age: 26, introduce() { console.log( "Hi! " + "I'm " + this.name + " and I'm " + this.age + " years old." ); }, };
Ở đây định nghĩa biến girl giữ đối tượng với thuộc tính name, age, và hàm introduce.
Hàm log câu giới thiệu dùng thuộc tính đối tượng, truy cập bằng this.wanted_property.
This và phạm vi trong JavaScript
Mặc định, khi dùng phạm vi toàn cục, nghĩa là không bên trong hàm hoặc đối tượng định nghĩa, this tham chiếu đối tượng toàn cục. Trong trình duyệt, window là đối tượng toàn cục.
Khi khai báo dữ liệu nguyên thủy (chuỗi, số, boolean...), đối tượng, hoặc hàm trong JavaScript, tất cả gắn vào đối tượng toàn cục (window).
Bất cứ gì viết "ngoài trời", đọc như có window. viết trước. Xem đoạn mã:
function returnThis() { return this; }
Đầu tiên định nghĩa hàm returnThis phạm vi toàn cục trả về giá trị this. Sau đó gọi bằng returnThis().
Vì phạm vi toàn cục, xem như window.returnThis(), trả về thứ bên trái dấu chấm.
Hàm đối tượng hay phương thức
Nếu bây giờ định nghĩa đối tượng mới có cùng hàm bên trong (gọi phương thức đối tượng) và gọi lại, nhận đối tượng đó (ký hiệu ngoặc nhọn) trả về.
const object = { returnThis() { return this; }, }; object.returnThis();
Cạm bẫy của this và ba giải pháp
Nếu hàm không trực tiếp gắn với đối tượng, this bên trong hàm sẽ tham chiếu đối tượng toàn cục.
const weirdBehaviorObject = { directlyTiedFunction() { return this; }, directlyTiedOuterFunction() { return function indirectNestedFunction() { return this; }; }, };
Chạy trong trình duyệt xem nhận được gì:
Định nghĩa đối tượng với hàm trả về this lại, và cái khác trả về hàm bên trong, rồi trả về this.
Khi muốn gọi indirectNestedFunction, trước viết () gọi hàm ngoài trả về hàm trong.
Sau đó gọi hàm trong bằng () nữa trả về window.
Đây là hành vi bí ẩn JavaScript, dẫn đến bất ngờ không mong muốn.
May mắn có cách giải quyết. Cách đầu làm closure, cách làm pre-ES6. Cách hai dùng hàm mũi tên, cách ba dùng phương thức bind, liên quan chặt call và apply.
#1 Giải quyết this với closure
Closure là bộ nhớ bền vững, nghĩa là biến không biến mất khi hàm kết thúc. Được tạo bởi engine JavaScript khi hàm lồng tham chiếu biến định nghĩa trong hàm ngoài để truy cập ngay khi hàm ngoài kết thúc. Đây là ví dụ với this:
const objectWithClosure = { closureFunction() { const self = this; return function enclosedFunction() { return self; }; }, };
Định nghĩa đối tượng với closureFunction, định nghĩa biến self lưu giá trị this. Vì closureFunction trực tiếp gắn với đối tượng chứa, giá trị this tham chiếu nó. Bây giờ, trong enclosedFunction, this thường tham chiếu window. Vì dùng closure truy cập this từ hàm chứa, giữ được đối tượng gọi làm giá trị.
#2 Giải quyết this với hàm mũi tên
Giải pháp hai dùng một trong tính năng hay nhất ES6: hàm mũi tên.
Hàm mũi tên đại diện cú pháp ngắn gọn viết hàm. Thêm vào, chúng nhìn lên một cấp phạm vi tìm this, nghĩa là hàm chứa nó.
Vì vậy nếu định nghĩa đối tượng như:
const arrowFunctionObject = { outerFunc() { return () => this; }, };
Thấy hàm mũi tên trả về đối tượng thực, không phải window.
Đó là vì nó nhìn lên outerFunction gọi nó, nơi this tham chiếu arrowFunctionObject. Vậy hàm mũi tên có closure tích hợp.
Hàm mũi tên lồng nhau
Tất nhiên, đây JavaScript, đây cạm bẫy khác. Có thể không bao giờ gặp ngoài phỏng vấn kỹ thuật, nhưng đủ lý do biết.
Điều gì xảy ra nếu lồng hàm mũi tên trong hàm thường, lại trong hàm khác? Trông như:
const arrowNestedInRegularFunctionObject = { outerFunction() { return function regularFunction() { return () => this; }; }, };
Đối tượng có outerFunction, khi gọi, trả về inner regularFunction.
Gọi regularFunction trả về hàm mũi tên sâu nhất.
Bây giờ, khi gọi, trả về window object.
Hàm mũi tên chỉ nhìn lên 1 cấp phạm vi. Vì regularFunction chứa nó không trực tiếp gắn với đối tượng, window trả về.
Nếu lồng hàm mũi tên trong hàm mũi tên, trong hàm thường, nhận kết quả mong đợi: đối tượng chứa thực tế.
const nestedArrowFunctionObject = { outerFunction() { return () => { return () => this; }; }, };
Trường hợp này hàm mũi tên trong nhìn lên hàm mũi tên ngoài. Hàm ngoài cũng mũi tên nên nhìn lên thêm một cấp. Hàm thường outerFunction trực tiếp gắn với đối tượng gọi
đối tượng, và kết quả, đối tượng trả về làm this.
#3 Giải quyết this với phương thức bind
Cuối cùng, giải pháp ba dùng phương thức bind. Bind dùng khai báo rõ ràng this tham chiếu gì. Nó lấy đối tượng truyền như đối số, gắn nó vào từ khóa this của hàm.
const arrowNestedInRegularFunctionObject = { outerFunction() { return function regularFunction() { return () => this; }.bind(arrowNestedInRegularFunctionObject); }, };
Trong ví dụ, chuỗi bind vào hàm bình thường trả về window làm this. Truyền đối tượng muốn tham chiếu làm this như đối số, vấn đề giải quyết.
This không khó khi hiểu JavaScript
Phù! Đi sâu khá nhiều. Bây giờ bạn hỏi, tại sao phiền toái và nhầm lẫn với this?
Lý do là, khác mọi thứ trong JavaScript, this được xác định động (dynamically scoped). Nghĩa là engine JavaScript định nghĩa this đại diện gì tại runtime khi mã thực thi, bằng nhìn từ đâu, ai gọi nó.
Ngược lại, mọi thứ khác trong JavaScript được xác định từ vựng (lexically scoped), nghĩa là giá trị định nghĩa nơi biến định nghĩa, khi mã viết.
Nhờ this, tạo "bản thiết kế" đối tượng một lần, động tạo đối tượng với giá trị thuộc tính khác nhau như ví dụ book.
Chúng ta cũng làm phương thức exposing và thao tác thuộc tính đối tượng với this.wanted_property.
Chúng ta làm việc quanh quirks JavaScript, điều khiển hành vi tự tin, tạo kết quả mong đợi.
Bây giờ bạn hiểu this trong JavaScript, cũng như vài khái niệm cơ bản khác, bạn đi thử nghiệm, thực hành, tự tin bước tới JavaScript mastery.
You got this!