對箭頭函數的 this 深度理解,它究竟指向誰?

簡介

    面向對象編程語言中 this 是一個很是重要的關鍵字,其在函數執行中經過 this 來明確操做的對象,在 JS 中,JS 並不是面嚮對象語言,可是他也有 this 關鍵字,用來指向函數的調用對象,博主在學習 es5 的時候對 this 理解很是容易,由於之前學過面嚮對象語言,也曾大量的使用,可是學習到 es6 的箭頭函數,它的 this 指向就讓我有點困惑,因而花了一些時間從各個技術博客, MDN 文檔,還有一些其餘的資料進行了研究,總算是對箭頭函數的 this 指向有個深入的認識了,下面就會講述下我本身對箭頭函數 this 的理解。前端

this 的重要性

this 在編程語言中用的次數很是多,多到你不知不覺就會下意識的寫出 this,好比事件綁定,對事件源的操做,好比對一個對象的相關屬性操做,繼承中的 this 使用等。因此 this 的指向是一個一直須要理解並掌握的一個知識,若是不清楚 this 的指向,那麼不少方法就會出現大問題,而且非語法的 bug 維護起來更是使人頭大。嚴格模式中的 this 在全局中指向 undefined,其餘地方下並無什麼影響,因此一下論點也只討論在非嚴格模式下的 this 指向es6

普通函數中的 this

普通函數中的 this 很好理解,無非如下四點:編程

  • 直接調用函數,this 指向全局 window
  • 對象調用函數,this 指向這個對象
  • 構造函數中的 this 指向將要實例化的對象
  • call,apply,bind 能夠改變函數執行時內部的 this 指向

總結一句話就是誰調用函數,this 就指向誰,普通函數的 this 取決於執行時的函數。app

箭頭函數中的 this

下面重點介紹下箭頭函數中的 this 問題編程語言

箭頭函數的語法:函數

// 無參數直接輸出一句話
var fun1 = () => console.log('hello');
// 有一個參數並返回 x*x
var fun2 = x => x*x;
// 有一個參數並返回 y*y
var fun3 = (y) => y*y;
// 有兩個參數並返回 x+y,也能夠簡寫 (x,y) => x+y;
var fun4 = (x,y) => {
	return x+y;
}
複製代碼

若是箭頭函數中沒有用到 this 的話,那麼大可放心的直接使用,由於代碼寫的更少更方便,但若是須要 this,那麼必定得清楚箭頭函數中 this 指向誰。箭頭函數的 this 指向也有不一樣的說法,下面列舉出不一樣說法。學習

  • 箭頭函數中沒有單獨的this,this值取決於箭頭函數所在的環境
  • 箭頭函數沒有本身的this, 它的this是繼承而來; 默認指向在定義它時所處的對象(宿主對象)
  • 箭頭函數的 this 遵循詞法做用域,指向其所屬環境的執行上下文(也能夠說是宿主對象)。

第一種說法較爲模糊,概念不是那麼的清晰,第二種說法經過繼承而來有點牽強的感受,並且我我的覺的有點誤導的感受,所以我看到第三種說法時,雖然以爲有點陌生,說法很是官方的感受,可是卻以爲自習深刻了解第三種說法,應該能徹底掌握箭頭函數的 this,因此就仔細研究了一下,下面將展開對第三種說法的論點,也是本篇的核心(前面一堆廢話,湊字數:huaji:)ui

詞法做用域,執行上下文

詞法做用域簡單來講指的是函數做用域的一種工做模式,因此詞法做用域的法則是基於做用域的概念。ES6 以前做用域分爲全局做用域、局部做用域,變量遵循詞法做用域,ES6 引入了塊級做用域,使得JS也能像其餘的編程語言有了真正的塊級代碼。執行上下文其實就是執行環境,也就是當前的 this,這裏有點繞了吧,其實不要緊,下面會說明的,只不過此時是把上面的第二種繼承方式的原理說明了,箭頭函數的 this 就取決於這個執行上下文的 this,所以才說他是經過繼承而來。this

有了上面的知識做爲根基,那麼究竟怎麼理解此法做用域,和執行上下文,以及若是肯定箭頭函數的 this 指向,接下來繼續說明。 先來一段代碼:es5

var num = 100;
var obj = {
    fun1function ({
        num = 200;
        console.log(num);
    },
    fun2function ({
        var num = 300;
        console.log(num)
    }
}
obj.fun1();         // No.2 200
obj.fun2();         // No.3 300
console.log(num);   // No.1 200
複製代碼

從上述代碼中,fun1 執行時爲 num 賦值,可是能夠從全局中尋找到 num,所以對全局的 num 進行操做,fun2 執行時在本身的局部做用域(函數)聲明瞭一個 num,此時的 num 爲局部的,與全局的 num 無關,fun2 執行完畢後局部 num 就消失了,因此全局的 num 最終結果爲200,這一段代碼中的變量使用的法則,遵循的就是此法做用域,說白點就是尋找變量的過程和其生命週期的範圍受此法做用域約束,另外一個隱藏的知識點就是 obj.fun1(),obj.fun2() 執行時的執行上下文就是 obj, this 就是obj,執行環境就是 obj。


如今真正進入箭頭函數的 this 討論,若是有點忘了箭頭函數 this 指向的第三種說明,如今能夠立馬向上翻滾看一下 :huaji:

看以下 Demo:

var obj = {};
var fun1 = function () {
	console.log(this);
}
var fun2 = () => {
	console.log(this);
}
console.log('normal------------');
// 全局環境下直接調用
fun1();
fun2();
console.log('call------------');
// 經過 call 進行執行環境的綁定
fun1.call(obj);
fun2.call(obj);
複製代碼

image.png
從執行結果來看,call 不會對箭頭函數進行綁定影響,也就是說箭頭函數從他定義的那一刻時,它的 this 就已經肯定了,沒法經過 call 更改,apply 也是一樣的。

再看下一段代碼:

var obj = {
	fun1: function () {
		console.log(this);
	},
	fun2: () => {
		console.log(this);
	}
}
obj.fun1();
obj.fun2();
複製代碼

image.png
結果看出普通函數執行時this取決於執行環境(執行上下文)也就是 obj,而箭頭函數的 this 卻指向 window,使用 call 能改變嗎?

obj.fun1();
obj.fun2();
obj.fun2.call(obj);
複製代碼

image.png
很顯然不能。 根據第三種說法解釋: 箭頭函數的 this 指向也遵循**詞法做用域**,指向當前環境的**執行上下文**

  • 當前的詞法做用域:依賴做用域,當前做用域是全局做用域。
  • 當前環境上下文:全局做用域的環境上下文 this 就是 window

再來一段代碼鞏固下:

var obj = {
	fun1: function () {
		setTimeout(function () {
			console.log('普通函數', this);
		})
	},
	fun2: function () {
		setTimeout(() => {
			console.log('箭頭函數', this);
		})
	}
}
obj.fun1();
obj.fun2();
複製代碼

image.png

  • 普通函數沒得說,計時器時間到執行回調函數的話則在全局環境中執行,所以 this 指向 window
  • 箭頭函數(this 在箭頭函數定義時就肯定,遵循詞法做用域,指向執行上下文對象(也能夠說宿主對象)))
    • 詞法做用域:當前所屬環境爲局部做用域,由於被定義在 function 內
    • 執行上下文:function 的執行上下文未來是在 obj 環境(除非用 call,apply,後面還會說明), 因此 this 已經在箭頭函數定義時被綁定爲 obj 了。又由於 fun2 的 this 指向的是 obj、箭頭函數經過此法做用域依賴 fun2,因此纔會有那個第二種說法說箭頭函數的 this 會繼承執行環境的執行上下文。

再來最後一段代碼:

var obj = {
	// 普通函數中定義一個當即執行函數輸出 this
	fun1: function () {
		(function () {
			console.log(this);
		})();
	},
	// 普通函數中定義一個當即執行箭頭函數輸出 this
	fun2: function () {
		(() => {
			console.log(this);
		})();
	},
	// 箭頭函數中定義一個當即執行的普通函數輸出 this
	fun3: () => {
		(function () {
			console.log(this);
		})();
	},
	// 箭頭函數中定義一個當即執行的箭頭函數輸出 this
	fun4: () => {
		(() => {
			console.log(this);
		})();
	}
}
複製代碼
  • 正常經過 obj 調用的結果:
console.log('正常執行------------');
obj.fun1(); // window
obj.fun2(); // obj
obj.fun3(); // window
obj.fun4(); // window
複製代碼

image.png

  • obj.fun1():內部當即執行函數由於直接調用,執行環境爲 window 因此 this 是 window
  • obj.fun2():內部的當即執行箭頭函數由於定義時 this 根據詞法做用域綁定執行上下文,所以箭頭函數的做用域爲 fun2,綁定 fun2 的執行上下文,this 綁定爲 obj
  • obj.fun3():能夠不分析箭頭函數,由於內部當即執行的普通函數直接調用,執行環境是 window,this 指向 window
  • obj.fun4():分析步驟同 fun2 分析,內部的當即執行箭頭函數根據詞法做用域約束,其屬於 obj.fun4 的箭頭函數,要綁定 obj.fun4 所在的執行上下文,但由於 obj.fun4 也是一個箭頭函數,因此也一樣受詞法做用域的約束,根據以前的示例,obj.fun4 的執行上下文指向的 window,所以內部的當即執行箭頭函數也指向的是 window
  • 經過 call 強行改變執行環境的結果:
// 下面 this 是全局的 window
console.log('使用call執行------------');
obj.fun1.call(this); // window
obj.fun2.call(this); // window
obj.fun3.call(this); // window
obj.fun4.call(this); // window
複製代碼

image.png
從結果上來看,只有 fun2 的結果被改變了,其餘的結果沒有影響,根據上面總結的判斷方法,應該能夠自行對除 fun2 的其餘結果進行分析,那麼如今回顧下 fun2 的代碼
image.png
爲何箭頭函數裏的 this 發生了變化!前面提到過箭頭函數必定定義後就會綁定 this,是沒法經過 call 和 apply 進行改變,爲何這裏發生了變化?是否是這裏比較特殊,不會遵循箭頭函數 this 的指向規則? 其實並非,這裏仍然遵循以前說的法則,正由於它遵照規則,因此輸出的 this 發生了變化,只不過這裏繞了一個彎,由於這裏是函數內部,這裏的當即執行普通剪頭函數在每次 fun2 調用時會從新進行一次函數的定義,而後執行,這裏 fun2 的代碼等價於

fun2: function () {
	var testFun = () => {
		console.log(this);
	}
	testFun();
}
複製代碼

再次根據箭頭函數 this 綁定的法則來看(箭頭函數的 this 遵循詞法做用域,指向其所屬環境的執行上下文(也能夠說是宿主對象)。),每當 fun2 被調用時,會從新定義箭頭函數,當前箭頭函數的詞法做用域是 fun2,其指向 fun2 的執行上下文,正常狀況是 obj,但咱們經過 obj.fun2.call(this) 強行改變了 fun2 的執行上下文爲 window,因此 fun2 的箭頭函數從新定義時則指向了 fun2 的執行上下文 window,也就是經過 call 的結果,因此這並不矛盾。 驗證代碼以下:

var obj = {
	// 普通函數中定義一個當即執行箭頭函數輸出 this
	fun2: function () {
		(() => {
			console.log(this);  // 正常調用 fun2 時,this 已經給被綁定爲 obj 了
		}).call(window);        // 沒法經過 call 強行綁定 window
	},
}
obj.fun2();
複製代碼

image.png
結果並無強行改變箭頭函數的 this ,證實上面說法時正確的。 同理,假如將 fun2 裏面的當即執行函數改爲計時器 + 箭頭函數的格式,那麼每次也是調用 fun2 從新生成計時器和箭頭函數,箭頭函數的內部 this 照樣依據詞法做用域綁定執行上下文。


若是讀者看到哪裏有誤或哪裏說的模糊還請說明,我會及時學習更正的,我也只是一個剛入門的前端小白,這也只是個人我的理解,也是第一次在掘金髮布的本身文章,但願大佬們不要嫌棄我

相關文章
相關標籤/搜索