這個 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的, 雖然這裡的 fn
是 obj1 的 fn
但 this
仍指向 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
直接呼叫了函式 fn
其 this
指向 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
的值為 obj
的 b
是個方法.
再進行呼叫 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)