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

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

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

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

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

其他範例

呼叫繫結函式時,「無法」透過 callapply 變更 this 的值:

// 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 事件監聽器等因素而設為其他值,因此使用它可能會導致難以理解的程式碼:

錯誤做法
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 內部都使用 this。已變更 console.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」相關資訊。有疑問我錯過了什麼嗎?歡迎透過 Tweet 張貼推文

感謝 Mathias BynensIngvar StepanyanThomas Steiner 審查。