JS 中的函式 this 指向總結

致于数据科学家的小陈發表於2024-04-06

這個 js 語言中的 this 和其他物件導向的語言有本質的不同, 也更復雜, 它更多取決於函式在不同場景下的呼叫方式, 要理解它並總結出它的規律的話, 優先要從上下文 這個概念認知說起.

理解上下文

上下文 context 可理解為程式執行時的背景環境, 包含了在特定時刻程式所需要的所有資訊. 包括變數的值, 函式的呼叫情況, 執行的位置等.

上下文的核心應用場景在於, 程式狀態管理, 函式呼叫, 記憶體管理, 異常處理等. 本篇這裡是以 JS 程式語言層面的上下文 this 指向總結, 就更多是一種規則梳理.

  • 函式中可以使用 this 關鍵字, 它表示函式的上下文
  • 與中文中的 類似, 函式中 this 指向必須透過 呼叫函式時 的 "前言後語" 來判斷
  • 如果函式不呼叫, 則不能確定函式的上下文

規則01: 物件.方法(), this 指向該打點的物件

當出現 物件.方法() 的場景時, 方法裡面的 this 就是這個物件.

// case 01
function fn() {
  console.log(this.a + this.b);
}

var obj = {
  a: 100,
  b: 200,
  fn: fn 
}

obj.fn()  // this 指向 obj

這裡便是構成了 obj.fn() 的形式, 此時物件的方法 fn 中的 this 則指向該物件 obj 則最後輸出 300.

// case 02
var obj1 = {
  a: 1,
  b: 2,
  fn: function () {
    console.log(this.a + this.b);
  }
}

var obj2 = {
  a: 3,
  b: 4,
  fn: obj1.fn 
}

obj2.fn() // this 指向 obj2

首先要注意對於 obj1 來說, 裡面的 this 在方法沒有被呼叫的時候, 是不知道具體指向的.

然後分析 obj2.fn() 是符合物件.方法()1的, 雖然這裡的 fnobj1 的 fnthis 仍指向 obj1 則最後輸出 7.

// case 03
function outer() {
  // 這裡的 a, b 是內部變數, 其實是個干擾項
  var a = 1
  var b = 2
  // 外層函式返回一個物件
  return {
    a: 3,
    b: 4,
    fn: function () {
      console.log(this.a + this.b);
    }
  }
}

outer().fn() // this 指向 outer() 返回的物件

分析呼叫可知 outer() 返回的是一個物件, 物件再呼叫其方法 fn 所以還是適用於 物件.方法() 的形式, 因此這裡的 this 便是指向其返回的物件, 則輸出 7.

// case 04
function fn() {
  console.log(this.a + this.b);
}

var obj = {
  a: 1,
  b: 2,
  c: [{
    a: 3,
    b: 4,
    c: fn
  }]
}

var a = 5  // a 是全域性變數
obj.c[0].c() // this 指向 c 裡面的物件

分析呼叫可知, obj.c[0] 是一個物件, 然後再呼叫裡面的 fn 方法, 則還是構成了 物件.方法() 形式, 則裡面的 this 指向 c 裡面的物件, 這個全域性的 5 沒有啥關係, 則最後輸出 7.

規則02: 圓括號直接呼叫函式(), this 指向 window 物件

當出現普通 函式() 的場景時, 函式里面的 this 在瀏覽器中指向 window物件

nodejs 中則指向空物件 {} 本篇的所有分析均用瀏覽器哈, 不在 nodejs 中執行.

// case 01
var obj = {
  a: 1,
  b: 2,
  fn: function () {
    console.log(this.a + this.b);
  }
}

var a = 3
var b = 4

var fn = obj.fn
fn() 

從呼叫分析, 這裡 fn 的呼叫首先是進行了一個函式的提取 obj.fn, 然後再呼叫則形成了 函式() 的形式, 則此時 this 在瀏覽器指向了 window 物件, 全域性變數 a, b 都是其屬性, 則最後輸出7.

注意在 nodejs 裡面上面的程式碼是不能執行的, 因為其沒有 window 物件哦

// case 02
function fn() {
  return this.a + this.b
}

// 全域性變數
var a = 1
var b = 2

var obj = {
  a: 3,
  b: fn(), // 函式呼叫()
  fn: fn 
}

var result = obj.fn() // 物件.方法()
console.log(result);

先執行 obj 的定義, 裡面的 b 直接呼叫了函式 fnthis 指向 window 全域性物件, 此時 b 的值為 1 + 2 = 3

然後從呼叫分析, obj.fn() 的形式是 物件.方法()this 指向 obj

// 此時的 obj
obj = {
    a: 3,
    b: 6,
    fn: fn
}

最後形成了 物件.方法 形式, 則最後輸出6.

// case 03
var c = 1
var obj = {
  a: function () {
    var c = 3
    return this.b 
  },
  b: function () {
    var c = 4
    document.write(this.c)
  },
  c: 2
}

var obj1 = obj.a()
obj1()

從呼叫分析, obj.a() 形式為 物件.方法()this 指向 obj 物件

則此時 a 方法裡面的 return this.b 的值為 objb 是個方法.

再進行呼叫 obj1 則形如 函式()this 指向了全域性 window 則此時的 c 是1, 而非函式里面的變數 4,

因此最後輸出為1.

規則03: 類陣列物件 陣列[下標] (), this 指向該類陣列

當類陣列裡面的元素是 function 時,裡面的 this 指向該類陣列.

// case 01
var arr = ['a', 'b', 'c', function () {
  console.log(this[0]);
}]

arr[3]()

從呼叫分析, arr[3] () 滿足形如 陣列[下標] () 的形式, 則 this 指向該陣列 arr, 最終輸出 'a`.

對於類陣列物件, 即所有鍵名為自然數序列 (從 0 開始), 且有 length 的物件, 最常見的是 arguments .

// case 02
function fn() {
  arguments[3]()
}

fn('a', 'b', 'c', function () {
  console.log(this[1]);
})

從呼叫分析, arguments[3] () 滿足形如 陣列[下標] () 的形式, 則 this 指向 arguments 即 fn 在呼叫時傳遞的實引數組, 下標1則輸出 'b'.

// case 03
var a = 6
var obj = {
  a: 5,
  b: 2,
  c: [ 1, a, function () { document.write(this[1])} ]
}

obj.c[2]()

從呼叫分析, obj.c 是一個陣列, 然後再進行下標呼叫, 即形如 陣列[下標] () 的形式, this 指向陣列本身.

下標為1 則指向了陣列裡面的 a , 這裡指向了全域性變數 a , 則最後輸出了 6.

規則04: IIFE中的函式, this 指向 window 物件

IIFE 表示立即執行函式, 定義後立即呼叫. 這在專案中經常用於在頁面載入完後, 立即執行獲取後端接收的資料方法, 定義 + 呼叫 的方式來渲染頁面.

// 寫法1: 將函式用 () 包裹起來再呼叫 ()
(function () {
   console.log(123)
})();


// 寫法2: 函式前面加 void, 最後再呼叫
void function fn() {
  console.log(123)
}()

// case 01
var a = 1
var obj = {
  a: 2,
  fn: (function () {
    var a = this.a
    return function () {
      console.log(a + this.a);
    }
  })() // IIFE, this 指向 window
}

obj.fn() // 物件.方法 this 指向 obj

從呼叫分析,

obj.fn 是一個立即執行函式, 會先執行, 此時的 this 指向全域性 window, 則閉包裡面的 this.a 為外面的 1.

obj.fn() 形如 物件.方法() ,此 this 指向 obj, 則 fn 返回的函式里面的 this.a 的值為 obj.a 的值為 2,

因此最後輸出了3.

規則05: 用定時器, 延時器呼叫函式, this 指向 window 物件

通常在做一些非同步任務 如想後端請求資料啥的, 就容易改變 this 指向, 通常的操作是可以用一個別的變數如叫 that 或者 self 來先指向 this 以保證 this 的指向不會改變. 當然這些前提是, 咱們能識別問題.

  • setInterval (函式, 時間)
  • setTimeout(函式, 時間)
// case 01
var obj = {
  a: 1,
  b: 2,
  fn: function () {
    console.log(this.a + this.b);
  }
}

var a = 3
var b = 4

setTimeout(obj.fn, 2000)

從呼叫分析, obj.fn 是一個函式, 然後外面被延時器呼叫, 2秒後執行, 則此時的 this 指向全域性 window

則最後輸出為7.

這裡可以進行一個呼叫變化.

// case 02
var obj = {
    a: 1,
    b: 2,
    fn: function () {
      console.log(this.a + this.b);
    }
  }

  var a = 3
  var b = 4

  setTimeout(function () {
    obj.fn() // 這裡真正呼叫
  }, 2000);

從呼叫分析, 這裡的 setTimeout 並不是呼叫了函式, 只是將整體延遲了 2秒.

而真正呼叫函式的是 obj.fn() 形如 物件.方法 , 則 this 指向的是 obj, 則最後輸出的是 3.

規則06: 事件處理函式, this 指向繫結事件的 DOM

Dom元素.onclick = function () { }

比如要實現一個效果, 當我們點選哪個盒子, 哪個盒子就變紅, 要求使用同一個事件函式處理實現, 但不能用事件委託的方式.

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    div {
      float: left;
      margin-right: 10px;
      width: 200px;
      height: 200px;
      border: 1px solid #000;
    }
  </style>
</head>

<body>
  <div id="box1">box1</div>
  <div id="box2">box2</div>
  <div id="box3">box3</div>

  <script>
    function setColorRed() {
      // 這裡的 this 指向前面繫結的 DOM 
      this.style.backgroundColor = 'red'
    }

    var box1 = document.getElementById('box1')
    var box2 = document.getElementById('box2')
    var box3 = document.getElementById('box3')

    box1.onclick = setColorRed
    box2.onclick = setColorRed
    box3.onclick = setColorRed

  </script>
</body>

</html>

注意這裡的 this 是指向當前繫結的元素, 而 e.target 指的是內層觸發的元素, 這倆不一樣哦.

再對上面的案例做一個升級: 點選哪個盒子, 哪個盒子在 2秒 後就變紅, 也是要求用一個事件函式來實現哦.

這咋一看似乎蠻簡單:

function setColorRed() {
      // 直接放延遲函式是不行的, 
      // 因為它的 this 指向從當前 dom 變成了 window
      setTimeout(function () {
        this.style.backgroundColor = 'red'
      })
    }

這樣其實是不行的, 因為又之前的規則5所知, 在延時器呼叫函式時, 裡面的 this 指向的是 window物件.

這裡最常用的一個巧妙辦法是:

  • 先用一個變數比如叫 self 來儲存原來的 this 指向的是 Dom, 進行備份
  • 然後在延時器中, 用 self 來替代 this 即可, 這樣還是滿足規則6的
function setColorRed() {
    // 這裡的 this 指向當前 dom
    var self = this 
    setTimeout(function () {
    // 用 self 替換 this, 因為這裡的 this 會指向 window 
    self.style.backgroundColor = 'red'
    }, 2000)
}

函式的 call 和 apply 方法能指定 this

在 js 中陣列和函式都是物件 object , 既然是物件, 那就會有一些原型上的方法, 這裡的 call / apply 作為函式物件的方法, 其功能是能指定函式的上下文 this

比如要統計語數英成績, 對每個小朋友, 比如油哥:

var youge = {
  chinese: 80,
  math: 70,
  english: 60
}

有一個統計成績的函式 sum

function sum() {
  console.log(this.chinese + this.math + this.english);
}

這兩個怎麼進行關聯呢, 簡單的方式將 sum 寫進 youge 物件, 然後構造出 物件.方法() 的方式, 則 this 指向該物件.

var youge = {
  chinese: 80,
  math: 70,
  english: 60,
  sum: function () {
      console.log(this.chinese + this.math + this.english);
  }
}

youge.sum() // 物件.方法() this 指向 物件, 輸出210

但 js 提供了更簡單的方法, 即可透過 call 或者 apply 方法直接指定函式物件的 this

function sum() {
  console.log(this.chinese + this.math + this.english);
}

var youge = {
  chinese: 80,
  math: 70,
  english: 60
}

sum.call(youge)

相關文章