JavaScript: Ý nghĩa của từ này là gì?

Việc tìm ra giá trị của this có thể khó khăn trong JavaScript, sau đây là cách thực hiện...

Jake Archibald
J Jake Archibald

this của JavaScript là phần lớn của nhiều trò đùa, và đó là bởi vì nó khá phức tạp. Tuy nhiên, tôi thấy các nhà phát triển làm những việc phức tạp hơn và dành riêng cho miền để tránh xử lý this này. Nếu bạn không chắc chắn về this, hy vọng thông tin này giúp ích cho bạn. Đây là hướng dẫn về this của tôi.

Tôi sẽ bắt đầu với tình huống cụ thể nhất và kết thúc với tình huống cụ thể nhất. Bài viết này khá giống một if (…) … else if () … else if (…) … lớn, vì vậy bạn có thể chuyển thẳng đến phần đầu tiên khớp với mã bạn đang xem.

  1. Nếu hàm đó được xác định là hàm mũi tên
  2. Nếu không, nếu hàm/lớp được gọi bằng new
  3. Nếu không, nếu hàm có giá trị this "giới hạn"
  4. Nếu không, nếu bạn đặt this tại thời điểm gọi
  5. Nếu không, hàm được gọi thông qua đối tượng mẹ (parent.func())
  6. Nếu không, nếu hàm hoặc phạm vi mẹ ở chế độ nghiêm ngặt
  7. Nếu không

Nếu hàm đó được xác định là hàm mũi tên:

const arrowFunction = () => {
  console.log(this);
};

Trong trường hợp này, giá trị của this luôn giống với this trong phạm vi mẹ:

const outerThis = this;

const arrowFunction = () => {
  // Always logs `true`:
  console.log(this === outerThis);
};

Các hàm mũi tên rất hữu ích vì không thể thay đổi giá trị bên trong của this, giá trị này luôn giống với this bên ngoài.

Ví dụ khác

Đối với các hàm mũi tên, bạn không thể thay đổi giá trị của this bằng bind:

// Logs `true` - bound `this` value is ignored:
arrowFunction.bind({foo: 'bar'})();

Đối với các hàm mũi tên, bạn không thể thay đổi giá trị của this bằng call hoặc apply:

// Logs `true` - called `this` value is ignored:
arrowFunction.call({foo: 'bar'});
// Logs `true` - applied `this` value is ignored:
arrowFunction.apply({foo: 'bar'});

Với các hàm mũi tên, bạn không thể thay đổi giá trị của this bằng cách gọi hàm đó dưới dạng thành phần của một đối tượng khác:

const obj = {arrowFunction};
// Logs `true` - parent object is ignored:
obj.arrowFunction();

Với các hàm mũi tên, bạn không thể thay đổi giá trị của this bằng cách gọi hàm đó dưới dạng một hàm khởi tạo:

// TypeError: arrowFunction is not a constructor
new arrowFunction();

Phương thức thực thể "Bound"

Với phương thức thực thể, nếu bạn muốn đảm bảo this luôn tham chiếu đến thực thể lớp, cách tốt nhất là sử dụng các hàm mũi tên và trường lớp:

class Whatever {
  someMethod = () => {
    // Always the instance of Whatever:
    console.log(this);
  };
}

Mẫu này thực sự hữu ích khi sử dụng phương thức thực thể làm trình nghe sự kiện trong các thành phần (chẳng hạn như thành phần Phản ứng hoặc thành phần web).

Những điều nói trên có thể khiến nó phá vỡ quy tắc "this sẽ giống như this trong phạm vi thành phần mẹ", nhưng sẽ hợp lý nếu bạn coi các trường lớp là cú pháp dễ dàng cài đặt mọi thứ trong hàm khởi tạo:

class Whatever {
  someMethod = (() => {
    const outerThis = this;
    return () => {
      // Always logs `true`:
      console.log(this === outerThis);
    };
  })();
}

// …is roughly equivalent to:

class Whatever {
  constructor() {
    const outerThis = this;
    this.someMethod = () => {
      // Always logs `true`:
      console.log(this === outerThis);
    };
  }
}

Các lớp phụ thay thế liên quan đến việc liên kết một hàm hiện có trong hàm khởi tạo hoặc chỉ định hàm đó trong hàm khởi tạo. Nếu bạn không thể sử dụng các trường lớp vì lý do nào đó, thì việc chỉ định hàm trong hàm khởi tạo là một cách hợp lý:

class Whatever {
  constructor() {
    this.someMethod = () => {
      // …
    };
  }
}

Ngược lại, nếu hàm/lớp được gọi bằng new:

new Whatever();

Thao tác ở trên sẽ gọi Whatever (hoặc hàm khởi tạo của hàm đó nếu là một lớp) với this được đặt thành kết quả của Object.create(Whatever.prototype).

class MyClass {
  constructor() {
    console.log(
      this.constructor === Object.create(MyClass.prototype).constructor,
    );
  }
}

// Logs `true`:
new MyClass();

Điều này cũng đúng với các hàm khởi tạo kiểu cũ:

function MyClass() {
  console.log(
    this.constructor === Object.create(MyClass.prototype).constructor,
  );
}

// Logs `true`:
new MyClass();

Ví dụ khác

Khi được gọi bằng new, bạn không thể thay đổi giá trị của this bằng bind:

const BoundMyClass = MyClass.bind({foo: 'bar'});
// Logs `true` - bound `this` value is ignored:
new BoundMyClass();

Khi được gọi bằng new, bạn không thể thay đổi giá trị của this bằng cách gọi hàm làm thành phần của một đối tượng khác:

const obj = {MyClass};
// Logs `true` - parent object is ignored:
new obj.MyClass();

Ngược lại, nếu hàm có giá trị this "giới hạn" thì:

function someFunction() {
  return this;
}

const boundObject = {hello: 'world'};
const boundFunction = someFunction.bind(boundObject);

Bất cứ khi nào boundFunction được gọi, giá trị this của thuộc tính này sẽ là đối tượng được truyền đến bind (boundObject).

// Logs `false`:
console.log(someFunction() === boundObject);
// Logs `true`:
console.log(boundFunction() === boundObject);

Ví dụ khác

Khi gọi một hàm ràng buộc, bạn không thể thay đổi giá trị của this bằng call hoặc apply:

// Logs `true` - called `this` value is ignored:
console.log(boundFunction.call({foo: 'bar'}) === boundObject);
// Logs `true` - applied `this` value is ignored:
console.log(boundFunction.apply({foo: 'bar'}) === boundObject);

Khi gọi một hàm ràng buộc, bạn không thể thay đổi giá trị của this bằng cách gọi hàm đó dưới dạng thành phần của một đối tượng khác:

const obj = {boundFunction};
// Logs `true` - parent object is ignored:
console.log(obj.boundFunction() === boundObject);

Ngược lại, nếu this được đặt vào thời gian gọi:

function someFunction() {
  return this;
}

const someObject = {hello: 'world'};

// Logs `true`:
console.log(someFunction.call(someObject) === someObject);
// Logs `true`:
console.log(someFunction.apply(someObject) === someObject);

Giá trị của this là đối tượng được truyền đến call/apply.

Rất tiếc, this được đặt thành một số giá trị khác bởi những công cụ như trình nghe sự kiện DOM và việc sử dụng giá trị đó có thể dẫn đến mã khó hiểu:

Không nên
element.addEventListener('click', function (event) {
  // Logs `element`, since the DOM spec sets `this` to
  // the element the handler is attached to.
  console.log(this);
});

Tôi tránh sử dụng this trong các trường hợp như trên mà thay vào đó:

Nên
element.addEventListener('click', (event) => {
  // Ideally, grab it from a parent scope:
  console.log(element);
  // But if you can't do that, get it from the event object:
  console.log(event.currentTarget);
});

Ngược lại, nếu hàm được gọi qua đối tượng mẹ (parent.func()):

const obj = {
  someMethod() {
    return this;
  },
};

// Logs `true`:
console.log(obj.someMethod() === obj);

Trong trường hợp này, hàm được gọi như một thành phần của obj, do đó this sẽ là obj. Điều này xảy ra tại thời điểm gọi, vì vậy, đường liên kết sẽ bị hỏng nếu hàm được gọi mà không có đối tượng mẹ hoặc qua một đối tượng mẹ khác:

const {someMethod} = obj;
// Logs `false`:
console.log(someMethod() === obj);

const anotherObj = {someMethod};
// Logs `false`:
console.log(anotherObj.someMethod() === obj);
// Logs `true`:
console.log(anotherObj.someMethod() === anotherObj);

someMethod() === obj sai vì someMethod không được gọi với tư cách là thành viên của obj. Bạn có thể gặp phải vấn đề này khi thử những việc như sau:

const $ = document.querySelector;
// TypeError: Illegal invocation
const el = $('.some-element');

Điều này bị lỗi vì quá trình triển khai querySelector xem xét giá trị this của riêng nó và kỳ vọng nó là một nút DOM thuộc loại, và ở trên làm hỏng kết nối đó. Để đạt được những mục trên một cách chính xác:

const $ = document.querySelector.bind(document);
// Or:
const $ = (...args) => document.querySelector(...args);

Thông tin thú vị: Không phải API nào cũng sử dụng this nội bộ. Các phương thức bảng điều khiển như console.log đã được thay đổi để tránh các tham chiếu this, vì vậy, log không cần phải liên kết với console.

Ngược lại, nếu hàm hoặc phạm vi mẹ ở chế độ nghiêm ngặt:

function someFunction() {
  'use strict';
  return this;
}

// Logs `true`:
console.log(someFunction() === undefined);

Trong trường hợp này, giá trị của this là không xác định. Bạn không cần có 'use strict' trong hàm nếu phạm vi của thành phần mẹ đang ở chế độ nghiêm ngặt (và tất cả các mô-đun đều ở chế độ nghiêm ngặt).

Nếu không thì hãy làm như sau:

function someFunction() {
  return this;
}

// Logs `true`:
console.log(someFunction() === globalThis);

Trong trường hợp này, giá trị của this sẽ giống với globalThis.

Chà!

Chỉ vậy thôi! Đó là mọi thông tin tôi biết về this. Bạn có câu hỏi? Thông tin nào đó tôi đã bỏ lỡ? Cứ thoải mái tweet với tôi.

Nhờ Mathias Bynens, Ingvar StepanyanThomas Steiner đã đánh giá.