Cách sử dụng từ khóa this trong JavaScript

Javascript căn bản | by Học Javascript

Trong JavaScript, từ khóa this là một trong những khái niệm quan trọng nhưng cũng gây nhiều nhầm lẫn cho người mới học. this không cố định mà thay đổi tùy thuộc vào ngữ cảnh thực thi của nó. Khi làm việc với các hàm, đối tượng, class, hay thậm chí trong các sự kiện DOM, this có thể tham chiếu đến những đối tượng khác nhau. Hiểu rõ cách hoạt động của this giúp lập trình viên viết mã hiệu quả hơn, tránh được các lỗi phổ biến và tận dụng tối đa sức mạnh của JavaScript. Trong bài viết này, mình sẽ cùng tìm hiểu chi tiết về cách sử dụng this trong từng ngữ cảnh khác nhau, đồng thời chỉ ra những lỗi thường gặp và cách khắc phục.

Giới thiệu về từ khóa this trong JavaScript

Trong JavaScript, this là một từ khóa đặc biệt dùng để tham chiếu đến một đối tượng nhất định trong ngữ cảnh thực thi của nó. Giá trị của this không cố định mà thay đổi tùy thuộc vào cách và nơi mà nó được sử dụng.

Tóm lại, this là một biến đặc biệt được tự động gán giá trị khi một hàm hoặc phương thức được gọi, và giá trị của nó sẽ thay đổi tùy theo ngữ cảnh thực thi.

Ví dụ cơ bản:

console.log(this); // Trong môi trường trình duyệt, this trỏ đến window

this tham chiếu đến đối tượng nào phụ thuộc vào ngữ cảnh thực thi

JavaScript xác định giá trị của this dựa trên cách một hàm được gọi. Dưới đây là một số ngữ cảnh phổ biến mà this có thể tham chiếu đến những đối tượng khác nhau:

Trong phạm vi toàn cục (Global Context)

  • Trong môi trường trình duyệt, this trỏ đến đối tượng window.
  • Trong chế độ strict mode, this sẽ là undefined.

Ví dụ:

console.log(this); // Trong trình duyệt, in ra Window object

"use strict";
function test() {
    console.log(this); // undefined trong strict mode
}
test();

Trong một phương thức của đối tượng (Object Method Context)

  • Khi this được gọi bên trong một phương thức của một đối tượng, nó tham chiếu đến chính đối tượng đó.

Ví dụ:

const person = {
    name: "John",
    greet: function() {
        console.log(this.name); // this tham chiếu đến object person
    }
};
person.greet(); // Output: "John"

Trong một hàm thông thường (Function Context)

  • Khi một hàm được gọi một cách bình thường (không phải là phương thức của một đối tượng), this mặc định tham chiếu đến window (hoặc undefined trong strict mode).

Ví dụ:

function showThis() {
    console.log(this); // Trong trình duyệt: window, trong strict mode: undefined
}
showThis();

Trong một hàm khởi tạo (Constructor Function Context)

  • Khi sử dụng new để tạo một đối tượng từ một hàm constructor, this trỏ đến đối tượng mới được tạo.

Ví dụ:

function Person(name) {
    this.name = name;
}
const user = new Person("Alice");
console.log(user.name); // Output: "Alice"

Trong class (ES6 Class Context)

  • Trong một lớp (class), this tham chiếu đến thể hiện của lớp.

Ví dụ:

class User {
    constructor(name) {
        this.name = name;
    }

    greet() {
        console.log(`Hello, my name is ${this.name}`);
    }
}

const user1 = new User("Bob");
user1.greet(); // Output: "Hello, my name is Bob"

Trong arrow function (Arrow Function Context)

  • this trong arrow function không có ngữ cảnh riêng, nó kế thừa this từ phạm vi bên ngoài.

Ví dụ:

const person = {
    name: "John",
    greet: () => {
        console.log(this.name); // this không tham chiếu đến person mà kế thừa từ phạm vi bên ngoài (window)
    }
};
person.greet(); // Output: undefined (hoặc lỗi trong strict mode)

Trong sự kiện DOM (Event Context)

  • Trong trình nghe sự kiện (event listener), this trỏ đến phần tử DOM mà sự kiện được gán.

Ví dụ:

document.querySelector("button").addEventListener("click", function() {
    console.log(this); // this tham chiếu đến button được click
});

Khi sử dụng call(), apply(), bind()

  • call(), apply()bind() giúp kiểm soát giá trị của this.

Ví dụ:

function sayHello() {
    console.log(this.name);
}

const user = { name: "Alice" };
sayHello.call(user); // Output: "Alice"

Sự khác biệt giữa this trong JavaScript so với các ngôn ngữ lập trình khác

Trong nhiều ngôn ngữ lập trình như Java, C++, this luôn tham chiếu đến đối tượng hiện tại của class, nhưng trong JavaScript, giá trị của this thay đổi tùy vào cách một hàm được gọi.

Khác biệt chính:

  • Không cố định: this trong JavaScript có thể tham chiếu đến các đối tượng khác nhau dựa vào cách hàm được gọi.
  • Phụ thuộc vào ngữ cảnh: Không giống như các ngôn ngữ khác, nơi this luôn tham chiếu đến instance của class, trong JavaScript, this có thể là window, một object cụ thể, hoặc undefined.
  • Arrow function không có this riêng: Trong JavaScript, arrow function kế thừa this từ phạm vi bao quanh, điều này khác với cách hoạt động của this trong Java hay C++.

Ví dụ về sự khác biệt giữa JavaScript và Java:

Java:

class Person {
    String name;
    
    Person(String name) {
        this.name = name;
    }
    
    void greet() {
        System.out.println("Hello, my name is " + this.name);
    }
}

Person p = new Person("Alice");
p.greet(); // Output: "Hello, my name is Alice"

JavaScript:

const person = {
    name: "Alice",
    greet: function() {
        console.log("Hello, my name is " + this.name);
    }
};

const greetFunc = person.greet;
greetFunc(); // Output: undefined (hoặc lỗi nếu strict mode)

Trong Java, this luôn tham chiếu đến đối tượng đang gọi phương thức. Nhưng trong JavaScript, nếu ta tách phương thức greet() ra như trên, this sẽ bị mất ngữ cảnh và không còn tham chiếu đến person.

Các ngữ cảnh sử dụng từ khóa this trong JavaScript

this trong phạm vi toàn cục (Global Context)

Trong môi trường trình duyệt (this === window)

Khi sử dụng this trong phạm vi toàn cục của JavaScript, nó sẽ tham chiếu đến đối tượng window (đối với môi trường trình duyệt).

Ví dụ:

console.log(this); // Trong trình duyệt, this tham chiếu đến đối tượng window

function showThis() {
    console.log(this); // Cũng tham chiếu đến window
}
showThis();

Giải thích:

  • Khi chạy trong môi trường toàn cục (không nằm trong một hàm hay đối tượng cụ thể), this sẽ tham chiếu đến window.
  • Khi gọi một hàm theo cách thông thường, nếu không ở chế độ strict mode, this vẫn là window.

Trong môi trường Node.js (this === global)

Trong môi trường Node.js, this trong phạm vi toàn cục sẽ tham chiếu đến global.

Ví dụ:

console.log(this === global); // true trong Node.js

this trong một đối tượng (Object Context)

Khi this được sử dụng trong một phương thức của một đối tượng, nó sẽ tham chiếu đến chính đối tượng đó.

Ví dụ:

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

person.greet(); // Output: "Hello, Alice"

Giải thích:

  • Ở đây, this.name sẽ tham chiếu đến person.name, vì this trong phương thức greet() tham chiếu đến đối tượng person.

this trong hàm thông thường (Function Context)

Nếu this được sử dụng trong một hàm thông thường (không nằm trong một đối tượng), nó sẽ có các hành vi sau:

  • Chế độ mặc định: this tham chiếu đến window.
  • Chế độ strict mode: thisundefined.

Ví dụ:

function showThis() {
    console.log(this);
}
showThis(); // Trong trình duyệt, output: Window

"use strict";
function showStrictThis() {
    console.log(this);
}
showStrictThis(); // Output: undefined

Giải thích:

  • Khi không ở chế độ strict mode, this trong hàm bình thường tham chiếu đến window.
  • Khi sử dụng "use strict", this trong hàm thông thường sẽ là undefined để tránh lỗi vô tình thao tác trên đối tượng toàn cục.

this trong Constructor Function

Khi sử dụng một hàm constructor với từ khóa new, this sẽ tham chiếu đến đối tượng mới được tạo.

Ví dụ:

function Person(name) {
    this.name = name;
}

const user = new Person("Alice");
console.log(user.name); // Output: "Alice"

Giải thích:

  • Khi new Person("Alice") được gọi, một đối tượng mới được tạo và this bên trong hàm constructor tham chiếu đến đối tượng mới này.

this trong Class (ES6 Class)

Trong ES6, khi this được sử dụng bên trong một phương thức của class, nó tham chiếu đến thể hiện của lớp đó.

Ví dụ:

class User {
    constructor(name) {
        this.name = name;
    }

    greet() {
        console.log(`Hello, my name is ${this.name}`);
    }
}

const user1 = new User("Bob");
user1.greet(); // Output: "Hello, my name is Bob"

Sự khác biệt giữa this trong class và object literal:

  • Trong class, this tham chiếu đến instance của class.
  • Trong object literal, this tham chiếu đến chính object đó.

this trong Arrow Function

Arrow function không có this riêng, nó kế thừa this từ phạm vi bao quanh.

Ví dụ:

const person = {
    name: "John",
    greet: () => {
        console.log(this.name); // this không tham chiếu đến person mà kế thừa từ phạm vi bao ngoài (window)
    }
};
person.greet(); // Output: undefined

So sánh this trong arrow function và function thông thường:

const person = {
    name: "John",
    greet1: function() {
        console.log(this.name);
    },
    greet2: () => {
        console.log(this.name);
    }
};

person.greet1(); // Output: "John"
person.greet2(); // Output: undefined

Giải thích:

  • greet1() là một function bình thường nên this tham chiếu đến person.
  • greet2() là một arrow function nên this kế thừa từ phạm vi bao ngoài (window), không phải từ person.

this trong sự kiện (Event Listeners)

Khi this được sử dụng trong một sự kiện DOM, nó tham chiếu đến phần tử mà sự kiện được gán.

Ví dụ:

document.querySelector("button").addEventListener("click", function() {
    console.log(this); // this tham chiếu đến button được click
});

Giải thích:

  • Trong trình nghe sự kiện, this tham chiếu đến phần tử mà sự kiện được kích hoạt.

this khi sử dụng call(), apply() và bind()

call()apply()

Dùng để gọi một hàm với this được xác định rõ ràng.

Ví dụ:

function sayHello() {
    console.log(this.name);
}

const user = { name: "Alice" };

sayHello.call(user); // Output: "Alice"
sayHello.apply(user); // Output: "Alice"

Giải thích:

  • call()apply() giúp ta chỉ định this khi gọi một hàm.

bind()

Tạo một hàm mới với this cố định.

Ví dụ:

const person = {
    name: "Alice",
    greet: function() {
        console.log(`Hello, ${this.name}`);
    }
};

const greetFunc = person.greet.bind(person);
greetFunc(); // Output: "Hello, Alice"
  • bind() tạo ra một bản sao của hàm greet nhưng với this được cố định về person.\

Các lỗi thường gặp với this và cách khắc phục trong Javascript

Trong JavaScript, this không phải lúc nào cũng hoạt động theo cách chúng ta mong đợi, đặc biệt khi sử dụng trong các hàm callback, event handler hoặc khi truyền hàm làm tham số. Dưới đây là một số lỗi phổ biến và cách khắc phục chúng.

Khi this không như mong đợi

Một trong những lỗi phổ biến nhất là khi this không trỏ đến đối tượng mong muốn. Điều này xảy ra khi this trong một hàm không phải là đối tượng chứa nó mà là window hoặc undefined.

Ví dụ lỗi

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

const greetFunc = person.greet;
greetFunc(); // Output: undefined (hoặc "Hello, undefined" trong trình duyệt)

Giải thích:

  • Khi gán person.greet vào greetFunc, this trong greetFunc() không còn trỏ đến person, mà trở thành window (hoặc undefined trong strict mode).

Cách khắc phục:

Sử dụng .bind() để cố định this

const greetFuncBound = person.greet.bind(person);
greetFuncBound(); // Output: "Hello, Alice"

Sử dụng arrow function (trong trường hợp cần truyền hàm)

const greetFuncArrow = () => person.greet();
greetFuncArrow(); // Output: "Hello, Alice"

Lỗi khi sử dụng this trong callback

Ví dụ lỗi

const person = {
    name: "Alice",
    greet: function() {
        setTimeout(function() {
            console.log("Hello, " + this.name);
        }, 1000);
    }
};

person.greet(); // Output: "Hello, undefined"

Giải thích:

  • setTimeout() chạy một hàm thông thường (không phải phương thức của person), nên this bên trong hàm này không tham chiếu đến person mà thay vào đó là window hoặc undefined (trong strict mode).

Cách khắc phục:

Dùng biến trung gian để lưu giá trị của this

const person = {
    name: "Alice",
    greet: function() {
        const self = this; // Lưu lại giá trị của this
        setTimeout(function() {
            console.log("Hello, " + self.name);
        }, 1000);
    }
};

person.greet(); // Output: "Hello, Alice"
Sử dụng arrow function (cách phổ biến hơn)
Arrow function không có this riêng mà kế thừa this từ phạm vi cha.
const person = {
    name: "Alice",
    greet: function() {
        setTimeout(() => {
            console.log("Hello, " + this.name);
        }, 1000);
    }
};

person.greet(); // Output: "Hello, Alice"

Lỗi khi sử dụng this trong vòng lặp hoặc phương thức mảng

Ví dụ lỗi

const team = {
    members: ["Alice", "Bob"],
    showMembers: function() {
        this.members.forEach(function(member) {
            console.log(this.name + " welcomes " + member);
        });
    }
};

team.showMembers(); 
// Output:
// "undefined welcomes Alice"
// "undefined welcomes Bob"

Giải thích:

  • Trong forEach(), this không tham chiếu đến team mà là window hoặc undefined.

Cách khắc phục:

Dùng .bind() để cố định this

const team = {
    members: ["Alice", "Bob"],
    name: "Team A",
    showMembers: function() {
        this.members.forEach(function(member) {
            console.log(this.name + " welcomes " + member);
        }.bind(this)); // Cố định this
    }
};

team.showMembers();
// Output:
// "Team A welcomes Alice"
// "Team A welcomes Bob"

Sử dụng arrow function

const team = {
    members: ["Alice", "Bob"],
    name: "Team A",
    showMembers: function() {
        this.members.forEach(member => {
            console.log(this.name + " welcomes " + member);
        });
    }
};

team.showMembers();
// Output:
// "Team A welcomes Alice"
// "Team A welcomes Bob"

Lỗi this trong event handler

Ví dụ lỗi

document.querySelector("button").addEventListener("click", function() {
    console.log(this.textContent);
});

const button = {
    text: "Click Me",
    handleClick: function() {
        document.querySelector("button").addEventListener("click", function() {
            console.log(this.text);
        });
    }
};

button.handleClick();
// Output khi click: undefined

Giải thích:

  • this trong event listener không tham chiếu đến button mà tham chiếu đến phần tử DOM (<button>).

Cách khắc phục:

Dùng arrow function để kế thừa this từ đối tượng cha

const button = {
    text: "Click Me",
    handleClick: function() {
        document.querySelector("button").addEventListener("click", () => {
            console.log(this.text);
        });
    }
};

button.handleClick();
// Output khi click: "Click Me"

Dùng .bind(this) để cố định this

const button = {
    text: "Click Me",
    handleClick: function() {
        document.querySelector("button").addEventListener("click", function() {
            console.log(this.text);
        }.bind(this));
    }
};

button.handleClick();
// Output khi click: "Click Me"

Kết bài

Từ khóa this trong JavaScript là một khái niệm quan trọng nhưng cũng dễ gây nhầm lẫn, đặc biệt khi nó có thể tham chiếu đến các đối tượng khác nhau tùy thuộc vào ngữ cảnh thực thi. Việc hiểu rõ cách this hoạt động trong các tình huống như phạm vi toàn cục, trong đối tượng, trong hàm, arrow function, class, sự kiện, cũng như khi sử dụng call(), apply(), bind() sẽ giúp lập trình viên viết mã chính xác hơn, tránh được những lỗi không mong muốn.

Bên cạnh đó, việc sử dụng các phương pháp như .bind(), arrow function hay lưu trữ this vào một biến trung gian có thể giúp khắc phục các lỗi phổ biến khi làm việc với this. Khi nắm vững và áp dụng đúng cách, bạn sẽ có thể viết mã JavaScript hiệu quả, dễ bảo trì và ít lỗi hơn.

Hy vọng bài viết này đã giúp bạn hiểu rõ hơn về this trong JavaScript và cách sử dụng nó một cách linh hoạt trong lập trình!

Bài viết liên quan