深刻理解JavaScript中的this指向

與其餘語言相比,js中的this有所不一樣,也是比較頭疼的問題。在參考了一些資料後,今天,就來深刻解析一下this指向問題,有不對的地方望你們指出。前端

爲何要用this

對於前端開發者來講,this是比較複雜的機制,那麼爲何要花大量時間來學習呢,先來看一段代碼。
若是不使用this,要給identify( )和speak( )顯式傳入一個對象:數組

function identify(context) {
    return context.name.toUpperCase();
}
function speak(context) {
    var greeting = "Hello, I'm" + identify(context);
    console.log(greeting);
}
identify(you);
speak(me);

能夠看到,speak( )裏面直接寫了identify( )的函數名,然而,隨着使用模式愈來愈複雜,顯式傳遞的上下文會讓代碼變得混亂,尤爲體如今面向對象中。
顯然,this提供了一種方式來隱式「傳遞」一個對象的引用,更加簡潔,易於複用。瀏覽器

this的誤解

1. this指向函數自己

記錄函數foo被調用的次數:app

function foo(num) {
    console.log("foo:" + num);
    
    // 記錄次數
    this.count++;
}

foo.count = 0;

var i;

for (i = 0; i < 10; i++) {
    if (i > 5) {
        foo(i);
    }
}

// foo: 6
// foo: 7
// foo: 8
// foo: 9

//foo被調用了多少次?
console.log(foo.count); // 0

從前兩次的console.log( )能夠看出,foo確實被調用了4次,可是foo.count仍然爲0,顯然this指向函數自己的理解是錯誤的。ide

2. this指向函數做用域

要明確的是,this在任何狀況下都不指向函數的詞法做用域。由於,做用域「對象」沒法經過JavaScript代碼訪問,它存在於JavaScript引擎內部。
下面的代碼試圖使用this來隱式引用函數的詞法做用域,沒有成功:函數

function foo() {
    var a = 2;
    this.bar();
}
function bar() {
    console.log(this.a);
}
foo(); // ReferenceError: a is not defined

直接報出了訪問不到foo( )中的a。ReferenceError和做用域判別失敗相關,而TypeError表明做用域判別成功,可是對結果的操做是非法的、不合理的。學習

this是什麼

排除了以上兩個誤解以後,來看一下this究竟是什麼。
this是運行時綁定的,它和函數聲明的位置沒有任何關係,只取決於函數的調用方式。當一個函數被調用時,會建立一個活動記錄(執行上下文),這個記錄包含函數在哪裏被調用,函數的調用方式、傳入的參數等,this就是這個記錄的一個屬性,在函數執行的過程當中用到。即,this老是指向調用它所在方法的對象this

1. 在瀏覽器中,調用方法沒有明確對象時,this指向window
function foo() {
    console.log(this.a);
}

var a = 2;

foo(); // 2

在全局中聲明變量a = 2,而後在全局中直接調用foo( ),this指向了全局對象,獲得a的值。
要注意的是,在嚴格模式(strict mood)下,若是this沒有被執行環境定義,那它將綁定爲undefined。code

function foo() {
    "use strict";
    
    console.log(this.a);
}

var a = 2;

foo(); // TypeError: this is undefined

在嚴格模式下,調用foo( )不影響this綁定。對象

function foo() {
    console.log(this.a);
}

var a = 2;

(function() {
    "use strict";
    
    foo(); // 2
})();
2. 在瀏覽器中,setTimeout、setInterval和匿名函數執行時的當前對象是全局對象window
function foo() {
    console.log(this.a);
}

var obj = {
    a: 2,
    foo: foo
};

var a = "global";

setTimeout(obj.foo, 100); // "global"

JavaScript中的setTimeout( )的實現和下面僞代碼類似:

function setTimeout(fn, delay) {
    // 等待delay毫秒
    fn(); // 調用函數
}
3. apply / call / bind能夠強制改變this指向
function foo() {
    console.log(this.a);
}

var obj = {
    a: 2
};

foo.call(obj); // 2
foo.apply(obj); // 2
foo.bind(obj); // 2

call和apply的區別在於第二個參數,call是把args所有列出來,用「,」分隔,而apply是一個類數組。call、apply是硬綁定,經過硬綁定的函數不能再修改它的this。

function foo() {
    console.log(this.a);
}

var obj = {
    a: 2
};

var bar = function() {
    foo.call(obj);
}

bar(); // 2
setTimeout(bar, 100); // 2

bar.call(window); // 2

函數foo( )內部手動調用了foo.call(obj),把foo的this強制綁定到了obj,因此後面即便又把bar( )綁定到了window,仍是沒法改變this指向。

4. new操做符改變this指向

在傳統的面嚮對象語言中,會使用new初始化類,然而在JavaScript中new的機制和麪向對象語言徹底不一樣。在js中,構造函數只是使用new操做符時被調用的函數,它們並不屬於一個類,也不會實例化一個類。也就是說,js中,不存在所謂的「構造函數」,只有對函數的「構造調用」。

function foo(a) {
    this.a = a;
}

var bar = new foo(2);
console.log(bar.a); // 2

使用new調用foo( ),會構造一個新對象並把它綁定到foo( )調用中的this上。

優先級

既然有那麼多能夠改變this的指向,那麼它們的優先級是怎麼樣的呢,記住這句話:範圍越小,優先級越高。能夠按照下面的順序來判斷:

  1. 判斷函數是否在new中調用過:

    var bar = new foo();
  2. 判斷函數是否經過call、apply、bind綁定過:

    var bar = foo.call(obj);
  3. 判斷函數是否在某個上下文對象中調用過:

    var bar = obj.foo();
  4. 若是以上狀況均不存在,那麼在嚴格模式下,綁定到undefined,不然綁定到全局對象:

    var bar = foo();
相關文章
相關標籤/搜索