一次搞懂JavaScript中的this

前言

期末考試以前電話面了一次騰訊的暑期實習生,問題都比較簡單,可是稍微一深刻本身的回答就不清楚了,其中有一個問題是ES6的箭頭函數中this的相關知識點,想到本身連普通函數的this都沒理解好就很丟人,惋惜這麼好的機會沒有把握住,此次索性從頭深刻的學習一下,這篇文章就做爲本身的學習筆記。javascript

文章大部份內容是摘抄,根據本身的學習經歷和理解過程從基本的this概念入手,逐步涉及this原理和後續的擴展。html

什麼是this

一句話解釋,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這個知識點的核心了,以前一直不懂,就是由於這裏瞭解的少,或者理解的很差,下面再詳細一點說。

這裏的執行上下文在更深層次上有一個執行上下文棧的概念,歸納來講就是,瀏覽器永遠執行在當前棧中最頂部的那個執行上下文,此時函數就是運行在這個執行上下文中。若是在一個對象或者函數內部又調用一個函數,都會建立一個新的執行上下文,並將這個新的執行上下文壓入執行棧中,調用的函數就會在這個新的執行上下文中執行。這一段很差理解,咱們舉個例子:

在閱讀下面那段話以前,先區分一下這些概念的讀法:

  • 執行上下文(Execution Context)= 執行上下文對象(能夠有多個)
  • 全局執行上下文(Global Execution Context)= 全局環境(只有一個)
  • 當前執行上下文(Current Execution Context)(執行棧中最頂部的那一個)
  • 執行上下文棧(Execution Context Stack) = 執行棧(用來排列存儲執行上下文)

例如,在全局中執行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

有時咱們想在函數中使用的環境變量並不必定是函數所在運行環境中的變量,而是某一個特定運行環境中的變量,在這種狀況下,咱們就須要將函數中的this綁定咱們須要的運行環境(上下文對象)上,ECMAScript 規範給全部函數都定義了 callapply 兩個方法,能夠用來綁定。

callapply用法基本一致,主要區別是傳參的形式不一樣。

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 是做爲函數上下文的對象,函數 foothis 指向了 obj 這個對象。參數 A 和 B 是放在數組中傳入 foo 函數,分別對應 foo 參數的列表元素。

call()

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 函數,而不是放到數組中。

上面兩種方法均可以經過第一個參數,把要綁定的上下文對象傳遞給函數。

bind()

在 ECMAScript5 中擴展了叫 bind 的方法,在低版本的 IE 中不兼容。它和 call 很類似,接受的參數有兩部分,第一個參數是是做爲函數上下文的對象,第二部分參數是個列表,能夠接受多個參數。 它們之間的區別有如下兩點。

bind 返回值是函數

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 中參數的基礎上再日後排。

ES6中箭頭函數的this

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,那麼foo1this指向哪裏呢?

要想拿到foo1的執行上下文,就需先執行foo1(),若是是obj.foo()this指向的是obj,最終的結果就是1(正確)。

筆記參考

文章內部可能在理解上和描述上存在錯誤,若是大佬們發現了請多多幫忙指正呀!😊

相關文章
相關標籤/搜索