JavaScript:這是什麼意思?

在 JavaScript 中找出 this 的值可能會很棘手,以下是如何操作:

Jake Archibald
Jake Archibald

JavaScript 的 this 經常成為笑柄,原因在於它實在太複雜。不過,我曾見過開發人員為了避免處理這個 this,而採取更複雜的做法,如果您不確定 this 的用途,希望以下說明對您有所幫助。這是我的 this 指南。

我會從最具體的情況開始,然後結束最不具體的情況。本文有點像是大型 if (…) … else if () … else if (…) …,您可以直接前往與所查看程式碼相符的第一個部分。

  1. 如果函式定義為箭頭函式
  2. 如果函式/類別是使用 new 呼叫
  3. 否則,如果函式具有「已繫結」this
  4. 否則,如果 this 是在呼叫時設定
  5. 如果函式是透過父項物件 (parent.func()) 呼叫
  6. 否則,如果函式或父項範圍處於嚴格模式
  7. 其他情況

如果函式定義為箭頭函式:

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

在這種情況下,this 的值「一律」會與父範圍中的 this 相同:

const outerThis = this;

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

箭頭函式非常實用,因為 this 的內部值無法變更,總是與外部 this 相同。

其他範例

使用箭頭函式時,無法使用 bind 變更 this 的值:

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

使用箭頭函式時,this 的值無法使用 callapply 變更:

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

使用箭頭函式時,如果以其他物件的成員身分呼叫函式,this 的值就無法變更:

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

使用箭頭函式時,您可以呼叫函式做為建構函式,但無法變更 this 的值:

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

「Bound」例項方法

使用例項方法時,如果您想確保 this 始終參照類別例項,最佳做法是使用箭頭函式和類別欄位

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

在元件 (例如 React 元件或網頁元件) 中使用例項方法做為事件監聽器時,這個模式非常實用。

上述做法可能會違反「this 會與父級範圍中的 this 相同」規則,但如果您將類別欄位視為在建構函式中設定內容的語法糖衣,就會開始覺得有道理:

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);
    };
  }
}

其他模式包括在建構函式中繫結現有函式,或在建構函式中指派函式。如果您因為某些原因無法使用類別欄位,在建構函式中指派函式是合理的替代做法:

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

否則,如果函式/類別是使用 new 呼叫,則:

new Whatever();

上述程式碼會呼叫 Whatever (或其建構函式,如果是類別的話),並將 this 設為 Object.create(Whatever.prototype) 的結果。

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

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

舊版建構函式也是如此:

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

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

其他範例

使用 new 呼叫時,this 的值無法使用 bind 變更:

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

使用 new 呼叫時,如果以其他物件的成員身分呼叫函式,this 的值就無法變更:

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

否則,如果函式含有「已繫結」this 值:

function someFunction() {
  return this;
}

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

每次呼叫 boundFunction 時,其 this 值都會是傳遞至 bind (boundObject) 的物件。

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

其他範例

呼叫已繫結的函式時,this 的值無法使用 callapply 變更:

// 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);

呼叫已繫結的函式時,如果以其他物件的成員身分呼叫函式,this 的值就無法變更:

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

否則,如果 this 是在呼叫時設定:

function someFunction() {
  return this;
}

const someObject = {hello: 'world'};

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

this 的值是傳遞至 call/apply 的物件。

不幸的是,this 會由 DOM 事件監聽器等項目設為其他值,因此使用 this 可能會導致難以理解的程式碼:

錯誤做法
element.addEventListener('click', function (event) {
  // Logs `element`, since the DOM spec sets `this` to
  // the element the handler is attached to.
  console.log(this);
});

在上述情況下,我會避免使用 this,而是改為:

正確做法
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);
});

否則,如果函式是透過父項物件 (parent.func()) 呼叫,則:

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

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

在這種情況下,函式會以 obj 的成員身分呼叫,因此 this 會是 obj。這會在呼叫時發生,因此如果函式在沒有父項物件或使用不同父項物件時呼叫,連結就會中斷:

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 為 false,因為 someMethod 並未obj 成員身分呼叫。您可能在嘗試以下操作時遇到這個問題:

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

這是因為 querySelector 的實作會查看自己的 this 值,並預期該值為某種 DOM 節點,而上述做法會中斷該連線。如要正確達成上述目標,請按照下列步驟操作:

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

有趣的事實:並非所有 API 在內部都使用 thisconsole.log 等控制台方法已變更,以避免 this 參照,因此 log 不必繫結至 console

否則,如果函式或父項範圍處於嚴格模式,則:

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

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

在這種情況下,this 的值為未定義。如果父範圍處於嚴格模式 (且所有模組都處於嚴格模式),則函式中不需要 'use strict'

如果是其他情況:

function someFunction() {
  return this;
}

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

在這種情況下,this 的值與 globalThis 相同。

呼!

大功告成!以上就是我對 this 的瞭解。有疑問我漏掉什麼了嗎?歡迎在 Twitter 上向我發文

感謝 Mathias BynensIngvar StepanyanThomas Steiner 審查。