this, bind và context trong TypeScript & JavaScript

Bài 15 – this, bind và cách kiểm soát context trong TypeScript & JavaScript

15/1/2024 DaiPhan
Bài 12 / 17

this, bind và context trong TypeScript & JavaScript

this là một trong những khái niệm dễ gây nhầm lẫn nhất trong JavaScript. Nắm chắc cách hoạt động của this và cách “cố định” context bằng bind sẽ giúp bạn tránh nhiều lỗi khó hiểu, đặc biệt trong class, callback và môi trường bất đồng bộ.

TypeScript giúp bạn bắt lỗi this ngay khi code, giúp an toàn hơn nhiều so với JavaScript thuần.

Nội dung chính

  • this là gì và phụ thuộc vào ngữ cảnh nào
  • this trong object, class, function, arrow function
  • Mất context trong callback và cách khắc phục
  • bind, call, apply – khác nhau thế nào
  • Cách TypeScript giúp kiểm soát lỗi liên quan đến this

Ví dụ chi tiết

1. this trong object

const user = {
  name: "Alice",
  greet() {
    console.log("Hello", this.name);
  }
};

user.greet(); // Output: "Hello Alice"

2. Mất context khi tách method

const user = {
  name: "Alice",
  greet() {
    console.log("Hello", this.name);
  }
};

const greetFn = user.greet;
// greetFn(); // Lỗi: Cannot read property 'name' of undefined

3. bind để cố định context

const user = {
  name: "Alice",
  greet() {
    console.log("Hello", this.name);
  }
};

const greetBound = user.greet.bind(user);
greetBound(); // Output: "Hello Alice"

4. this trong class

class Counter {
  constructor() {
    this.count = 0;
  }

  increase() {
    this.count++;
    console.log(`Count: ${this.count}`);
  }
}

const c = new Counter();
const inc = c.increase;
// inc(); // Lỗi: Cannot read property 'count' of undefined

const incFixed = c.increase.bind(c);
incFixed(); // Output: "Count: 0"

5. Arrow function giữ nguyên context

class Counter2 {
  constructor() {
    this.count = 0;
  }

  increase = () => {
    this.count++;
    console.log(`Count: ${this.count}`);
  };
}

const c2 = new Counter2();
const fn = c2.increase;
fn(); // Luôn hoạt động đúng vì arrow function không tạo this mới

6. callapply

function say(message) {
  console.log(message, this.name);
}

const user = { name: "Bob" };

// call - truyền tham số dạng liệt kê
say.call(user, "Hi");     // Output: "Hi Bob"

// apply - truyền tham số dạng array
say.apply(user, ["Hello"]); // Output: "Hello Bob"

7. Ví dụ thực tế với callback

class Button {
  constructor(label) {
    this.label = label;
    this.clicked = false;
  }

  // Method thường - dễ mất context
  handleClick() {
    this.clicked = true;
    console.log(`Button "${this.label}" clicked: ${this.clicked}`);
  }

  // Arrow function - giữ context
  handleClickArrow = () => {
    this.clicked = true;
    console.log(`Button "${this.label}" clicked: ${this.clicked}`);
  };
}

const button = new Button("Submit");

// Giả lập event listener
const simulateClick = (handler) => {
  setTimeout(handler, 1000);
};

// Sẽ gây lỗi vì mất context
// simulateClick(button.handleClick);

// Hoạt động đúng với bind
simulateClick(button.handleClick.bind(button));

// Hoạt động đúng với arrow function
simulateClick(button.handleClickArrow);

Kiến thức trọng tâm

1. this phụ thuộc vào cách hàm được gọi, không phải nơi hàm được khai báo

Đây là nguyên nhân chính gây lỗi “this is undefined”. Trong JavaScript, giá trị của this được xác định tại thời điểm gọi hàm, không phải khi khai báo hàm.

2. Dùng bind, call, apply để điều khiển context

  • bind → Tạo hàm mới với context cố định, không gọi hàm ngay lập tức
  • call → Gọi hàm với context và tham số dạng liệt kê
  • apply → Gọi hàm với context và tham số dạng array

3. Arrow function không tạo this mới

Luôn dùng this của nơi nó được khai báo → giải pháp an toàn trong class, callback, event handler. Đây là cách hiện đại và được khuyến nghị để tránh lỗi về context.

4. TypeScript giúp phát hiện lỗi this

// TypeScript có thể phát hiện lỗi this nếu bật strict mode
class SafeCounter {
  count = 0;

  increase(this: SafeCounter) {
    this.count++;
  }
}

const counter = new SafeCounter();
const method = counter.increase;
// method(); // TypeScript sẽ báo lỗi compile-time

Bài tập thực hành

Bài tập 1: Object và method

// Tạo object car có method start() in ra this.brand
const car = {
  brand: "Toyota",
  start() {
    console.log(`Starting ${this.brand}...`);
  }
};

// Tách method ra và sửa bằng bind
const startMethod = car.start;
const boundStart = startMethod.bind(car);
boundStart(); // Output: "Starting Toyota..."

Bài tập 2: Class với async method

// Tạo class Timer có method tick()
class Timer {
  constructor(name) {
    this.name = name;
    this.ticks = 0;
  }

  tick() {
    this.ticks++;
    console.log(`${this.name} tick: ${this.ticks}`);
  }
}

const timer = new Timer("MyTimer");

// Gọi tick trong setTimeout và sửa lỗi mất context
// Cách 1: Dùng bind
setTimeout(timer.tick.bind(timer), 1000);

// Cách 2: Dùng arrow function
setTimeout(() => timer.tick(), 1000);

// Cách 3: Chuyển method thành arrow function trong class
class BetterTimer {
  constructor(name) {
    this.name = name;
    this.ticks = 0;
  }

  tick = () => {
    this.ticks++;
    console.log(`${this.name} tick: ${this.ticks}`);
  };
}

Bài tập 3: Hàm generic với call

// Viết hàm introduce nhận message, dùng call để in ra message + this.name
function introduce(message) {
  console.log(`${message}, I'm ${this.name}!`);
}

const person = { name: "Alice" };
const employee = { name: "Bob" };

// Sử dụng call
introduce.call(person, "Hello");     // Output: "Hello, I'm Alice!"
introduce.call(employee, "Welcome"); // Output: "Welcome, I'm Bob!"

// Sử dụng apply với array
introduce.apply(person, ["Hi there"]); // Output: "Hi there, I'm Alice!"

Sai lầm thường gặp

3. Quên bind khi truyền method làm callback

// ❌ Sai: Mất context
button.addEventListener('click', obj.handleClick);

// ✅ Đúng: Giữ context với bind
button.addEventListener('click', obj.handleClick.bind(obj));

// ✅ Đúng: Dùng arrow function
button.addEventListener('click', () => obj.handleClick());

4. Nhầm lẫn giữa method thường và arrow function

class MyClass {
  // ❌ Có thể mất context nếu không bind
  handleEvent() {
    console.log(this);
  }

  // ✅ Luôn giữ đúng context
  handleEvent = () => {
    console.log(this);
  };
}

3. Sử dụng this trong nested function

const obj = {
  name: "Test",
  method() {
    // ❌ this trong function thường sẽ mất context
    setTimeout(function() {
      console.log(this.name); // undefined
    }, 0);

    // ✅ Dùng arrow function để giữ context
    setTimeout(() => {
      console.log(this.name); // "Test"
    }, 0);
  }
};

Kết luận

Hiểu đúng về this, cùng với bind và arrow function, giúp bạn tránh được nhiều lỗi khó chịu trong ứng dụng thực tế. Đây là kỹ năng nền tảng quan trọng khi làm việc với:

  • Class và OOP: Đảm bảo method hoạt động đúng khi được gọi từ nhiều nơi
  • Event handler: Xử lý sự kiện không bị mất context
  • Callback và async: Tránh lỗi bất đồng bộ
  • Functional programming: Kết hợp với các pattern như currying, partial application

Với TypeScript, bạn có thêm lớp bảo vệ compile-time giúp phát hiện lỗi this sớm, làm cho code trở nên robust và dễ bảo trì hơn.

17 bài học
Bài 12
Tiến độ hoàn thành 71%

Đã hoàn thành 12/17 bài học