Bài viết này sẽ đi sâu vào từ khóa "this" trong JavaScript, khám phá cách thức hoạt động, lý do tại sao nó hoạt động khác nhau trong các ngữ cảnh khác nhau và cách làm chủ nó có thể làm cho code của bạn sạch hơn và hiệu quả hơn. Sau khi đọc xong, bạn sẽ nắm vững cách sử dụng hiệu quả từ khóa "this" trong JavaScript cho các dự án của mình.
Vậy từ khóa "this" trong JavaScript là gì?
Từ khóa "this" trong JavaScript rất cần thiết vì nó cho phép tương tác động và dựa trên ngữ cảnh trong code của bạn. Dưới đây là một số lý do tại sao nó rất có giá trị:
- Truy cập trực tiếp các thuộc tính của đối tượng: "this" cho phép bạn truy cập và thao tác các thuộc tính và phương thức của đối tượng từ bên trong các hàm của nó, giúp dễ dàng làm việc trực tiếp với đối tượng hơn.
- Xử lý sự kiện: Trong JavaScript hướng sự kiện, "this" thường đề cập đến phần tử HTML đã kích hoạt sự kiện. Điều này giúp dễ dàng quản lý các tương tác DOM mà không cần truyền rõ ràng các phần tử vào các hàm.
- Liên kết động: "this" thích ứng dựa trên cách gọi hàm, chứ không phải nơi nó được định nghĩa. Tính linh hoạt này cho phép "this" tham chiếu đến các đối tượng khác nhau, tùy thuộc vào ngữ cảnh - cho dù đó là hàm toàn cục, phương thức bên trong đối tượng hay hàm gọi lại trong trình lắng nghe sự kiện.
- Đơn giản hóa các mẫu hướng đối tượng: Sử dụng "this" cho phép tiếp cận hướng đối tượng hơn, trong đó các thuộc tính và phương thức được đóng gói trong các đối tượng, làm cho code có tổ chức và có thể mở rộng.
- Thực thi theo ngữ cảnh: "this" cho phép các hàm chạy trong ngữ cảnh của đối tượng gọi chúng. Điều này có nghĩa là bạn không cần phải truyền đối tượng làm đối số; "this" sẽ tự động tham chiếu đến nó.
- Hữu ích trong các hàm tạo và lớp: Trong các lớp ES6 hoặc các hàm tạo truyền thống, "this" là không thể thiếu để định nghĩa các thuộc tính và phương thức trên phiên bản mới được tạo, cung cấp cho mỗi phiên bản dữ liệu riêng của nó.
Với những tính năng này, "this" không chỉ là một từ khóa mà còn là một khía cạnh cơ bản trong cách tiếp cận của JavaScript đối với các hàm, đối tượng và lập trình hướng ngữ cảnh.
Hiểu "this" trong các ngữ cảnh khác nhau
Giá trị của từ khóa "this" trong JavaScript không cố định và có thể thay đổi tùy thuộc vào ngữ cảnh mà hàm được gọi. Tính chất động này của "this" là một trong những khía cạnh độc đáo nhất - và đôi khi gây nhầm lẫn - của JavaScript. Nói chung, có một số ngữ cảnh xác định giá trị của "this".
Chúng ta hãy phân tích từng ngữ cảnh với các ví dụ để xem "this" hoạt động như thế nào:
1. Ngữ cảnh mặc định/toàn cục
Khi "this" được sử dụng trong ngữ cảnh toàn cục hoặc bên trong một hàm độc lập, nó đề cập đến đối tượng toàn cục, là window trong trình duyệt và global trong Node.js.
Ví dụ:
function showGlobalContext() { console.log(this);
} showGlobalContext();
Code này xuất ra Window ... trong trình duyệt hoặc [global object] trong Node.js. Vì showGlobalContext được gọi trong ngữ cảnh toàn cục, "this" trỏ đến đối tượng toàn cục (window trong trình duyệt hoặc global trong Node.js). Ở đây, không có liên kết rõ ràng hoặc ngầm định, vì vậy "this" mặc định là phạm vi toàn cục.
2. Liên kết ngầm định
Khi một hàm được gọi như một phương thức của một đối tượng, "this" đề cập đến đối tượng đã gọi phương thức đó. Điều này được gọi là liên kết ngầm định.
Ví dụ:
const person = { name: "Alice", greet() { console.log(`Hello, I am ${this.name}`); }
}; person.greet();
Điều này xuất ra Hello, I am Alice vì greet được gọi bởi đối tượng person. Do liên kết ngầm định, "this" bên trong greet đề cập đến person, cho phép truy cập vào thuộc tính name của nó. Liên kết ngầm định xảy ra khi hàm được gọi với một đối tượng đứng trước.
3. Liên kết rõ ràng
JavaScript cho phép liên kết rõ ràng của "this" bằng cách sử dụng các phương thức call, apply và bind. Các phương thức này cho phép bạn đặt "this" trực tiếp thành một đối tượng cụ thể.
Ví dụ:
function introduce() { console.log(`Hello, I am ${this.name}`);
} const user = { name: "Bob" }; // Using call
introduce.call(user); // Using apply
introduce.apply(user); // Using bind
const boundIntroduce = introduce.bind(user);
boundIntroduce();
Mỗi lần gọi phương thức đều xuất ra Hello, I am Bob. Với call và apply, chúng ta ngay lập tức gọi introduce, đặt rõ ràng "this" thành user, có thuộc tính name là "Bob". Tuy nhiên, phương thức bind trả về một hàm mới với "this" được liên kết vĩnh viễn với user, cho phép boundIntroduce được gọi sau này với "this" vẫn được đặt thành user.
4. Hàm mũi tên
Hàm mũi tên trong JavaScript không có liên kết "this" riêng. Thay vào đó, chúng kế thừa "this" từ phạm vi từ vựng của chúng hoặc ngữ cảnh mà chúng được định nghĩa. Hành vi này rất hữu ích cho các hàm gọi lại và hàm lồng nhau.
Ví dụ:
const team = { name: "Development Team", members: ["Alice", "Bob", "Charlie"], introduceTeam() { this.members.forEach(member => { console.log(`${member} is part of ${this.name}`); }); }
}; team.introduceTeam();
Đoạn mã trên sẽ xuất ra:
Alice is part of Development Team
Bob is part of Development Team
Charlie is part of Development Team
Ở đây, hàm mũi tên bên trong forEach không tạo "this" riêng của nó; thay vào đó, nó kế thừa "this" từ introduceTeam, được gọi bởi team. Do đó, "this" bên trong hàm mũi tên đề cập đến team, cho phép truy cập vào thuộc tính name. Nếu một hàm thông thường được sử dụng trong forEach, "this" sẽ là undefined (trong chế độ nghiêm ngặt) hoặc trỏ đến đối tượng toàn cục, dẫn đến kết quả không mong muốn.
5. Liên kết mới (Hàm tạo)
Khi một hàm được sử dụng làm hàm tạo (được gọi với từ khóa new), "this" bên trong hàm đó đề cập đến phiên bản mới được tạo. Điều này hữu ích để tạo nhiều phiên bản của một đối tượng với các thuộc tính và phương thức riêng của chúng.
Ví dụ:
function Person(name) { this.name = name;
} const person1 = new Person("Alice");
console.log(person1.name); // Output: Alice
Trong ví dụ này, gọi new Person("Alice") tạo một đối tượng mới trong đó "this" đề cập đến đối tượng mới đó, chứ không phải toàn cục hoặc bất kỳ ngữ cảnh nào khác. Kết quả là một phiên bản mới (person1) với thuộc tính name được đặt thành "Alice".
6. Ngữ cảnh lớp
Trong cú pháp ES6+, các lớp JavaScript cũng sử dụng từ khóa "this" để tham chiếu đến phiên bản của lớp trong các phương thức. Hành vi này tương tự như liên kết new, vì mỗi phiên bản của lớp sẽ có ngữ cảnh "this" riêng.
Ví dụ:
class Car { constructor(model) { this.model = model; } showModel() { console.log(`This car is a ${this.model}`); }
} const myCar = new Car("Toyota");
myCar.showModel(); // Output: This car is a Toyota
Ở đây, "this" bên trong showModel đề cập đến phiên bản cụ thể myCar, cho phép truy cập vào thuộc tính model của nó. Mỗi phiên bản được tạo với new Car sẽ có "this" riêng của nó đề cập đến phiên bản đó.
7. Trình lắng nghe sự kiện DOM
Trong các trình lắng nghe sự kiện, "this" đề cập đến phần tử HTML đã kích hoạt sự kiện. Điều này giúp dễ dàng truy cập các thuộc tính hoặc phương thức của phần tử đó mà không cần phải truyền nó làm đối số một cách rõ ràng.
Ví dụ:
const button = document.querySelector("button");
button.addEventListener("click", function() { console.log(this); // Output: <button> element
});
Trong trường hợp này, "this" bên trong trình lắng nghe sự kiện đề cập đến phần tử button đã được nhấp, cho phép truy cập vào các thuộc tính và phương thức của nó. Tuy nhiên, nếu bạn sử dụng hàm mũi tên làm trình xử lý sự kiện, "this" sẽ đề cập đến phạm vi từ vựng, có thể dẫn đến hành vi không mong muốn.
Những cạm bẫy thường gặp với từ khóa "this" trong JavaScript
Những hiểu lầm xung quanh "this" có thể dẫn đến kết quả không mong muốn trong JavaScript. Dưới đây là một số cạm bẫy thường gặp cần chú ý:
1. Mất "this" trong Hàm gọi lại
Khi truyền một phương thức làm hàm gọi lại, "this" có thể mất tham chiếu ban đầu của nó. Điều này xảy ra vì khi một hàm được gọi như một hàm độc lập (mà không có đối tượng gọi nó), "this" mặc định là đối tượng toàn cục hoặc trở thành undefined trong chế độ nghiêm ngặt.
Ví dụ:
const user = { name: "Alice", greet() { console.log(`Hello, I am ${this.name}`); }
}; setTimeout(user.greet, 1000); // Output: Hello, I am undefined
Trong ví dụ này, this trở thành undefined trong greet vì setTimeout gọi greet như một hàm độc lập, không phải như một phương thức của user.
2. "this" không mong muốn trong Hàm mũi tên
Hàm mũi tên không có ngữ cảnh "this" riêng của chúng; thay vào đó, chúng kế thừa "this" từ phạm vi từ vựng xung quanh. Điều này có thể gây ra sự cố khi hàm mũi tên được sử dụng trong các tình huống mà "this" nên tham chiếu đến đối tượng gọi, chẳng hạn như trong các phương thức hoặc trình lắng nghe sự kiện. Hành vi này có thể dẫn đến các giá trị không mong muốn cho "this" trong các trường hợp mà nhà phát triển có thể mong đợi một ngữ cảnh "this" mới.
Ví dụ:
const button = document.querySelector("button"); button.addEventListener("click", () => { console.log(this); // Output: Window object (or global object in Node.js)
});
Ở đây, "this" đề cập đến đối tượng toàn cục thay vì button vì hàm mũi tên kế thừa "this" từ phạm vi định nghĩa của chúng, chứ không phải từ ngữ cảnh sự kiện.
3. "this" trong Hàm lồng nhau
Khi sử dụng các hàm thông thường được lồng nhau trong các phương thức, "this" có thể bất ngờ trỏ đến đối tượng toàn cục thay vì hàm hoặc đối tượng bên ngoài. Điều này xảy ra vì mỗi lần gọi hàm có ngữ cảnh "this" riêng. Trong một hàm lồng nhau, nếu "this" không được liên kết rõ ràng, nó sẽ mặc định trở lại ngữ cảnh toàn cục, điều này có thể dẫn đến hành vi không mong muốn khi cố gắng truy cập các thuộc tính của đối tượng bên ngoài.
Ví dụ:
const person = { name: "Alice", introduce() { function showName() { console.log(this.name); } showName(); // Output: undefined (or error in strict mode) }
}; person.introduce();
Trong ví dụ này, "this" bên trong showName mặc định là phạm vi toàn cục thay vì tham chiếu đến person, dẫn đến kết quả đầu ra không mong muốn.
Best practice tốt nhất để sử dụng từ khóa "thís"
Làm chủ từ khóa "this" trong JavaScript có thể cải thiện đáng kể khả năng đọc và khả năng bảo trì của mã. Dưới đây là một số phương pháp hay nhất để giúp đảm bảo "this" hoạt động như mong đợi trong các ngữ cảnh khác nhau:
1. Sử dụng Hàm mũi tên cho Phạm vi từ vựng
Đối với các hàm cần giữ lại "this" từ phạm vi xung quanh, hãy sử dụng hàm mũi tên. Hàm mũi tên không có "this" riêng, vì vậy chúng kế thừa nó từ nơi chúng được định nghĩa. Điều này hữu ích trong các hàm gọi lại hoặc hàm lồng nhau.
Ví dụ:
const person = { name: "Alice", introduce() { setTimeout(() => { console.log(`Hello, I am ${this.name}`); }, 1000); }
}; person.introduce(); // Output: Hello, I am Alice
2. Sử dụng Bind, Call hoặc Apply để Liên kết rõ ràng
Khi bạn cần đặt "this" thành một đối tượng cụ thể, hãy sử dụng bind, call hoặc apply. Điều này hữu ích cho các hàm gọi lại hoặc các lệnh gọi hàm độc lập trong đó bạn muốn "this" tham chiếu đến một đối tượng cụ thể.
Ví dụ:
const user = { name: "Bob", greet() { console.log(`Hello, I am ${this.name}`); }
}; setTimeout(user.greet.bind(user), 1000); // Output: Hello, I am Bob
3. Tránh "this" trong Phạm vi toàn cục
Trong phạm vi toàn cục, "this" đề cập đến đối tượng window (trong trình duyệt) hoặc global (trong Node.js), điều này có thể dẫn đến kết quả không mong muốn. Giữ các hàm phụ thuộc vào "this" trong các đối tượng hoặc lớp.
Ví dụ:
function showThis() { console.log(this);
} showThis(); // Output: window or global object
4. Sử dụng "this" trong Lớp và Hàm tạo
Trong các lớp ES6 hoặc hàm tạo, hãy sử dụng "this" cho các thuộc tính của phiên bản. Điều này giữ cho dữ liệu của mỗi phiên bản riêng biệt, tuân theo thiết kế hướng đối tượng.
Ví dụ:
class Car { constructor(model) { this.model = model; } showModel() { console.log(`This car is a ${this.model}`); }
} const myCar = new Car("Toyota");
myCar.showModel(); // Output: This car is a Toyota
5. Kiểm tra "this" trong Các ngữ cảnh khác nhau
Kiểm tra cách "this" hoạt động khi hàm của bạn được sử dụng trong các ngữ cảnh khác nhau — chẳng hạn như phương thức, hàm gọi lại và trình lắng nghe sự kiện. Điều này giúp phát hiện kết quả không mong muốn sớm trong quá trình phát triển.
Kết luận
Trong bài viết này, chúng ta đã khám phá từ khóa "this" trong JavaScript, bao gồm hành vi của nó trong các ngữ cảnh khác nhau như toàn cục, ngầm định, rõ ràng, liên kết mới và hàm mũi tên. Chúng ta cũng đã thảo luận về các cạm bẫy thường gặp cần tránh và các phương pháp hay nhất để đảm bảo "this" hoạt động như mong đợi trong mã của bạn. Làm chủ "this" có thể cải thiện đáng kể độ rõ ràng và tính linh hoạt của mã, cho phép bạn viết JavaScript hiệu quả và dễ bảo trì hơn.