切換語言為:簡體

JavaScript 之 `this` 詳解

  • 爱糖宝
  • 2024-07-28
  • 2055
  • 0
  • 0

每當提到 JavaScript 的 this 關鍵字,許多開發者都會感到頭痛。它似乎總是以出乎意料的方式工作,讓人捉摸不透。但事實上,一旦你理解了 this 的行為,一切都會變得清晰起來。

this的基本概念

this 在 JavaScript 中是一個特殊的關鍵字,它代表的是函式執行上下文中的“當前物件”。然而,this 的指向並不是固定的,它取決於函式是如何被呼叫的。這一點在 ECMAScript 規範中有詳細的定義:

tc39.es/ecma262/#se…

JavaScript 之 `this` 詳解 

並且,如果你不瞭解它的運作機制,有時就會出現一些你認為的莫名其妙的現象。

那麼,這個 this 到底說了什麼內容呢?

說起來也簡單,this 的指向會根據函式的呼叫方式不同而變化。 對於一些常見的函式呼叫模式,我們可以總結如下:

  1. 全域性作用域中的 this

    • 當在全域性作用域中呼叫一個函式時,this 通常指向全域性物件(在瀏覽器中是 window,在 Node.js 中是 global)。

  2. 作為普通函式呼叫時的 this

    • 當一個函式被直接呼叫時(不是作為一個物件的方法),this 通常指向全域性物件(非嚴格模式下)或者 undefined(嚴格模式下)。

  3. 作為物件方法呼叫時的 this

    • 當一個函式作為物件的一個方法被呼叫時,this 通常指向那個物件。

  4. 建構函式中的 this

    • 使用 new 關鍵字呼叫建構函式時,this 指向新建立的物件例項。

  5. 事件處理程式中的 this

    • 當一個函式作為事件處理程式被呼叫時,this 通常指向觸發事件的元素。

  6. 箭頭函式中的 this

    • 箭頭函式不會繫結自己的 this,而是繼承自外圍函式的作用域中的 this

  7. 使用 .call, .apply, .bind 改變 this

    • 可以使用這些方法顯式地設定函式執行時的 this 值。

爲了描述this 的不同繫結方式,程式設計師們達成了共識,給this的繫結也分爲了一下幾類

社羣裡廣泛使用的術語

預設繫結

當函式獨立呼叫時,this指向全域性物件(在瀏覽器環境中為window物件)。

示例:

複製
function sayName() {
  console.log(this.name);
}

var name = '全域性名稱';
sayName(); // 輸出:全域性名稱

注意:當這裏是嚴格模式的時候,會訪問undefined導致TypeError

隱式繫結

當函式作為物件的方法呼叫時,this指向該物件。

var person = {
  name: '張三',
  sayName: function() {
    console.log(this.name);
  }
};

person.sayName(); // 輸出:張三

函式呼叫中的this

當一個函式作為普通函式呼叫時,this的行為取決於上下文:

  • 非嚴格模式下,this指向全域性物件。

  • 嚴格模式下,thisundefined

function greet() {
    console.log(this);
}

greet(); // 輸出: window (非嚴格模式) 或 undefined (嚴格模式)

箭頭函式中的this

箭頭函式不繫結自己的this。它們繼承外部函式或全域性作用域中的this

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

person.greet(); // 輸出: Hello, undefined (因為這裏的this是全域性的)

new繫結this

在建構函式中,this指向新建立的物件例項。

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

const alice = new Person('Alice');
console.log(alice.name); // 輸出: Alice

顯式繫結

透過call、apply和bind方法,可以顯式指定函式的this指向。

示例:

function sayName(age) {
  console.log(this.name + ',年齡:' + age);
}

var person = {
  name: '李四'
};

sayName.call(person, 25); // 輸出:李四,年齡:25

案例

來吧,讓我們從最簡單的案例開始看。

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

sayHello(); // 輸出全域性物件(非嚴格模式下)或者 undefined(嚴格模式下)

請仔細閱讀上面的程式碼,然後你認為 sayHello 函式中的 this 是什麼?

要回答這個問題,我們先要了解全域性作用域的this

在全域性作用域(非嚴格模式下),this指向全域性物件。在瀏覽器環境中,這通常指的是window物件。

console.log(this); // 輸出: window

在嚴格模式下(use strict), 全域性作用域中的this將被設定為undefined

'use strict';
console.log(this); // 輸出: undefined

很明顯, 這裏是普通函式的呼叫,且不在嚴格模式下,所以this指向全域性。我們在瀏覽器環境中執行得到的答案就是window,在node環境指向的是globalThis


接下來我們來看一個稍微複雜一點的例子:

const obj = {
  name: 'Alice',
  greet: function() {
    console.log(`Hello, my name is ${this.name}`);
  }
};

obj.greet(); // 輸出 "Hello, my name is Alice"

請仔細閱讀上面的程式碼,然後你認為 greet 函式中的 this 是什麼?

相信你能夠很自信的回答這個問題,greet 函式中的 this 指向 obj 物件。

這個答案是正確的,但如果我追問你是怎麼得到這個答案的,我猜不瞭解 this 行為的你可能會說,因為它是作為一個物件的方法被呼叫的,所以 this 指向 obj

這裏的情況是函式作為物件的方法被呼叫,因此遵循第3條規則。所以答案是'Hello, my name is Alice'


接下來我們繼續增加複雜度:

const obj = {
  name: 'Alice',
  greet: function() {
    console.log(`Hello, my name is ${this.name}`);
  }
};

const greet = obj.greet;
greet();

這裏我們將 greet 函式賦值給了一個新的變數,並直接呼叫它。你認為 greet 函式中的 this 是什麼? 它依然是作為一個普通函式被呼叫了,遵循第二條,所以如果在瀏覽器環境下列印的仍是windows,但在node環境中會是globalThis


我們再來看看使用 new 關鍵字呼叫建構函式的情況:

function Person(name) {
  this.name = name;
  this.sayHello = function() {
    console.log(`Hello, my name is ${this.name}`);
  };
}

const alice = new Person('Alice');
alice.sayHello(); // 輸出 "Hello, my name is Alice"

這裏我們使用 new 關鍵字建立了一個新的 Person 例項。你認為 sayHello 函式中的 this 是什麼? sayHello 函式中的 this 指向 alice 物件。

這裏的情況是函式作為物件的方法被呼叫,因此遵循第3條規則。此外,alice 物件是在建構函式中建立的,因此遵循第4條規則。


接下來我們看看箭頭函式中的 this 指向:

const obj = {
  name: 'Alice',
  greet: () => {
    console.log(`Hello, my name is ${this.name}`);
  }
};

obj.greet(); // 輸出 "Hello, my name is undefined"

這裏我們使用了箭頭函式。你認為 greet 函式中的 this 是什麼?

greet 函式中的 this 指向全域性物件(非嚴格模式下)或者 undefined(嚴格模式下)。

這裏的情況是箭頭函式,因此遵循第6條規則,它繼承了外圍函式的作用域中的 this

我們再看一個例子

// 定義一個物件
const user = {
    name: 'Alice',
    logName: function() {
        console.log('logName:', this.name); // 1
        
        const innerFunc1 = () => {
            console.log('innerFunc1:', this.name); // 2
        };
        
        const innerFunc2 = () => {
            console.log('innerFunc2:', this.name); // 3
            
            const innerFunc3 = () => {
                console.log('innerFunc3:', this.name); // 4
                
                setTimeout(() => {
                    console.log('setTimeout:', this.name); // 5
                }, 1000);
            };
            
            innerFunc3();
        };
        
        innerFunc1();
        innerFunc2();
    }
};

// 呼叫物件的方法
user.logName();

// 在全域性作用域中定義一個箭頭函式
const globalArrowFunc = () => {
    console.log('globalArrowFunc:', this); // 6
};

globalArrowFunc();

先別看下面,你能獨立分析出這裏所有日誌的內容嗎?

  • 分析

  1. logName: 在物件方法 logName 內部,this 指向 user 物件,因此 this.name 輸出 Alice

  2. innerFunc1: 箭頭函式 innerFunc1 繼承了 logNamethis,因此 this.name 輸出 Alice

  3. innerFunc2: 同樣,箭頭函式 innerFunc2 繼承了 logNamethis,因此 this.name 輸出 Alice

  4. innerFunc3: 箭頭函式 innerFunc3 也繼承了 logNamethis,因此 this.name 輸出 Alice

  5. setTimeout: 即使在 setTimeout 中定義了一個箭頭函式,它依然繼承了 logNamethis,因此 this.name 輸出 Alice

  6. globalArrowFunc: 在全域性作用域中定義的箭頭函式,this 指向全域性物件,因此 this 輸出全域性物件。

你都分析對了嗎?


this指向的優先順序

當多種繫結規則同時存在時,它們的優先順序順序為:new繫結 > 顯式繫結 > 隱式繫結 > 預設繫結。

this指向問題到這裏就大致講完了,感謝大家的閱讀

結論

理解this的指向對於避免常見的JavaScript錯誤至關重要。記住,this的值是在執行時由呼叫上下文決定的,而箭頭函式則會繼承其封閉作用域的this

在開發過程中,確保你清楚每個函式呼叫時this的預期值,並使用適當的工具和技術來控制或修改它的行為。希望這篇文章對你有所幫助。

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.