期末考試以前電話面了一次騰訊的暑期實習生,問題都比較簡單,可是稍微一深刻本身的回答就不清楚了,其中有一個問題是ES6的箭頭函數中this的相關知識點,想到本身連普通函數的this都沒理解好就很丟人,惋惜這麼好的機會沒有把握住,此次索性從頭深刻的學習一下,這篇文章就做爲本身的學習筆記。javascript
文章大部份內容是摘抄,根據本身的學習經歷和理解過程從基本的this概念入手,逐步涉及this原理和後續的擴展。html
一句話解釋,this表示函數執行時所在的運行環境(執行上下文對象),換句話說就是,誰調用的函數,this就表示是誰。若是不理解,看下面這個例子。java
var obj = {
bar: 1,
foo: function() {
console.log(this.bar);
}
};
var bar = 2;
var foo = obj.foo;
obj.foo(); // 1
foo(); // 2
複製代碼
對於obj.foo()
來講,obj.foo()
中foo()
的執行上下文對象是obj
,因此this
表示obj
;而對於foo()
來講,foo()
的執行上下文對象是全局環境,this
表示全局環境。git
this
的目的就是在函數體內部,指代函數當前的運行環境(context),若是還不理解,就是不明白什麼是運行環境,那接着看下面的解釋吧。es6
JavaScript 語言之因此有this
的設計,跟內存裏面的數據結構有關係。github
var obj = {
foo: 5
};
複製代碼
JavaScript引擎會先在內存裏面,生成一個對象{foo: 5}
,而後把這個對象的內存地址賦值給obj
。也就是說,變量obj
是一個地址,若是要讀取obj.foo
,引擎先從obj
拿到內存地址,而後再從該地址讀出原始對象,返回它的foo
屬性。數組
原始對象以字典結構保存,每一個屬性名都對應一個屬性描述對象。舉例來講,上面例子的foo
屬性,其實是如下面形式保存的。瀏覽器
{
foo: {
[[value]]: 5
[[writable]]: true
[[enumerable]]: true
[[configurable]]: true
}
}
複製代碼
foo
屬性的值保存在屬性描述對象的value
屬性裏面。數據結構
這樣的結構是清晰的,問題在於屬性的值多是一個函數。app
var obj = {
foo: function() {}
};
複製代碼
這時,引擎會將函數單獨保存到內存中,而後再將函數的地址賦給foo
屬性的value
屬性。
{
foo: {
[[value]]: 函數的地址
...
}
}
複製代碼
因爲函數是一個單獨的值,因此它能夠在不一樣的環境(執行上下文)中執行。
上面這句話能夠說是this
這個知識點的核心了,以前一直不懂,就是由於這裏瞭解的少,或者理解的很差,下面再詳細一點說。
這裏的執行上下文在更深層次上有一個執行上下文棧的概念,歸納來講就是,瀏覽器永遠執行在當前棧中最頂部的那個執行上下文,此時函數就是運行在這個執行上下文中。若是在一個對象或者函數內部又調用一個函數,都會建立一個新的執行上下文,並將這個新的執行上下文壓入執行棧中,調用的函數就會在這個新的執行上下文中執行。這一段很差理解,咱們舉個例子:
在閱讀下面那段話以前,先區分一下這些概念的讀法:
例如,在全局中執行foo()
,也就是在全局執行上下文中執行時,執行棧內只有一個全局執行上下文;當執行obj.foo()
時,obj
這個執行上下文將被壓入執行棧中,foo()
會在這個新的當前執行上下文中被執行,因此就有了函數能夠在不一樣的環境(執行上下文)中執行。
明白執行環境(執行上下文)的概念後,this
的做用就容易理解了,this
就是用來指代函數當前運行環境的!
若是函數的當前運行環境內還定義了其它變量(環境變量),咱們就可使用this來調用了。
例如:
var f = function() {
console.log(this.x);
};
var x = 1;
var obj = {
f: f,
x: 2
};
// 全局環境下執行
f(); // 1
// obj 環境下執行
obj.f(); // 2
複製代碼
上面代碼中,函數f
在全局環境執行,this.x
指向全局環境的x
。
在obj
環境執行,this.x
指向obj.x
。
深刻理解原理後,this
的概念和使用就變得清晰了。
爲了加深理解,這裏有一個嵌套對象的示例。
var obj = {
bar: 1,
obj1: {
bar: 2,
foo: function() {
console.log(this.bar);
}
}
};
obj.obj1.foo(); // 2
複製代碼
上面這段代碼中,foo
所在的運行環境(執行上下文對象)爲obj1
,此時執行棧中從下到上依次是全局執行上下文、obj
執行上下文和obj1
執行上下文,因此this
表示obj1
。
var obj = {
bar: 1,
foo1: function() {
var bar = 2;
var foo2 = function() {
return this.bar;
}
console.log(foo2());
}
}
var bar = 3;
obj.foo1(); // 3
複製代碼
上面這段代碼中foo2
函數在被定義後就被調用,比obj.foo1
更早調用,此時執行棧中只有全局執行上下文(window),在嚴格模式(strict)下,執行上下文則是undefined
,這也是 JavaScript 的一個大坑。必定要注意單獨調用函數時,其內部this
的指向!
有時咱們想在函數中使用的環境變量並不必定是函數所在運行環境中的變量,而是某一個特定運行環境中的變量,在這種狀況下,咱們就須要將函數中的this
綁定咱們須要的運行環境(上下文對象)上,ECMAScript 規範給全部函數都定義了 call 與 apply 兩個方法,能夠用來綁定。
call
和apply
用法基本一致,主要區別是傳參的形式不一樣。
apply
方法傳入兩個參數:一個是做爲函數上下文的對象,另一個是做爲函數參數所組成的數組。
var obj = {
bar: 1;
};
function foo(fistParam, secondParm) {
console.log(firstParam + ' ' + this.bar + ' ' + secondParam);
};
foo.apply(obj, ['A', 'B']); // A 1 B
複製代碼
能夠看到,obj
是做爲函數上下文的對象,函數 foo
中 this
指向了 obj
這個對象。參數 A 和 B 是放在數組中傳入 foo
函數,分別對應 foo
參數的列表元素。
call
方法第一個參數也是做爲函數上下文的對象,可是後面傳入的是一個參數列表,而不是單個數組。
var obj = {
bar: 1
}
function foo(fistParam, secondParm) {
console.log(firstParam + ' ' + this.bar + ' ' + secondParam);
};
func.call(obj, 'C', 'D'); // C 1 D
複製代碼
對比 apply
咱們能夠看到區別,C 和 D 是做爲單獨的參數傳給 foo
函數,而不是放到數組中。
上面兩種方法均可以經過第一個參數,把要綁定的上下文對象傳遞給函數。
在 ECMAScript5 中擴展了叫 bind
的方法,在低版本的 IE 中不兼容。它和 call
很類似,接受的參數有兩部分,第一個參數是是做爲函數上下文的對象,第二部分參數是個列表,能夠接受多個參數。 它們之間的區別有如下兩點。
var obj = {
bar: 1
}
function foo() {
console.log(this.bar);
}
var func = foo.bind(obj);
func(); // 1
複製代碼
bind
方法不會當即執行,而是返回一個改變了上下文 this
後的函數。而原函數 foo
中的 this
並無被改變,依舊指向全局對象 window
。
function func1(a, b, c) {
console.log(a, b, c);
}
var func2 = func1.bind(null, 1);
func1('A', 'B', 'C'); // A B C
func2('A', 'B', 'C'); // 1 A B
func2('B', 'C'); // 1 B C
func1.call(null, 1); // 1 undefined undefined
複製代碼
call
是把第二個及之後的參數做爲 func1
方法的實參傳進去,而 func2
方法的實參實則是在 bind
中參數的基礎上再日後排。
var obj = {
bar: 1,
foo1: function() {
var bar = 2;
var foo2 = function() {
return this.bar;
}
console.log(foo2());
}
}
var bar = 3;
obj.foo1(); // 3
複製代碼
仍是看這個例子,當在函數foo1
中單獨調用內部的函數foo2
時,foo2
中的this
可能指向window
或者undefined
;除此以外,咱們通常是經過對象來調用,無論如何調用,this
所表明的對象老是視狀況而定,這會給咱們帶來必定的麻煩,如今,箭頭函數幫咱們解決了這個問題。
在《ECMAScript 6 入門》一書中,阮一峯老師這樣描述:
箭頭函數體內的
this
對象,就是定義時所在的對象,而不是使用時所在的對象。
這句話是存在歧義的,由於在JavaScript中函數和對象之間的界限並不清晰,看下面這個例子,foo2
函數是定義在foo1
中的,那foo2
中的this
是否能夠用來表示foo1
呢? 這種思惟得出的結果是2(錯誤)。
阮老師在 ruanyf/es6tutorial issue中解釋到,foo2
位於foo1
內部,只有當foo1
函數運行後,foo2
纔會按照定義生成。這種解釋對應書中的概念是沒有問題的,但用第一種理解方式會給咱們帶來必定的困擾。
那該如何理解呢?
在 issue 中有另一個解釋:
全部的箭頭函數都沒有本身的this,都指向外層
或者將書中的描述改成
箭頭函數中的this老是指向所在函數運行時的this
這兩種描述就容易理解了。
const obj = {
bar: 1,
foo1: function() {
const bar = 2;
const foo2 = () => {
return this.bar;
}
console.log(foo2());
}
}
const bar = 3;
obj.foo1(); // 1
複製代碼
由於箭頭函數內部的this
沒有指向,因此當執行foo2()
時,要去外層foo1
尋找this
,那麼foo1
的this
指向哪裏呢?
要想拿到foo1
的執行上下文,就需先執行foo1()
,若是是obj.foo()
,this
指向的是obj
,最終的結果就是1(正確)。
文章內部可能在理解上和描述上存在錯誤,若是大佬們發現了請多多幫忙指正呀!😊