關於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函式執行前,建立新的執行上下文併入棧頂;
- 執行f1函式。呼叫函式f2,再次建立新的執行上下文併入棧頂;
- 執行f2函式。由於執行上下文中作用域鏈是由f2-->window,首先在f2函式作用域中沒有找到x,向上查詢至window全域性作用域,window中也沒有定義x,因此執行結果是:Uncaught ReferenceError: x is not defined
相信通過以上的分析,你對作用域和執行上下文已經有所瞭解。總結以上兩個階段我們可以簡單的描述為:
- 定義階段:
- 確定作用域:即確定當前程式碼塊內“資源”的可見性。
- 執行階段:
- 確定執行上下文:作用域鏈、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的值