在網上很多文章都對 Javascript 中的 this 做了詳細的介紹,但大多是介紹各個繫結方式或呼叫方式下 this 的指向,於是我想有一個統一的思路來更好理解 this 指向,使大家更好判斷,以下有部分內容不是原理,而是一種解題思路。
從call方法開始
call 方法允許切換函式執行的上下文環境(context),即 this 繫結的物件。
大多數介紹 this 的文章中都會把 call 方法放到最後介紹,但此文我們要把 call 方法放在第一位介紹,並從 call 方法切入來研究 this ,因為 call 函式是顯式繫結 this 的指向,我們來看看它如何模擬實現(不考慮傳入 null 、 undefined 和原始值):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Function.prototype.call = function(thisArg) { var context = thisArg; var arr = []; var result; context.fn = this; for (let i = 1, len = arguments.length; i < len; i++) { arr.push('arguments[' + i + ']'); } result = eval("context.fn(" + arr + ")"); delete context.fn; return result; } |
從以上程式碼我們可以看到,把呼叫 call 方法的函式作為第一個引數物件的方法,此時相當於把第一個引數物件作為函式執行的上下文環境,而 this 是指向函式執行的上下文環境的,因此 this 就指向了第一個引數物件,實現了 call 方法切換函式執行上下文環境的功能。
物件方法中的this
在模擬 call 方法的時候,我們使用了物件方法來改變 this 的指向。呼叫物件中的方法時,會把物件作為方法的上下文環境來呼叫。
既然 this 是指向執行函式的上下文環境的,那我們先來研究一下呼叫函式時的執行上下文情況。
下面我門來看看呼叫物件方法時執行上下文是如何的:
1 2 3 4 5 6 7 |
var foo = { x : 1, getX: function(){ console.log(this.x); } } foo.getX(); |
從上圖中,我們可以看出getX
方法的呼叫者的上下文是foo
,因此getX
方法中的 this 指向呼叫者上下文foo
,轉換成 call 方法為foo.getX.call(foo)
。
下面我們把其他函式的呼叫方式都按呼叫物件方法的思路來轉換。
建構函式中的this
1 2 3 4 5 6 7 8 |
function Foo(){ this.x = 1; this.getX = function(){ console.log(this.x); } } var foo = new Foo(); foo.getX(); |
執行 new 如果不考慮原型鏈,只考慮上下文的切換,就相當於先建立一個空的物件,然後把這個空的物件作為建構函式的上下文,再去執行建構函式,最後返回這個物件。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var newMethod = function(func){ var context = {}; func.call(context); return context; } function Foo(){ this.x = 1; this.getX = function(){ console.log(this.x); } } var foo = newMethod(Foo); foo.getX(); |
DOM事件處理函式中的this
1 2 3 |
DOMElement.addEventListener('click', function(){ console.log(this); }); |
把函式繫結到DOM事件時,可以當作在DOM上增加一個函式方法,當觸發這個事件時呼叫DOM上對應的事件方法。
1 2 3 4 |
DOMElement.clickHandle = function(){ console.log(this); } DOMElement.clickHandle(); |
普通函式中的this
1 2 3 4 5 |
var x = 1; function getX(){ console.log(this.x); } getX(); |
這種情況下,我們建立一個虛擬上下文物件,然後普通函式作為這個虛擬上下文物件的方法呼叫,此時普通函式中的this就指向了這個虛擬上下文。
那這個虛擬上下文是什麼呢?在非嚴格模式下是全域性上下文,瀏覽器裡是 window ,NodeJs裡是 Global ;在嚴格模式下是 undefined 。
1 2 3 4 5 6 7 |
var x = 1; function getX(){ console.log(this.x); } [viturl context].getX = getX; [viturl context].getX(); |
閉包中的this
1 2 3 4 5 6 7 8 9 10 11 12 |
var x = 1; var foo = { x: 2, y: 3, getXY: function(){ (function(){ console.log("x:" + this.x); console.log("y:" + this.y); })(); } } foo.getXY(); |
這段程式碼的上下文如下圖:
這裡需要注意的是,我們再研究函式中的 this 指向時,只需要關注 this 所在的函式是如何呼叫的, this 所在函式外的函式呼叫都是浮雲,是不需要關注的。因此在所有的圖示中,我們只需要關注紅色框中的內容。
因此這段程式碼我們關注的部分只有:
1 2 3 |
(function(){ console.log(this.x); })(); |
與普通函式呼叫一樣,建立一個虛擬上下文物件,然後普通函式作為這個虛擬上下文物件的方法立即呼叫,匿名函式中的 this 也就指向了這個虛擬上下文。
引數中的this
1 2 3 4 5 6 7 8 |
var x = 1; var foo = { x: 2, getX: function(){ console.log(this.x); } } setTimeout(foo.getX, 1000); |
函式引數是值傳遞的,因此上面程式碼等同於以下程式碼:
1 2 3 4 |
var getX = function(){ console.log(this.x); }; setTimeout(getX, 1000); |
然後我們又回到了普通函式呼叫的問題。
全域性中的this
全域性中的 this 指向全域性的上下文
1 2 |
var x = 1; console.log(this.x); |
複雜情況下的this
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var x = 1; var a = { x: 2, b: function(){ return function(){ return function foo(){ console.log(this.x); } } } }; (function(){ var x = 3; a.b()()(); })(); |
看到上面的情況是有很多個函式,但我們只需要關注 this 所在函式的呼叫方式,首先我們來簡化一下如下:
1 2 3 4 5 6 7 8 |
var x = 1; (function(){ var x = 3; var foo = function(){ console.log(this.x); } foo(); }); |
this 所在的函式 foo 是個普通函式,我們建立一個虛擬上下文物件,然後普通函式作為這個虛擬上下文物件的方法立即呼叫。因此這個 this指向了這個虛擬上下文。在非嚴格模式下是全域性上下文,瀏覽器裡是 window ,NodeJs裡是 Global ;在嚴格模式下是 undefined 。
總結
在需要判斷 this 的指向時,我們可以安裝這種思路來理解:
- 判斷 this 在全域性中OR函式中,若在全域性中則 this 指向全域性,若在函式中則只關注這個函式並繼續判斷。
- 判斷 this 所在函式是否作為物件方法呼叫,若是則 this 指向這個物件,否則繼續操作。
- 建立一個虛擬上下文,並把this所在函式作為這個虛擬上下文的方法,此時 this 指向這個虛擬上下文。
- 在非嚴格模式下虛擬上下文是全域性上下文,瀏覽器裡是 window ,Node.js裡是 Global ;在嚴格模式下是 undefined 。
圖示如下: