你真的理解this嗎

Coderfei發表於2019-03-29

this關鍵字是JavaScript中最複雜的機制之一,是一個特別的關鍵字,被自動定義在所有函式的作用域中,但是相信很多JvaScript開發者並不是非常清楚它究竟指向的是什麼。聽說你很懂this,是真的嗎?

請先回答第一個問題:如何準確判斷this指向的是什麼?【面試的高頻問題】

你真的理解this嗎

再看一道題,控制檯列印出來的值是什麼?【瀏覽器執行環境】

var number = 5;
var obj = {
    number: 3,
    fn1: (function () {
        var number;
        this.number *= 2;
        number = number * 2;
        number = 3;
        return function () {
            var num = this.number;
            this.number *= 2;
            console.log(num);
            number *= 3;
            console.log(number);
        }
    })()
}
var fn1 = obj.fn1;
fn1.call(null);
obj.fn1();
console.log(window.number);複製程式碼

如果你思考出來的結果,與在瀏覽中執行結果相同,並且每一步的依據都非常清楚,那麼,你可以選擇繼續往下閱讀,或者關閉本網頁,愉快得去玩耍。如果你是連猜帶蒙的,或者對自己的答案並不那麼確定,那麼請繼續往下閱讀。

畢竟花一兩個小時的時間,把this徹底搞明白,是一件很值得事情,不是嗎?

本文將細緻得講解this的繫結規則,並在最後剖析前文兩道題。

為什麼要學習this?

首先,我們為什麼要學習this?

  1. this使用頻率很高,如果我們不懂this,那麼在看別人的程式碼或者是原始碼的時候,就會很吃力。
  2. 工作中,濫用this,卻沒明白this指向的是什麼,而導致出現問題,但是自己卻不知道哪裡出問題了。【在公司,我至少幫10個以上的開發人員處理過這個問題】
  3. 合理的使用this,可以讓我們寫出簡潔且複用性高的程式碼。
  4. 面試的高頻問題,回答不好,抱歉,出門右拐,不送。

不管出於什麼目的,我們都需要把this這個知識點整的明明白白的。

OK,Let's go!

this是什麼?

言歸正傳,this是什麼?首先記住this不是指向自身!this 就是一個指標,指向呼叫函式的物件。這句話我們都知道,但是很多時候,我們未必能夠準確判斷出this究竟指向的是什麼?這就好像我們聽過很多道理 卻依然過不好這一生。今天我們們不探討如何過好一生的問題,但是呢,希望閱讀完下面的內容之後,你能夠一眼就看出this指向的是什麼。

為了能夠一眼看出this指向的是什麼,我們首先需要知道this的繫結規則有哪些?

  1. 預設繫結
  2. 隱式繫結
  3. 硬繫結
  4. new繫結

上面的名詞,你也許聽過,也許沒聽過,但是今天之後,請牢牢記住。我們將依次來進行解析。

預設繫結

預設繫結,在不能應用其它繫結規則時使用的預設規則,通常是獨立函式呼叫。

function sayHi(){
    console.log('Hello,', this.name);
}
var name = 'YvetteLau';
sayHi();複製程式碼

在呼叫Hi()時,應用了預設繫結,this指向全域性物件(非嚴格模式下),嚴格模式下,this指向undefined,undefined上沒有this物件,會丟擲錯誤。

上面的程式碼,如果在瀏覽器環境中執行,那麼結果就是 Hello,YvetteLau

但是如果在node環境中執行,結果就是 Hello,undefined.這是因為node中name並不是掛在全域性物件上的。

本文中,如不特殊說明,預設為瀏覽器環境執行結果。

隱式繫結

函式的呼叫是在某個物件上觸發的,即呼叫位置上存在上下文物件。典型的形式為 XXX.fun().我們來看一段程式碼:

function sayHi(){
    console.log('Hello,', this.name);
}
var person = {
    name: 'YvetteLau',
    sayHi: sayHi
}
var name = 'Wiliam';
person.sayHi();複製程式碼

列印的結果是 Hello,YvetteLau.

sayHi函式宣告在外部,嚴格來說並不屬於person,但是在呼叫sayHi時,呼叫位置會使用person的上下文來引用函式,隱式繫結會把函式呼叫中的this(即此例sayHi函式中的this)繫結到這個上下文物件(即此例中的person)

需要注意的是:物件屬性鏈中只有最後一層會影響到呼叫位置。

function sayHi(){
    console.log('Hello,', this.name);
}
var person2 = {
    name: 'Christina',
    sayHi: sayHi
}
var person1 = {
    name: 'YvetteLau',
    friend: person2
}
person1.friend.sayHi();複製程式碼

結果是:Hello, Christina.

因為只有最後一層會確定this指向的是什麼,不管有多少層,在判斷this的時候,我們只關注最後一層,即此處的friend。

隱式繫結有一個大陷阱,繫結很容易丟失(或者說容易給我們造成誤導,我們以為this指向的是什麼,但是實際上並非如此).

function sayHi(){
    console.log('Hello,', this.name);
}
var person = {
    name: 'YvetteLau',
    sayHi: sayHi
}
var name = 'Wiliam';
var Hi = person.sayHi;
Hi();複製程式碼

結果是: Hello,Wiliam.

這是為什麼呢,Hi直接指向了sayHi的引用,在呼叫的時候,跟person沒有半毛錢的關係,針對此類問題,我建議大家只需牢牢繼續這個格式:XXX.fn();fn()前如果什麼都沒有,那麼肯定不是隱式繫結,但是也不一定就是預設繫結,這裡有點小疑問,我們後來會說到。

除了上面這種丟失之外,隱式繫結的丟失是發生在回撥函式中(事件回撥也是其中一種),我們來看下面一個例子:

function sayHi(){
    console.log('Hello,', this.name);
}
var person1 = {
    name: 'YvetteLau',
    sayHi: function(){
        setTimeout(function(){
            console.log('Hello,',this.name);
        })
    }
}
var person2 = {
    name: 'Christina',
    sayHi: sayHi
}
var name='Wiliam';
person1.sayHi();
setTimeout(person2.sayHi,100);
setTimeout(function(){
    person2.sayHi();
},200);
複製程式碼

結果為:

Hello, Wiliam
Hello, Wiliam
Hello, Christina複製程式碼
  • 第一條輸出很容易理解,setTimeout的回撥函式中,this使用的是預設繫結,非嚴格模式下,執行的是全域性物件

  • 第二條輸出是不是有點迷惑了?說好XXX.fun()的時候,fun中的this指向的是XXX呢,為什麼這次卻不是這樣了!Why?

    其實這裡我們可以這樣理解: setTimeout(fn,delay){ fn(); },相當於是將person2.sayHi賦值給了一個變數,最後執行了變數,這個時候,sayHi中的this顯然和person2就沒有關係了。

  • 第三條雖然也是在setTimeout的回撥中,但是我們可以看出,這是執行的是person2.sayHi()使用的是隱式繫結,因此這是this指向的是person2,跟當前的作用域沒有任何關係。

讀到這裡,也許你已經有點疲倦了,但是答應我,別放棄,好嗎?再堅持一下,就可以掌握這個知識點了。


顯式繫結

顯式繫結比較好理解,就是通過call,apply,bind的方式,顯式的指定this所指向的物件。(注意:《你不知道的Javascript》中將bind單獨作為了硬繫結講解了)

call,apply和bind的第一個引數,就是對應函式的this所指向的物件。call和apply的作用一樣,只是傳參方式不同。call和apply都會執行對應的函式,而bind方法不會。

function sayHi(){
    console.log('Hello,', this.name);
}
var person = {
    name: 'YvetteLau',
    sayHi: sayHi
}
var name = 'Wiliam';
var Hi = person.sayHi;
Hi.call(person); //Hi.apply(person)複製程式碼

輸出的結果為: Hello, YvetteLau. 因為使用硬繫結明確將this繫結在了person上。

那麼,使用了硬繫結,是不是意味著不會出現隱式繫結所遇到的繫結丟失呢?顯然不是這樣的,不信,繼續往下看。

function sayHi(){
    console.log('Hello,', this.name);
}
var person = {
    name: 'YvetteLau',
    sayHi: sayHi
}
var name = 'Wiliam';
var Hi = function(fn) {
    fn();
}
Hi.call(person, person.sayHi); 複製程式碼

輸出的結果是 Hello, Wiliam. 原因很簡單,Hi.call(person, person.sayHi)的確是將this繫結到Hi中的this了。但是在執行fn的時候,相當於直接呼叫了sayHi方法(記住: person.sayHi已經被賦值給fn了,隱式繫結也丟了),沒有指定this的值,對應的是預設繫結。

現在,我們希望繫結不會丟失,要怎麼做?很簡單,呼叫fn的時候,也給它硬繫結。

function sayHi(){
    console.log('Hello,', this.name);
}
var person = {
    name: 'YvetteLau',
    sayHi: sayHi
}
var name = 'Wiliam';
var Hi = function(fn) {
    fn.call(this);
}
Hi.call(person, person.sayHi);複製程式碼

此時,輸出的結果為: Hello, YvetteLau,因為person被繫結到Hi函式中的this上,fn又將這個物件繫結給了sayHi的函式。這時,sayHi中的this指向的就是person物件。

至此,革命已經快勝利了,我們來看最後一種繫結規則: new 繫結。

new 繫結

javaScript和C++不一樣,並沒有類,在javaScript中,建構函式只是使用new操作符時被呼叫的函式,這些函式和普通的函式並沒有什麼不同,它不屬於某個類,也不可能例項化出一個類。任何一個函式都可以使用new來呼叫,因此其實並不存在建構函式,而只有對於函式的“構造呼叫”。

使用new來呼叫函式,會自動執行下面的操作:

  1. 建立一個新物件
  2. 將建構函式的作用域賦值給新物件,即this指向這個新物件
  3. 執行建構函式中的程式碼
  4. 返回新物件

因此,我們使用new來呼叫函式的時候,就會新物件繫結到這個函式的this上。

function sayHi(name){
    this.name = name;
	
}
var Hi = new sayHi('Yevtte');
console.log('Hello,', Hi.name);複製程式碼

輸出結果為 Hello, Yevtte, 原因是因為在var Hi = new sayHi('Yevtte');這一步,會將sayHi中的this繫結到Hi物件上。

繫結優先順序

我們知道了this有四種繫結規則,但是如果同時應用了多種規則,怎麼辦?

顯然,我們需要了解哪一種繫結方式的優先順序更高,這四種繫結的優先順序為:

new繫結 > 顯式繫結 > 隱式繫結 > 預設繫結

這個規則時如何得到的,大家如果有興趣,可以自己寫個demo去測試,或者記住上面的結論即可。

繫結例外

凡事都有例外,this的規則也是這樣。

如果我們將null或者是undefined作為this的繫結物件傳入call、apply或者是bind,這些值在呼叫時會被忽略,實際應用的是預設繫結規則。

var foo = {
    name: 'Selina'
}
var name = 'Chirs';
function bar() {
    console.log(this.name);
}
bar.call(null); //Chirs 複製程式碼

輸出的結果是 Chirs,因為這時實際應用的是預設繫結規則。

箭頭函式

箭頭函式是ES6中新增的,它和普通函式有一些區別,箭頭函式沒有自己的this,它的this繼承於外層程式碼庫中的this。箭頭函式在使用時,需要注意以下幾點:

(1)函式體內的this物件,繼承的是外層程式碼塊的this。

(2)不可以當作建構函式,也就是說,不可以使用new命令,否則會丟擲一個錯誤。

(3)不可以使用arguments物件,該物件在函式體內不存在。如果要用,可以用 rest 引數代替。

(4)不可以使用yield命令,因此箭頭函式不能用作 Generator 函式。

(5)箭頭函式沒有自己的this,所以不能用call()、apply()、bind()這些方法去改變this的指向.

OK,我們來看看箭頭函式的this是什麼?

var obj = {
    hi: function(){
        console.log(this);
        return ()=>{
            console.log(this);
        }
    },
    sayHi: function(){
        return function() {
            console.log(this);
            return ()=>{
                console.log(this);
            }
        }
    },
    say: ()=>{
        console.log(this);
    }
}
let hi = obj.hi();  //輸出obj物件
hi();               //輸出obj物件
let sayHi = obj.sayHi();
let fun1 = sayHi(); //輸出window
fun1();             //輸出window
obj.say();          //輸出window複製程式碼

那麼這是為什麼呢?如果大家說箭頭函式中的this是定義時所在的物件,這樣的結果顯示不是大家預期的,按照這個定義,say中的this應該是obj才對。

我們來分析一下上面的執行結果:

  1. obj.hi(); 對應了this的隱式繫結規則,this繫結在obj上,所以輸出obj,很好理解。
  2. hi(); 這一步執行的就是箭頭函式,箭頭函式繼承上一個程式碼庫的this,剛剛我們得出上一層的this是obj,顯然這裡的this就是obj.
  3. 執行sayHi();這一步也很好理解,我們前面說過這種隱式繫結丟失的情況,這個時候this執行的是預設繫結,this指向的是全域性物件window.
  4. fun1(); 這一步執行的是箭頭函式,如果按照之前的理解,this指向的是箭頭函式定義時所在的物件,那麼這兒顯然是說不通。OK,按照箭頭函式的this是繼承於外層程式碼庫的this就很好理解了。外層程式碼庫我們剛剛分析了,this指向的是window,因此這兒的輸出結果是window.
  5. obj.say(); 執行的是箭頭函式,當前的程式碼塊obj中是不存在this的,只能往上找,就找到了全域性的this,指向的是window.

你說箭頭函式的this是靜態的?

依舊是前面的程式碼。我們來看看箭頭函式中的this真的是靜態的嗎?

我要說:非也

var obj = {
    hi: function(){
        console.log(this);
        return ()=>{
            console.log(this);
        }
    },
    sayHi: function(){
        return function() {
            console.log(this);
            return ()=>{
                console.log(this);
            }
        }
    },
    say: ()=>{
        console.log(this);
    }
}
let sayHi = obj.sayHi();
let fun1 = sayHi(); //輸出window
fun1();             //輸出window

let fun2 = sayHi.bind(obj)();//輸出obj
fun2();                      //輸出obj複製程式碼

可以看出,fun1和fun2對應的是同樣的箭頭函式,但是this的輸出結果是不一樣的。

所以,請大家牢牢記住一點: 箭頭函式沒有自己的this,箭頭函式中的this繼承於外層程式碼庫中的this.

總結

關於this的規則,至此,就告一段落了,但是想要一眼就能看出this所繫結的物件,還需要不斷的訓練。

我們來回顧一下,最初的問題。

1. 如何準確判斷this指向的是什麼?

  1. 函式是否在new中呼叫(new繫結),如果是,那麼this繫結的是新建立的物件。
  2. 函式是否通過call,apply呼叫,或者使用了bind(即硬繫結),如果是,那麼this繫結的就是指定的物件。
  3. 函式是否在某個上下文物件中呼叫(隱式繫結),如果是的話,this繫結的是那個上下文物件。一般是obj.foo()
  4. 如果以上都不是,那麼使用預設繫結。如果在嚴格模式下,則繫結到undefined,否則繫結到全域性物件。
  5. 如果把Null或者undefined作為this的繫結物件傳入call、apply或者bind,這些值在呼叫時會被忽略,實際應用的是預設繫結規則。
  6. 如果是箭頭函式,箭頭函式的this繼承的是外層程式碼塊的this。

2. 執行過程解析

var number = 5;
var obj = {
    number: 3,
    fn: (function () {
        var number;
        this.number *= 2;
        number = number * 2;
        number = 3;
        return function () {
            var num = this.number;
            this.number *= 2;
            console.log(num);
            number *= 3;
            console.log(number);
        }
    })()
}
var myFun = obj.fn;
myFun.call(null);
obj.fn();
console.log(window.number);複製程式碼

我們來分析一下,這段程式碼的執行過程。

  1. 在定義obj的時候,fn對應的閉包就執行了,返回其中的函式,執行閉包中程式碼時,顯然應用不了new繫結(沒有出現new 關鍵字),硬繫結也沒有(沒有出現call,apply,bind關鍵字),隱式繫結有沒有?很顯然沒有,如果沒有XX.fn(),那麼可以肯定沒有應用隱式繫結,所以這裡應用的就是預設繫結了,非嚴格模式下this繫結到了window上(瀏覽器執行環境)。【這裡很容易被迷惑的就是以為this指向的是obj,一定要注意,除非是箭頭函式,否則this跟詞法作用域是兩回事,一定要牢記在心】
window.number * = 2; //window.number的值是10(var number定義的全域性變數是掛在window上的)

number = number * 2; //number的值是NaN;注意我們這邊定義了一個number,但是沒有賦值,number的值是undefined;Number(undefined)->NaN

number = 3;          //number的值為3複製程式碼
  1. myFun.call(null);我們前面說了,call的第一個引數傳null,呼叫的是預設繫結;
fn: function(){
    var num = this.number;
    this.number *= 2;
    console.log(num);
    number *= 3;
    console.log(number);
}複製程式碼

執行時:

var num = this.number; //num=10; 此時this指向的是window
this.number * = 2;     //window.number = 20
console.log(num);      //輸出結果為10
number *= 3;           //number=9; 這個number對應的閉包中的number;閉包中的number的是3
console.log(number);   //輸出的結果是9複製程式碼
  1. obj.fn();應用了隱式繫結,fn中的this對應的是obj.
var num = this.number;//num = 3;此時this指向的是obj
this.number *= 2;     //obj.number = 6;
console.log(num);     //輸出結果為3;
number *= 3;          //number=27;這個number對應的閉包中的number;閉包中的number的此時是9
console.log(number);  //輸出的結果是27複製程式碼
  1. 最後一步console.log(window.number);輸出的結果是20

因此組中結果為:

10
9
3
27
20複製程式碼

嚴格模式下結果,大家根據今天所學,自己分析,鞏固一下知識點。


相關文章