引言
關於this是面試和日常開發中非常常見的概念之一,也是最易弄混的概念之一,this這個名稱本身有時也容易讓人迷惑。自己之前面試時在this上也是栽過幾次跟頭,特地梳理一下,目的是徹底吃透,以絕後患。
常見錯誤理解
-
this是指向自身嗎?
特別是在函式中使用this的時候,this是指的所在的這個函式物件嗎?看下面示例:
function test() { console.log(this.name); } test.name='aaaa'; test(); 複製程式碼
在上面這個示例裡如果this是指向當前函式的話,執行test後是不是應該輸出'aaaa',但實際上輸出的是空字串,因為此時this指向的是window(在瀏覽器裡執行),所以this並不是來指向自身的。(輸出的具體原因下面再分析)
-
this指向函式的作用域嗎
這個我們同樣可以通過程式碼來驗證下,如下:
function test() { var name = 'bbbbb' console.log(this.name); } test(); 複製程式碼
執行後發現輸出的仍然是空字串,原因和上面一樣,this沒有指向當前函式的作用域。但this一定會不指向當前函式作用域嗎?也不一定,只需知道不能根據所在函式作用域來確定this的指向就對了,應該是確定this的指向,再確定是不是當前函式的作用域。
解決掉常見的理解錯誤後,我們看下this其實是在執行時(即被呼叫時)進行繫結的,並不是在宣告時繫結,它的上下文取決於函式呼叫時的各種條件。this 的繫結和函式宣告的位置沒有任何關係,只取決於函式的呼叫方式。確定this指向的步驟應該是“確定呼叫位置->應用規則->確定this指向”。
尋找呼叫位置
this既然是函式呼叫是才繫結的,那麼需要首先需要確定函式的呼叫位置。這個一般是比較容易的,先確認呼叫棧,然後當前呼叫棧的前一個就是呼叫位置了。示例如下:
function a() {
// 呼叫棧是c->b->a, 呼叫位置b
console.log(a);
}
function b() {
// 呼叫棧是c->b, 呼叫位置c
a()
}
function c() {
// 呼叫棧是c, 呼叫位置是全域性作用域
b();
}
c(); // c的呼叫位置
複製程式碼
應用繫結規則
確定呼叫位置後,需要應this的繫結規則,有四種繫結規則,判斷條件如下:
預設繫結
一般可以理解為無法應用其他規則時的兜底預設規則,獨立函式呼叫時一般適用。
function test(){
console.log(this.a);
}
var a = 'aaa'; // 或者window.a='aaa'
test(); // 輸出2
複製程式碼
上面這種方式便是預設繫結,test()不在任何物件內的獨立呼叫,適用於預設繫結,預設繫結this指向的全域性物件,在瀏覽器裡面就是window,在node裡面就是global, ps:楊哥模式下,全域性物件無法使用預設繫結,預設繫結會繫結到undefined上
隱式繫結
隱式繫結存在於在呼叫位置有上下文物件或者說呼叫時被物件包含或擁有,示例如下:
const obj = {
name: 'oooo',
say: function(){
console.log(this.name);
}
}
obj.say();// oooo
複製程式碼
看上面函式say的呼叫,不是say單獨呼叫,而是被物件obj包含著呼叫,此時this是指向obj物件的。
隱式丟失
有一種情況是看似應該是隱式繫結,但實際卻是預設繫結,有兩個栗子如下:
栗子one:
var name = 'globallll';
var obj = {
name: 'oooo',
say: function(){
console.log(this.name);
}
}
var copy = obj.say;
copy();// globallll
栗子two:
var name = 'globallll';
var obj = {
name: 'oooo',
say: function(){
console.log(this.name);
}
}
function b(func){
func();
}
b(obj.say);// globallll
複製程式碼
看起來say函式的確是obj物件的一部分呀,但為什麼看起來this是指向的window呢?《you don't know JavaScript》裡面把這種特殊對待稱為是隱式丟失,但我理解是這種情況是不滿足隱式函式繫結的,因為隱式函式繫結應當是呼叫是被物件包含著呼叫,而不是說只要是物件的其中一部分就可以了,重點在於呼叫時是否被函式包含著!
我們來看下上述的兩個例子,第一個是把obj裡的函式say的引用賦值給copy變數,再通過copy來呼叫,copy呼叫時並沒有被obj包含著呼叫,這就適用預設繫結規則--獨立函式呼叫,因此此時this是指向window的。第二個例子同理,只不過看起來是呼叫的obj.say(),但實際過程是:
func = obj.say;
func();
複製程式碼
和第一個一樣都有一個賦值的過程。
顯式繫結
首先為啥需要顯示繫結呢?因為以上兩個規則會導致this的指向不穩定,但有時我們需要函式中this穩定指向某個物件的。比如下面這個:
var obj = {
name: 'ooooo',
say: function(){
console.log(this.name);
}
}
var name = 'globalllll';
setTimeout(obj.say, 1000); // globalllll
複製程式碼
這個例子中我們其實是想讓this指向obj然後輸出‘ooooo’的,但實際上呼叫的過程中首先進行了賦值然後進行了呼叫,導致使用預設繫結,this指向了window。為了能固定this的繫結,才有了顯示繫結。
顯示繫結有三種方式:apply,call和bind。這三個函式大家應該用的比較多了,其中apply和call只有傳參的區別,而apply和bind的區別在於apply繫結後立即執行,而bind可以返回繫結後的函式。上述例子可以這麼解決:
var obj = {
name: 'ooooo',
say: function(){
console.log(this.name);
}
}
var test = obj.say.bind(obj);//繫結後this指向不可修改
//或者
//var test = function (){
// obj.say.call(obj);
//}
var name = 'globalllll';
setTimeout(test, 1000); // oooo
複製程式碼
思考?:如何用apply或者call實現bind?
進階:實現apply?
new繫結
new是一個由類新建示例的過程。使用new時會呼叫建構函式,但是過程中並不是例項化了一些類,而是通過新建一個物件,然後執行原型的連結,新物件繫結函式呼叫的this,返回這個物件,返回的這個物件我們就稱為一個例項。比如: function Person(name){ this.name = name; } var student = new Person('LiMing'); console.log(student.name) // LiMing
上述程式碼的過程是:
- 新建一個物件
- 執行原型的連結(不是本文的重點)
- 將新建物件繫結到Person中的this上,也就是目前this指向新建物件
- 返回這個新建物件給student,即student等於新建物件
這種在new的過程中繫結this的方式稱為是new繫結。
優先順序
上面我們瞭解四種繫結規則,但問題是如果符合其中一種以上的情形時應該如果確定是哪種呢?這就要確定四種規則的優先順序了。優先順序如下: new 繫結>顯式繫結>隱式繫結>預設繫結。
特殊情況
凡事有例外,this也一樣。下面介紹下特殊情況。
箭頭函式
es6中引入的箭頭函式雖然也叫函式,但是卻不適用於上面的四規則。先引入一個?:
function a(){
return ()=>{
console.log(this.name);
}
}
const obj1={
name: 11111,
}
const obj2={
name: 22222,
}
var test = a.call(obj1);
test.call(obj2); // 11111
複製程式碼
上面例子中箭頭函式理論上繫結的是obj2,但是實際輸出的卻是11111。所以箭頭函式是不適用於上面的四規則的。箭頭函式的具體規則時:箭頭函式this是在宣告時就確定了,其this就是宣告時所在作用域的this確定的。比如上面的例子,箭頭函式是在a函式中宣告的,所以箭頭函式中所用的this就是a的this,而a中的this是根據呼叫位置和規則確定是obj1,所以箭頭函式中this也是指向obj1。
結語
面試時有很多問題都是考察this的,題目雖然變化萬千,但是真正掌握了原理,還是能夠做到胸有成竹,迎刃而解的。最近找工作面試的人比較多,分享出來希望能幫到大家!