[Medium] 📄 this Binding
1. What is this in JavaScript?
thistrong JavaScript là gì?
this là một từ khóa trong JavaScript, trỏ đến đối tượng ngữ cảnh khi hàm được thực thi. Giá trị của this phụ thuộc vào cách hàm được gọi, chứ không phải nơi nó được định nghĩa.
Các quy tắc binding của this
Có bốn quy tắc binding cho this trong JavaScript (theo thứ tự ưu tiên từ cao đến thấp):
- Binding new: Sử dụng từ khóa
newđể gọi hàm khởi tạo - Binding tường minh: Sử dụng
call,apply,bindđể chỉ địnhthismột cách rõ ràng - Binding ngầm định: Gọi thông qua phương thức của đối tượng
- Binding mặc định: Hành vi mặc định trong các trường hợp khác
2. Please explain the difference of this in different contexts
Vui lòng giải thích sự khác biệt của
thistrong các ngữ cảnh khác nhau
1. this trong môi trường toàn cục
console.log(this); // Trình duyệt: window, Node.js: global
function globalFunction() {
console.log(this); // Chế độ không nghiêm ngặt: window/global, chế độ nghiêm ngặt: undefined
}
globalFunction();
'use strict';
function strictFunction() {
console.log(this); // undefined
}
strictFunction();
2. this trong hàm thông thường (Function)
this của hàm thông thường phụ thuộc vào cách gọi:
function regularFunction() {
console.log(this);
}
// Gọi trực tiếp: this trỏ đến đối tượng toàn cục (chế độ không nghiêm ngặt) hoặc undefined (chế độ nghiêm ngặt)
regularFunction(); // window (chế độ không nghiêm ngặt) hoặc undefined (chế độ nghiêm ngặt)
// Gọi như phương thức của đối tượng: this trỏ đến đối tượng đó
const obj = {
method: regularFunction,
};
obj.method(); // obj
// Sử dụng call/apply/bind: this trỏ đến đối tượng được chỉ định
const customObj = { name: 'Custom' };
regularFunction.call(customObj); // customObj
3. this trong Arrow Function
Arrow Function không có this riêng, nó sẽ kế thừa this từ phạm vi bên ngoài (phạm vi từ vựng).
const obj = {
name: 'Object',
// Hàm thông thường
regularMethod: function () {
console.log('regularMethod this:', this); // obj
// Hàm thông thường bên trong: this sẽ thay đổi
function innerRegular() {
console.log('innerRegular this:', this); // window/undefined
}
innerRegular();
// Arrow Function bên trong: this kế thừa từ bên ngoài
const innerArrow = () => {
console.log('innerArrow this:', this); // obj
};
innerArrow();
},
// Arrow Function
arrowMethod: () => {
console.log('arrowMethod this:', this); // window (kế thừa từ toàn cục)
},
};
obj.regularMethod();
obj.arrowMethod();
4. this trong phương thức của đối tượng
const person = {
name: 'John',
age: 30,
// Hàm thông thường: this trỏ đến person
greet: function () {
console.log(`Hello, I'm ${this.name}`); // "Hello, I'm John"
},
// Phương thức viết tắt ES6: this trỏ đến person
introduce() {
console.log(`I'm ${this.name}, ${this.age} years old`);
},
// Arrow Function: this kế thừa từ bên ngoài (ở đây là toàn cục)
arrowGreet: () => {
console.log(`Hello, I'm ${this.name}`); // "Hello, I'm undefined"
},
};
person.greet(); // "Hello, I'm John"
person.introduce(); // "I'm John, 30 years old"
person.arrowGreet(); // "Hello, I'm undefined"
5. this trong hàm khởi tạo
function Person(name, age) {
this.name = name;
this.age = age;
this.greet = function () {
console.log(`Hello, I'm ${this.name}`);
};
}
const john = new Person('John', 30);
john.greet(); // "Hello, I'm John"
console.log(john.name); // "John"
6. this trong Class
class Person {
constructor(name) {
this.name = name;
}
// Phương thức thông thường: this trỏ đến instance
greet() {
console.log(`Hello, I'm ${this.name}`);
}
// Thuộc tính Arrow Function: this được gắn kết với instance
arrowGreet = () => {
console.log(`Hi, I'm ${this.name}`);
};
}
const john = new Person('John');
john.greet(); // "Hello, I'm John"
// Gán phương thức cho biến sẽ mất binding của this
const greet = john.greet;
greet(); // Lỗi: Cannot read property 'name' of undefined
// Thuộc tính Arrow Function không mất binding của this
const arrowGreet = john.arrowGreet;
arrowGreet(); // "Hi, I'm John"
3. Quiz: What will be printed?
Câu đố: Đoạn mã sau sẽ in ra gì?
Câu 1: Phương thức đối tượng và Arrow Function
const obj = {
name: 'Object',
regularFunc: function () {
console.log('A:', this.name);
},
arrowFunc: () => {
console.log('B:', this.name);
},
};
obj.regularFunc();
obj.arrowFunc();
Nhấn để xem đáp án
// A: Object
// B: undefined
Giải thích:
regularFunclà hàm thông thường, được gọi quaobj.regularFunc(),thistrỏ đếnobj, nên in ra"A: Object"arrowFunclà Arrow Function, không cóthisriêng, kế thừathistừ bên ngoài (toàn cục). Toàn cục không có thuộc tínhname, nên in ra"B: undefined"
Câu 2: Hàm được truyền như tham số
const person = {
name: 'John',
greet: function () {
console.log(`Hello, ${this.name}`);
},
};
person.greet(); // 1
const greet = person.greet;
greet(); // 2
setTimeout(person.greet, 1000); // 3
Nhấn để xem đáp án
// 1: "Hello, John"
// 2: "Hello, undefined" hoặc lỗi (chế độ nghiêm ngặt)
// 3: "Hello, undefined" hoặc lỗi (chế độ nghiêm ngặt)
Giải thích:
person.greet()- Gọi thông qua đối tượng,thistrỏ đếnpersongreet()- Sau khi gán phương thức cho biến và gọi trực tiếp,thisbị mất, trỏ đến toàn cục hoặcundefinedsetTimeout(person.greet, 1000)- Phương thức được truyền như callback,thisbị mất
Câu 3: Hàm lồng nhau
const obj = {
name: 'Outer',
method: function () {
console.log('A:', this.name);
function inner() {
console.log('B:', this.name);
}
inner();
const arrow = () => {
console.log('C:', this.name);
};
arrow();
},
};
obj.method();
Nhấn để xem đáp án
// A: Outer
// B: undefined
// C: Outer
Giải thích:
A-methodđược gọi quaobj,thistrỏ đếnobjB-innerlà hàm thông thường, được gọi trực tiếp,thistrỏ đến toàn cục hoặcundefinedC-arrowlà Arrow Function, kế thừathistừmethodbên ngoài, trỏ đếnobj
Câu 4: setTimeout và Arrow Function
const obj = {
name: 'Object',
method1: function () {
setTimeout(function () {
console.log('A:', this.name);
}, 100);
},
method2: function () {
setTimeout(() => {
console.log('B:', this.name);
}, 100);
},
};
obj.method1();
obj.method2();
Nhấn để xem đáp án
// A: undefined
// B: Object
Giải thích:
A- Callback củasetTimeoutlà hàm thông thường, khi thực thithistrỏ đến toàn cụcB- Callback củasetTimeoutlà Arrow Function, kế thừathistừmethod2bên ngoài, trỏ đếnobj
Câu 5: Binding phức tạp của this
const obj1 = {
name: 'obj1',
getThis: function () {
return this;
},
};
const obj2 = {
name: 'obj2',
};
console.log('A:', obj1.getThis().name);
const getThis = obj1.getThis;
console.log('B:', getThis() === window); // Giả sử trong môi trường trình duyệt
obj2.getThis = obj1.getThis;
console.log('C:', obj2.getThis().name);
const boundGetThis = obj1.getThis.bind(obj2);
console.log('D:', boundGetThis().name);
Nhấn để xem đáp án
// A: obj1
// B: true
// C: obj2
// D: obj2
Giải thích:
A- Gọi quaobj1,thistrỏ đếnobj1B- Gọi trực tiếp,thistrỏ đến toàn cục (window)C- Gọi quaobj2,thistrỏ đếnobj2D- Sử dụngbindđể gắnthisvớiobj2
Câu 6: Hàm khởi tạo và prototype
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
console.log(`Hello, I'm ${this.name}`);
};
Person.prototype.delayedGreet = function () {
setTimeout(function () {
console.log('A:', this.name);
}, 100);
};
Person.prototype.arrowDelayedGreet = function () {
setTimeout(() => {
console.log('B:', this.name);
}, 100);
};
const john = new Person('John');
john.delayedGreet();
john.arrowDelayedGreet();
Nhấn để xem đáp án
// A: undefined
// B: John
Giải thích:
A- Callback hàm thông thường củasetTimeout,thistrỏ đến toàn cụcB- Callback Arrow Function củasetTimeout, kế thừathistừarrowDelayedGreetbên ngoài, trỏ đếnjohn
Câu 7: Biến toàn cục vs thuộc tính đối tượng
var name = 'jjjj';
var obj = {
a: function () {
name = 'john';
console.log(this.name);
},
};
obj.a();
Nhấn để xem đáp án
// undefined
Giải thích:
Chìa khóa của câu hỏi này nằm ở việc hiểu sự khác biệt giữa biến toàn cục và thuộc tính đối tượng:
-
thistrongobj.a()trỏ đến đâu:- Gọi qua
obj.a(),thistrỏ đếnobj
- Gọi qua
-
name = 'john'sửa đổi biến toàn cục:name = 'john'; // Không có var/let/const, sửa đổi name toàn cục// Tương đương vớiwindow.name = 'john'; // Môi trường trình duyệt -
this.nametruy cập thuộc tính đối tượng:console.log(this.name); // Tương đương với console.log(obj.name) -
Đối tượng
objkhông có thuộc tínhname:obj.name; // undefined (name không được định nghĩa trong đối tượng obj)
Quá trình thực thi đầy đủ:
// Trạng thái ban đầu
window.name = 'jjjj'; // Biến toàn cục
obj = {
a: function () { /* ... */ },
// Lưu ý: obj không có thuộc tính name!
};
// Thực thi obj.a()
obj.a();
↓
// 1. name = 'john' → Sửa đổi window.name toàn cục
window.name = 'john'; // ✅ Biến toàn cục đã được sửa đổi
// 2. this.name → Truy cập obj.name
this.name; // Bằng obj.name
obj.name; // undefined (obj không có thuộc tính name)
Hiểu lầm phổ biến:
Nhiều người nghĩ sẽ in ra 'john', vì:
- ❌ Nhầm tưởng rằng
name = 'john'sẽ thêm thuộc tính vàoobj - ❌ Nhầm tưởng rằng
this.namesẽ truy cập biến toàn cục
Hiểu đúng:
- ✅
name = 'john'chỉ sửa đổi biến toàn cục, không ảnh hưởng đếnobj - ✅
this.nametruy cậpobj.name, không phảinametoàn cục
Nếu muốn in ra 'john', nên viết như sau:
var obj = {
a: function () {
this.name = 'john'; // ✅ Thêm thuộc tính name cho obj
console.log(this.name); // 'john'
},
};
obj.a(); // In ra 'john'
console.log(obj.name); // 'john'
Câu 8: Bẫy biến toàn cục (mở rộng)
var name = 'global';
var obj = {
name: 'object',
a: function () {
name = 'modified'; // Lưu ý: không có var/let/const
console.log('1:', name); // Truy cập biến toàn cục
console.log('2:', this.name); // Truy cập thuộc tính đối tượng
},
};
obj.a();
console.log('3:', name); // Biến toàn cục
console.log('4:', obj.name); // Thuộc tính đối tượng
Nhấn để xem đáp án
// 1: modified
// 2: object
// 3: modified
// 4: object
Giải thích:
// Trạng thái ban đầu
window.name = 'global'; // Biến toàn cục
obj.name = 'object'; // Thuộc tính đối tượng
// Thực thi obj.a()
name = 'modified'; // Sửa đổi window.name toàn cục
console.log('1:', name); // Truy cập toàn cục: 'modified'
console.log('2:', this.name); // Truy cập obj.name: 'object'
// Sau khi thực thi
console.log('3:', name); // Toàn cục: 'modified'
console.log('4:', obj.name); // Đối tượng: 'object' (không bị sửa đổi)
Khái niệm chính:
name(không cóthis.) → Truy cập biến toàn cụcthis.name(cóthis.) → Truy cập thuộc tính đối tượng- Hai cái này là hai biến hoàn toàn khác nhau!
4. How to preserve this in callbacks?
Làm thế nào để giữ
thistrong hàm callback?
Phương pháp 1: Sử dụng Arrow Function
const obj = {
name: 'Object',
method: function () {
// ✅ Arrow Function kế thừa this từ bên ngoài
setTimeout(() => {
console.log(this.name); // "Object"
}, 1000);
},
};
obj.method();
Phương pháp 2: Sử dụng bind()
const obj = {
name: 'Object',
method: function () {
// ✅ bind gắn kết this
setTimeout(
function () {
console.log(this.name); // "Object"
}.bind(this),
1000
);
},
};
obj.method();
Phương pháp 3: Lưu this vào biến (phương pháp cũ)
const obj = {
name: 'Object',
method: function () {
// ✅ Lưu this vào biến
const self = this;
setTimeout(function () {
console.log(self.name); // "Object"
}, 1000);
},
};
obj.method();
Phương pháp 4: Sử dụng call() hoặc apply()
function greet() {
console.log(`Hello, I'm ${this.name}`);
}
const person1 = { name: 'John' };
const person2 = { name: 'Jane' };
greet.call(person1); // "Hello, I'm John"
greet.apply(person2); // "Hello, I'm Jane"
5. Common this pitfalls
Các bẫy phổ biến của
this
Bẫy 1: Gán phương thức đối tượng cho biến
const obj = {
name: 'Object',
greet: function () {
console.log(this.name);
},
};
obj.greet(); // ✅ "Object"
const greet = obj.greet;
greet(); // ❌ undefined (this bị mất)
// Giải pháp: sử dụng bind
const boundGreet = obj.greet.bind(obj);
boundGreet(); // ✅ "Object"
Bẫy 2: this trong event listener
const button = document.querySelector('button');
const obj = {
name: 'Object',
// ❌ Arrow Function: this không trỏ đến button
handleClick1: () => {
console.log(this); // window
},
// ✅ Hàm thông thường: this trỏ đến phần tử kích hoạt sự kiện
handleClick2: function () {
console.log(this); // phần tử button
},
// ✅ Nếu cần truy cập this của đối tượng, bọc bằng Arrow Function
handleClick3: function () {
button.addEventListener('click', () => {
console.log(this.name); // "Object"
});
},
};
Bẫy 3: Callback trong phương thức mảng
const obj = {
name: 'Object',
items: [1, 2, 3],
// ❌ Callback hàm thông thường sẽ mất this
processItems1: function () {
this.items.forEach(function (item) {
console.log(this.name, item); // undefined 1, undefined 2, undefined 3
});
},
// ✅ Callback Arrow Function giữ được this
processItems2: function () {
this.items.forEach((item) => {
console.log(this.name, item); // "Object" 1, "Object" 2, "Object" 3
});
},
// ✅ Sử dụng tham số thisArg của forEach
processItems3: function () {
this.items.forEach(function (item) {
console.log(this.name, item); // "Object" 1, "Object" 2, "Object" 3
}, this); // Tham số thứ hai chỉ định this
},
};
6. this binding rules summary
Tóm tắt quy tắc binding của
this
Thứ tự ưu tiên (từ cao đến thấp)
// 1. Binding new (ưu tiên cao nhất)
function Person(name) {
this.name = name;
}
const john = new Person('John');
console.log(john.name); // "John"
// 2. Binding tường minh (call/apply/bind)
function greet() {
console.log(this.name);
}
const obj = { name: 'Object' };
greet.call(obj); // "Object"
// 3. Binding ngầm định (phương thức đối tượng)
const obj2 = {
name: 'Object2',
greet: greet,
};
obj2.greet(); // "Object2"
// 4. Binding mặc định (ưu tiên thấp nhất)
greet(); // undefined (chế độ nghiêm ngặt) hoặc window.name
Bảng so sánh Function vs Arrow Function
| Đặc điểm | Function | Arrow Function |
|---|---|---|
Có this riêng | ✅ Có | ❌ Không |
this phụ thuộc vào | Cách gọi | Vị trí định nghĩa (phạm vi từ vựng) |
Có thể thay đổi this bằng call/apply/bind | ✅ Có thể | ❌ Không thể |
| Có thể dùng làm hàm khởi tạo | ✅ Có thể | ❌ Không thể |
Có đối tượng arguments | ✅ Có | ❌ Không |
| Phù hợp cho | Phương thức đối tượng, hàm khởi tạo | Callback, khi cần kế thừa this bên ngoài |
Cách ghi nhớ
"Arrow kế thừa, hàm gọi"
- Arrow Function:
thiskế thừa từ phạm vi bên ngoài, được xác định khi định nghĩa- Hàm thông thường:
thisphụ thuộc vào cách gọi, được xác định khi thực thi
7. Best practices
Các phương pháp tốt nhất
✅ Cách làm được khuyến nghị
// 1. Sử dụng hàm thông thường hoặc cú pháp viết tắt ES6 cho phương thức đối tượng
const obj = {
name: 'Object',
// ✅ Tốt: Hàm thông thường
greet: function () {
console.log(this.name);
},
// ✅ Tốt: Viết tắt ES6
introduce() {
console.log(this.name);
},
};
// 2. Sử dụng Arrow Function cho callback
class Component {
constructor() {
this.name = 'Component';
}
mount() {
// ✅ Tốt: Arrow Function giữ được this
setTimeout(() => {
console.log(this.name);
}, 1000);
}
}
// 3. Sử dụng hàm thông thường khi cần this động
const button = {
label: 'Click me',
// ✅ Tốt: Cần truy cập this của phần tử DOM
handleClick: function () {
console.log(this); // phần tử DOM button
},
};
❌ Cách làm không được khuyến nghị
// 1. Không sử dụng Arrow Function cho phương thức đối tượng
const obj = {
name: 'Object',
// ❌ Xấu: this không trỏ đến obj
greet: () => {
console.log(this.name); // undefined
},
};
// 2. Không sử dụng Arrow Function làm hàm khởi tạo
// ❌ Xấu: Arrow Function không thể dùng làm hàm khởi tạo
const Person = (name) => {
this.name = name; // Lỗi!
};
// 3. Không sử dụng Arrow Function khi cần truy cập arguments
// ❌ Xấu: Arrow Function không có arguments
const sum = () => {
console.log(arguments); // ReferenceError
};