seansie's blog

js中的this到底是甚麼 js隨筆 |seansie blog

this 這個關鍵字在 js 中令許多人困惑之處,尤其是在普通函數中,因為 this 所指向的物件是動態的取決於呼叫方式。

重點來了,”方式”這個詞可不是字面意義上的”方式”,像我就曾經誤解這個詞,不僅有一些js對this的行規(執行上下文),而且這麼模糊的詞彙容易被各自解讀,如果不是老手來看這個”方式”恐怕是聽君一席話,如聽一席話。畢竟簡單把方式跟專業術語執行上下文當成同個東西只會使理解過於片面。

而本人覺得這個詞非常會讓人誤會,所以我在之後會用其他比較清楚的詞彙來描述這個,像是上下文。

定義

this 指向取決於**被呼叫的函數物件(callable function object)的執行上下文(executing context),**而上下文取決於呼叫方法的類型

先來進行名詞解釋

1.上下文

這個蠻重要的,也是 js 官方推薦的術語,然後後面章節如果有看不懂的建議回來這個”萬物之源”章節來釐清觀念,因為非常重要。

在 js 引擎中,每次呼叫 (也就是() 運算子,詳細見第二點)一個函數物件的時候,就會自動建立一個執行上下文,提供 this 使用,而執行上下文取決於多種因素,看你怎麼呼叫(就是剛剛提到的方法),但是哪些方法對應那些執行上下文只有下面有提到的分類,不要自己腦補出其他類別。

a. 全域呼叫

  • 語法 : 函數物件()

簡單粗暴直接呼叫,那自然而然沒有啥 上下文 可言,不過在非嚴格模式 js 會自動把他補成指向 全域物件

b. 透過物件呼叫

  • 語法 : 物件.函數物件的屬性()

既然是透過 物件 呼叫的,那該函數的上下文自然而然就會指向 物件

注意這裡不要理解成父物件喔,我一開始學的時候就是因為 too young sometimes naive啦,想說把他理解成從屬關係,結果一直卡住(要是這麼簡單怎麼對得起js大魔王關卡的稱號🤣🤣)

其實關鍵在於函數物件的怎麼被呼叫的 形式 (像是a跟b就是不同形式), 而不是父物件之類的從屬關係,藉由其形式(看看前面有沒有物件(那陀東西)阿)就能分辨出到底是 a 跟b了,而 c 跟 d本身很明顯不會搞錯。

c 建構子呼叫

會指向新建立的物件

d. call bind apply (手動改變上下文)

進階用法,因為篇幅,等一下在子文章中會一一介紹。

舉例來說,像是據說 vuejs 的vue元件實體透過 bind 方法把所有旗下諸物件像是data() method 的之類的,全部把他的執行上下文綁在vue實體上,使得初學者使用更方便且清晰

2.被呼叫的函數物件

先定義一下函數物件

在 js 中 呼叫函數是這樣吧 func()

而可能有些人習慣靜態語言(例如我ww)可能會有點困擾,在 js 中因為萬物皆物件,所以其實 func 跟 () 是分開的 。

可以理解成 func 是名詞(函數物件) 而 () 是動詞 (執行)。

而假設 func() 能成功執行,那本文文所指的 被呼叫的函數物件 就是 func ,因為它既能也被呼叫了,也滿足了定義的一部份。

而對於如何區分這四種,對於 1.a 1.b 兩點,他們的執行上下文會取決於 () 左邊的函數物件的長相,其他(1.cd)的特徵蠻明顯的,應該不用介紹。

而他 func()的上下文據1.a.所示是全域物件。

然後若是要呼叫 obj.func(),根據定義 1.b obj() 就是 func() 的上下文, 所以根據定義 this會指向 被呼叫函數物件( func )的 obj 。

1. 全域呼叫

  • **牛刀小試:**JavaScript

    `function testThis() { console.log(this); }

    testThis(); // 在瀏覽器中,這裡的 this 會指向什麼?在 Node.js 環境中呢?`

  • 解析:

    • 在瀏覽器中,非嚴格模式下,this 指向 window 物件。
    • 在 Node.js 環境中,非嚴格模式下,this 指向 global 物件。
    • 在嚴格模式下(‘use strict’),無論是瀏覽器或 Node.js,this 都會是 undefined。

2. 透過物件呼叫

  • **牛刀小試:**JavaScript

    const obj = {
      name: '我是物件',
      testThis: function() {
        console.log(this.name);
      }
    };
    
    obj.testThis(); // 這裡的 this.name 會印出什麼?
    
  • 解析:

    • this.name 會印出 “我是物件”,因為 testThis 是透過 obj 物件呼叫的,this 指向 obj。
  • **進階挑戰:**JavaScript

    const obj = {
      name: '我是物件',
      testThis: function() {
        console.log(this.name);
      }
    };
    
    const anotherFunc = obj.testThis;
    anotherFunc(); // 這裡的 this.name 會印出什麼?為什麼?
    
  • 解析

    • anotherFunc()是屬於全域呼叫,所以this是指向全域物件。
    • anotherFunc變數所指向的函數物件本身在被呼叫的時候,是以全域呼叫,而非透過物件呼叫。

3. 建構子呼叫

  • **牛刀小試:**JavaScript

    function Person(name) {
      this.name = name;
      this.greet = function() {
        console.log('你好,我是 ' + this.name);
      };
    }
    
    const p1 = new Person('小明');
    p1.greet(); // 這裡的 this.name 會印出什麼?
    
  • 解析:

    • this.name 會印出 “小明”,因為 Person 是作為建構函式使用 new 關鍵字呼叫的,this 指向新建立的物件 p1。

4. call, bind, apply (手動改變上下文)

  • **牛刀小試 (call):**JavaScript

    function greet() {
      console.log('你好,我是 ' + this.name);
    }
    
    const person = { name: '小華' };
    greet.call(person); // 這裡會印出什麼?
    
  • 解析:

    • 會印出 “你好,我是 小華”,因為 call 方法將 greet 函數的 this 設定為 person 物件。
  • **牛刀小試 (bind):**JavaScript

    function greet() {
      console.log('你好,我是 ' + this.name);
    }
    
    const person = { name: '小美' };
    const boundGreet = greet.bind(person);
    boundGreet(); // 這裡會印出什麼?
    
  • 解析:

    • 會印出 “你好,我是 小美”,因為 bind 方法創建了一個新函數 boundGreet,並將其 this 永久綁定到 person 物件。
  • **牛刀小試 (apply):**JavaScript

    function introduce(greeting, punctuation) {
        console.log(greeting + ',我是 ' + this.name + punctuation);
    }
    
    const person = { name: '老王' };
    introduce.apply(person, ['哈囉', '!']); // 這裡會印出什麼?
    
    • 解析: 會印出 “哈囉,我是 老王!",因為apply方法的第一個引數是設定this的上下文,而第二個引數是陣列,會將陣列的內容當成參數傳入。