結合執行棧、執行上下文,理解this的指向問題

不吃藥不喝水發表於2019-08-10

關於js中的this指向,必須深刻理解下面這句話:

  • this指向是在執行時確定的,不是定義時確定的
  • this指向是在執行時確定的,不是定義時確定的
  • this指向是在執行時確定的,不是定義時確定的

在介紹this指向之前,首先來解答由上面這句話引申出來的一個問題:“定義”時確定了什麼,“執行”時又確定了什麼呢? 以下面的程式碼為例展開說明:

function f1(){
    var x = 10;
    f2();
}
function f2(){
    console.log(x);
}
f1();
複製程式碼

定義時 -- 作用域

作用域決定程式碼塊內“資源”的可見性。作用域在定義時就確定,並且不會改變。

在給出上面程式碼執行的結果之前,我們先來分析一下程式碼定義到執行的過程。首先,在定義時確定程式碼的作用域:

  • 全域性作用域:在全域性作用域中只有兩個變數f1和f2,分別指向一個函式;
  • f1函式作用域:在f1函式作用域中,只有一個變數x;
  • f2函式作用域:在f2函式作用域中,沒有定義變數
    作用域
    至此,程式碼的作用域確定。作用域的確定,程式碼塊內“資源”的可見性隨即確定:
  • 全域性作用域內可訪問的變數:f1和f2;
  • f1函式作用域內可訪問的變數:x和全域性作用域內的所有可見的變數;
  • f2函式作用域內可訪問的變數:全域性作用域內的所有可見的變數

也許這時候你更加確定程式碼的執行結果,但是這裡暫不揭曉,我們繼續分析。

執行時 -- 執行上下文

執行上下文是在程式碼執行時確定,當函式被呼叫時,會在執行棧中建立一個新的執行上下文,併入棧頂。 (對於執行棧和執行上下文的理解可以參考此篇文章,也可以參考我翻譯以後的譯文)

上述程式碼段中,f1在全域性作用域內被呼叫。下面我們分析一下函式呼叫和執行的具體過程:

  • f1函式在全域性執行上下文中被呼叫;
  • 在f1函式執行前,建立新的執行上下文併入棧頂;
    結合執行棧、執行上下文,理解this的指向問題
  • 執行f1函式。呼叫函式f2,再次建立新的執行上下文併入棧頂;
    結合執行棧、執行上下文,理解this的指向問題
  • 執行f2函式。由於執行上下文中作用域鏈是由f2-->window,首先在f2函式作用域中沒有找到x,向上查詢至window全域性作用域,window中也沒有定義x,因此執行結果是:Uncaught ReferenceError: x is not defined
    結合執行棧、執行上下文,理解this的指向問題

相信通過以上的分析,你對作用域和執行上下文已經有所瞭解。總結以上兩個階段我們可以簡單的描述為:

  • 定義階段:
    • 確定作用域:即確定當前程式碼塊內“資源”的可見性。
  • 執行階段:
    • 確定執行上下文:作用域鏈、VO/AO、this的值

但是,這些和this的指向有什麼關係呢?其實回顧剛剛的定義過程和執行過程,你會發現,在定義階段根本就沒有涉及到this,只有在執行階段才有this的出現。因此,你也就明白了本文最開始的一句話:this指向是在執行時確定的,而不是在定義時確定的。

this指向

關於this指向,相信大家看到過很多描述,比如:

  • 如果函式作為物件的方法時,方法中的 this 指向該物件;
  • 建構函式中的this,指向new出來的新物件
  • 普通函式在全域性中呼叫,this指向window
  • ....

面對這麼多情況,我們該怎麼記憶?其實,this指向沒有這麼多規則,歸根結底只有一句話:this總是指向當前函式所在的執行上下文 (注:箭頭函式後面會單獨介紹)。 結合這句話,我們僅從定義和執行兩個角度再去分析常見的場景:

場景一:物件中的函式

var x = 1;
var obj = {
    x: 10,
    foo: function (){
        var x = 100;
        console.log(this.x); 
    }
}
obj.foo(); // 列印10
複製程式碼
- 定義階段:
  - 全域性作用域可訪問的變數:x 和 obj ,其中obj的屬性:x 和 foo
- 執行階段:
  - 呼叫obj.foo(),建立新的執行上下文
      - 確定作用域鏈和變數物件,其中foo的作用域鏈為:foo-->obj-->window
      - 確定this的值,foo所在的執行上下文為obj,所以foo中的this指向obj
  - 執行obj.foo(),控制檯中列印10
複製程式碼

場景二:全域性作用域中的函式

function fun1 (){
    console.log(this);
}
fun1(); // 列印window
複製程式碼
- 定義階段:
  - 全域性作用域可訪問的變數:fun1 
- 執行階段:
  - 呼叫fun1(),相當於呼叫window.foo(),建立新的執行上下文
      - 確定作用域鏈和變數物件,其中fun1的作用域鏈為:fun1 --> window
      - 確定this的值,fun1所在的執行上下文為window,所以fun1中的this指向window
  - 執行func1(),控制檯列印window
複製程式碼

場景三:建構函式

function Student (name,age){
    this.name = name;
    this.age = age;
}
Student.prototype.show = function (){
    console.log(this.name);
}
var andy = new Student('andy',10);
andy.show(); // 列印andy
複製程式碼
- 定義階段:
  - 全域性作用域可訪問的變數:Student、andy 
- 執行階段:
  - 呼叫new Student('andy',10),其中new完成的操作如下所示:
      - 建立空物件,即var obj = {}
      - 當前空物件的原型指向建構函式的prototype物件,即obj.__proto__ = Object.create(Student.prototype)
      - this指向當前物件obj
      - 給當前空物件賦值,即obj.name = 'andy',obj.age = 10
      - 返回當前空物件obj
  - 執行andy.show(),通過原型鏈的方式呼叫Student中的show()方法,show方法當前所在的執行上下文為andy,因此控制檯列印andy
複製程式碼

場景四:回撥函式

var x = 1;
var obj1 = {
    x:10,
    foo: function (){
        setTimeout(function (){
            console.log(this.x);
        },1000)
    }
}
obj1.foo(); // 列印1
複製程式碼
- 定義階段:
  - 全域性作用域可訪問的變數:x、obj1 
- 執行階段:
  - 呼叫obj1.foo(),建立新的執行上下文
      - 確定作用域鏈和變數物件,其中foo的作用域鏈為:foo --> obj --> window
      - setTimeout函式是window的內建函式,因此setTimeout中回撥函式的執行上下文為window,則回撥函式中的this指向window
  - 執行obj1.foo(),控制檯列印1
複製程式碼

場景五:call,bind,apply

var x = 1;
var obj2 = {
    x:10,
    foo: function (){
        console.log(this.x);
    }
}
var obj3 = {
    x: 100
}
obj2.foo.call(obj3); // 列印100
複製程式碼
call函式是改變“呼叫者”的執行上下文(即this指向)並立即執行“呼叫者”
- 定義階段:
  - 全域性作用域可訪問的變數:x、obj2、obj3,其中obj2的屬性包括x和foo,obj3的屬性包括x
- 執行階段:
  - 呼叫obj2.foo.call(obj3), ,將“呼叫者”foo的執行上下文改為call函式的第一個引數obj3
  - foo的執行上下文更改為新的執行上下文obj3
      - 確定作用域鏈和變數物件,其中foo的作用域鏈由為:foo --> obj2 --> window 更改為 foo --> obj3 --> window
      - 確定this的值,foo所在的執行上下文為obj3,所以foo中的this指向obj3
  - 執行obj2.foo.call(obj3),控制檯列印100
 
 apply和call的作用相同,只是第二個引數的資料型別是陣列。bind也是改變呼叫者的執行上下文,不同於call和apply的地方是,函式不會立即執行。
複製程式碼

總結以上情況,我們發現每一個this值的確定過程都涉及到了執行上下文,因此this總是指向當前函式所在的執行上下文。但是,箭頭函式中的this指向也是這麼確定的嗎?

箭頭函式中的this

本文一開始我們就提到一句話,this指向是在執行時確定的,不是在定義時確定的。這句話在箭頭函式中還適用嗎?我們來看下面的例子:

var x = 1;
var obj = {
    x: 10,
    foo: () => {
        console.log(this.x);
    }
}
obj.foo();
複製程式碼
我們還是按照定義和執行兩個角度進行分析:
- 定義階段:
  - 全域性作用域可訪問的變數:x、obj
- 執行階段:
  - 呼叫obj.foo(),建立新的執行上下文
      - 確定作用域鏈和變數物件,其中foo的作用域鏈為:foo --> obj --> window
      - 確定this的值,foo所在的執行上下文為obj,所以foo中的this指向obj
  - 執行obj.foo(),控制檯列印10
複製程式碼

實際結果是我們分析的10嗎?執行程式碼發現不是10,而是1。這是為什麼呢?因為箭頭函式中沒有this,箭頭函式體內的this === 最靠近箭頭函式的 繫結this的 普通函式的 this值。再看下面的例子:

var x = 1;
var obj = {
    x: 10,
    foo: function (){
        var x = 100;
        setTimeout(() => {
            console.log(this.x);
        },1000)
    }
}
obj.foo();
複製程式碼
- 定義階段:
  - 全域性作用域可訪問的變數:x、obj
- 執行階段:
  - 呼叫obj.foo(),建立新的執行上下文
      - 確定作用域鏈和變數物件,其中foo的作用域鏈為:foo --> obj --> window
      - 確定this的值,foo所在的執行上下文為obj,所以foo中的this指向obj
      - foo內部呼叫setTimeout函式,其中回撥函式為箭頭函式
  - 執行obj.foo()
      - 執行回撥函式,其中this位於回撥函式內部,所以沿著作用域鏈向上查詢,發現foo中有this,並且this繫結在obj中,因此回撥函式中的this也指向obj
      - 列印結果為10
複製程式碼

總結

  • this指向是在執行時確定的,不是在定義時確定的;
  • 箭頭函式中的this指向 ===> 最靠近它的 繫結this指向的 函式中的 this的值
以上是我個人的近期學習總結,如有不妥之處,還望各位指出

相關文章