Ràng buộc của this trong Javascript. Sử dụng call, apply, bind
Một khi chúng ta làm việc với Javascript thì chắc chắn phải làm quen với this rồi, this là một trong những khái niệm có thể gọi là phức tạp trong JS, nhưng nó cũng chỉ có vài vấn đề cơ bản cần nhớ thôi.
Từ khóa this
cho phép chúng ta quyết định đối tượng nào là tiêu điểm khi thực thi hàm hay phương thức. Tóm lược thì chúng ta có:
this
là một tham chiếu của đối tượng tại nơi mà hàm được gọi- Context mặc định của
this
làwindow
- Context được lấy ngay tại nơi hàm được gọi
- Hàm mũi tên có
this
nằm ngoài context
Ràng buộc window
Để nhận biết context mặc định của this
, chúng ta dùng một hàm đơn giản để in this
ra xe nó là gì
function Foo() {
console.log(this);
}
Foo(); // context ở dòng này là 'window'
// output: 'window'
// Nếu Foo là một hàm mũi tên thì nó cũng sẽ ràng buộc this với window
Và ta thấy được rằng context mặc định của this
dù là hàm thông thường hay hàm mũi tên nó đều là window
Giả sử ta có đoạn code sau
function sayName () {
console.log(`My name is ${this.age}`)
}
sayAge() // My age thành undefined
// My age thành undefined bởi vì this.age undefined
// Nếu ta thêm
window.age = 27
// chúng ta có thể thấy là ngoài các ràng buộc ở trên thì cuối cùng this sẽ ràng buộc với window.
sayName()
Ràng buộc ngầm
Ví dụ này cho ta thấy this
là tham chiếu của đối tượng ngay tại nơi hàm được gọi
const user = {
name: 'John',
greet() {
alert(`My name is ${this.name}`)
}
}
user.greet()
// My name is John
Để tìm ra từ khóa này đang tham chiếu gì, hãy nhìn sang bên trái của dấu chấm khi hàm được gọi, đó chính là user
mà this
đang tham chiếu.
Ví dụ sâu hơn
const user = {
name: 'John',
greet() {
alert(`My name is ${this.name}`)
},
mother: {
name: 'Mary',
greet() {
alert(`My name is ${this.name}`)
}
}
}
thì khi gọi user.mother.greet
ta có this
thuộc đối tượng user.mother
Ràng buộc trực tiếp
Xét trường hợp hàm greet
không nằm trong object
function greet () {
alert(`Hello, my name is ${this.name}`)
}
const user = {
name: 'Tyler',
age: 27,
}
.call
Để lấy được ngữ cảnh this
trong object, chúng ta sử dụng phương thức call
vì call cho phép ta lấy được ngữ cảnh của đối tượng khi gọi hàm
greet.call(user)
Trong trường hợp hàm greet
có tham số, chúng ta có thể sử dụng như sau:
function greet (l1, l2, l3) {
alert(
`My name is ${this.name} and I know ${l1}, ${l2}, and ${l3}`
)
}
const user = {
name: 'John',
age: 27,
}
const languages = ['JavaScript', 'Ruby', 'Python']
greet.call(user, languages[0], languages[1], languages[2])
.apply
Tuy nhiên, việc gán quá nhiều giá trị cho tham số cũng gây rối rắm nên chúng ta dùng apply
greet.apply(user, languages) // kết quả cũng tương tự greet.call(user, languages[0], languages[1], languages[2])
.bind
Với .bind
nó cũng hoạt động tương tự .call
, nhưng thay vì thực thi hàm ngay, thì nó sẽ trả về một hàm để chúng ta có thể sử dụng sau.
const newFn = greet.bind(user, languages[0], languages[1], languages[2])
// Call after
newFn()
Ràng buộc new
Khi một hàm được gọi bằng new
từ khóa this
sẽ được tham chiếu vào object khởi tạo.
function User (name, age) {
this.name = name
this.age = age
}
const me = new User('John', 27)
Về cơ bản thì JS sẽ tạo một object mới gọi là this
đại diện cho prototype của User. Khi một hàm được gọi với từ khóa new
thì object mới của this
này được tham chiếu tại nơi instance được tạo.
Hàm mũi tên không phải là construction nên nếu chúng ta không khai báo với từ khóa new
được
Ràng buộc lexical (từ vựng)
Từ khóa this
trong JS thường được cho là phức tạp hơn mức cần thiết, tuy nhiên ràng buộc lexical là khái niệm trực quan nhất. Khái niệm lexical có thể tìm hiểu tại đây. Nói đơn giản thì lexical là “khu vực” ngay tại nơi nó (this
) được tạo ra.
es6 cung cấp hàm mũi tên cho phép viết hàm một cách ngắn gọn
friends.map((friend) => friend.name)
Khác với hàm thông thường, hàm mũi tên không có this
cho nó. Thực chất this
nằm đúng ở mức độ lexical, và chúng ta có thể truy vấn các biến số theo cách thông thường.
Trước hết chúng ta lấy ví dụ khi sử dụng hàm thông thường
const user = {
name: 'John',
age: 27,
languages: ['English', 'Vietnamese', 'Japanese'],
greet() {
const hello = `My name is ${this.name} and I know`
const langs = this.languages.reduce(function (str, lang, i) {
if (i !== this.languages.length - 1) {
return `${str} and ${lang}.`
}
return `${str} ${lang},`
}, "")
console.log(hello + langs)
}
}
Khi gọi hàm trên chúng ta sẽ gặp lỗi
**Uncaught TypeError: Cannot read property 'length' of undefined
**vì this.languages
có giá trị undefined
, nghĩa là this
ở đây lại không tham chiếu vào user.
Cách kiểm tra là chúng ta xác định nơi hàm được thực thi, ta có thể thấy là hàm chúng ta được truyền vào một hàm có sẵn reduce
, vì vậy ta cũng không biết là reduce
được thực thi ra sao, bên trong nó là một hàm vô danh bên trong reduce
và chúng ta không rõ nơi nó sẽ thực thi.
Chính vì lẽ đó, chúng ta phải xác định cái gì được truyền vào reduce
khi nó thực thi hàm vô danh.
Sử dụng bind
để truyền vào hàm vô danh
const user = {
name: 'Tyler',
age: 27,
languages: ['JavaScript', 'Ruby', 'Python'],
greet() {
const hello = `Hello, my name is ${this.name} and I know`
const langs = this.languages.reduce(function (str, lang, i) {
if (i === this.languages.length - 1) {
return `${str} and ${lang}.`
}
return `${str} ${lang},`
}.bind(this), "")
alert(hello + langs)
}
}
Vấn đề đã được giải quyết.
Việc xử lý trên cho thấy chúng ta đã truyền một hàm mới vào trong reduce
, điều này nghĩa là chúng ta đã tạo ra một context mới.
Với hàm mũi tên, nó không có khái niệm this
, nó sẽ dùng cách tìm kiếm thông thường để tìm giá trị biến tại scope gần nhất, vì vậy sử dụng hàm mũi tên nó sẽ đơn giản xử lý đoạn code trên.
const user = {
name: 'John',
age: 27,
languages: ['JavaScript', 'Ruby', 'Python'],
greet() {
const hello = `Hello, my name is ${this.name} and I know`
const langs = this.languages.reduce((str, lang, i) => {
if (i === this.languages.length - 1) {
return `${str} and ${lang}.`
}
return `${str} ${lang},`
}, "")
alert(hello + langs)
}
}
Thế đấy, this
trong Javascript thực ra có nhiêu đó thôi, cũng không khó hiểu lắm đâu.