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

上述程式碼會呼叫 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 的物件。

遺憾的是,由於 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 進行評論。