探索JavaScript的this機制

cTomorrow發表於2019-04-08

前言

白的不能再白的小白,通過各種渠道學習到了this,和大家分享,有什麼錯誤的話還望指出,共同學習進步。覺得好的話還望點個小贊,為繼續堅持寫文章增加動力。

正文

我在想寫這篇文章的時候,我是無從下手的。因為我自己也同樣說不清在JavaScript中this到底是什麼?我們都知道在一些強型別語言中(像java)對this有一個很好的定義:this就是一個指標,指向當前物件。在JavaScript中如果用上面這種方法來表示this的話,是不完全的。對於我來說,this就像JavaScript中的魔術師,千變萬化中又有著那麼一絲絲小套路。

何為this?

this是JavaScript語法中很難捉摸透的一個東西。我們都知道JavaScript執行機制分為四個步驟:詞法分析語法分析預編譯解釋執行(執行時)。我們在執行程式碼的時候,通常是在全域性中執行一個函式或者通過物件方法呼叫執行函式,在函式呼叫過程中得到this。也就是說this這個東西就生在執行時,即this是在執行時繫結的,而非編譯時

JavaScript中函式呼叫的幾種方式

預設呼叫

在JavaScript中呼叫函式的最常見的一種方法就是預設呼叫。

function test(){
    console.log(this);
}
test()
複製程式碼

正如上面程式碼這樣呼叫test函式,就是預設呼叫。

預設呼叫的情況下,函式的this指向全域性物件。

我們都知道在ES5出現了嚴格模式這個概念。在嚴格模式下全域性物件的this指向undefined,非嚴格模式下全域性物件的this指向Window物件。

//嚴格模式
"use strict";
function test(){
    console.log(this);  //undefined
}
test()
複製程式碼
//非嚴格模式下
function test(){
    console.log(this);  //window
}
test();
複製程式碼

IIFE

立即執行函式在執行的時候會形成一個閉包,其中的this永遠指向window

(function(){
    console.log(this);  //window
}())
複製程式碼

隱式繫結

隱式繫結也可以說成作為物件的方法來呼叫。看下面程式碼

function test(){
    console.log(this.a);
}
var obj = {
    a : 2,
    test:test,
}
obj.test(); //2
複製程式碼

這裡可以看出obj中有一個屬性為test,它的屬性名為test函式。當test這個屬性被建立的時候,會在記憶體中開闢一小段空間,而記憶體中所存的值就是test函式所在記憶體的地址。此時就把test函式隱式的繫結到了obj上面,當使用obj呼叫test的時候,test中的this就指向了呼叫test的物件。

這裡就可以總結出一個套路:誰呼叫函式,函式的this就指向誰

另外一種情況:

function foo(){
    console.log(this.a);
}
var obj2 = {
    a:1,
    foo :foo,
}
var obj1 = {
    a : 2,
    obj2 : obj2
}
obj1.obj2.foo() //1
複製程式碼

當物件方法呼叫鏈很長的時候,只有方法引用鏈的上一層或者說是最後一層在呼叫位置中起作用。上面程式碼中只有obj2會起作用。

隱式繫結中還有一個現象是隱式丟失,也就是說當把一個物件的方法暴露在外部的時候(也就是說把物件的方法賦於一個全域性的變數),再通過預設呼叫的方式呼叫這個函式,此時的this就不會再指向物件,而是指向window。

var obj  ={
    foo : function(){
        console.log(this);
    }
}
obj.foo()  //obj
var fun = obj.foo; //把物件的方法暴露在外部
fun();  //window
複製程式碼

顯示繫結

顯示繫結也可以稱之為硬繫結。所謂硬繫結就是使用一種手段強制的改變函式的this執行,這種強制的手段通過call、apply、bind等函式的API來實現。這裡由於說的是this機制,就先不提call、apply、bind等原理。

Function.prototype.call

call的作用就是改變函式呼叫中的this指向,call方法第一個引數就是繫結的this到哪個物件上,後面的引數是傳遞函式的實參。

function test(name,age){
    console.log(name);
    console.log(age);
    console.log(this);
}
var obj ={}
test.call(obj,'xiaoming',18);    // xiaohong 18 obj
複製程式碼
Function.prototype.apply

apply的作用和call的作用相同,同樣是為了改變this指向,唯一不同的區別是call在傳遞函式實參的時候是通過分來傳參,apply函式傳參是通過傳遞一個陣列。

function test(name,age){
    console.log(name);
    console.log(age);
    console.log(this);
}
var obj ={}
test.call(obj,['xiaoming',18]);    // xiaohong 18 obj
複製程式碼
Function.prototype.bind

bind是ES5出現的一個API,同樣也是可以改變this指向。bind和上面兩個API有很大的不同之處,call和apply是立即執行所呼叫的函式。bind是返回一個新函式,並且支援柯里化(分佈傳參)。

function test(name,age){
    console.log(name);
    console.log(age);
    console.log(this);
}
var obj ={}
var fun = test.bind(obj,'xiaoming');    //呼叫bind方法,第一個引數傳入所要繫結的物件,第二個引數傳入name,並且返回一個新函式。
fun(18);        //傳遞剩餘的引數。
                //輸出 xiaohong 18 obj
複製程式碼

建構函式呼叫

JavaScript中的物件導向程式設計是很模糊的,起初這個語言並不是為物件導向程式設計而設計的,JavaScript語言的特性更類似於函數語言程式設計。我們為了使用JavaScript模擬物件導向程式設計,從而有了建構函式的概念:使用new呼叫的函式稱為構造呼叫,而這個函式被稱為建構函式。

發生建構函式呼叫會自動執行以下操作:

  1. 建立一個全新的物件。
  2. 這個新物件會被執行[[Prototype]連線
  3. 這個新物件會繫結到函式呼叫的this
  4. 如果函式沒有返回其他物件(物件、陣列、函式),那麼new表示式中的函式呼叫會自動返回這個新物件。
function Test(name,age){
    this.name = name;
    this.age = age;
}
var test = new Test('xiaoming',18);
console.log(test)   //Test{}    
複製程式碼

函式呼叫的優先順序

那麼多函式呼叫方式,如果同時出現的話究竟哪個優先順序高,哪個優先順序低呢?

預設呼叫和隱式繫結

先來看一下最基礎的兩種

function test(){
    console.log(this);
}
//預設呼叫
test(); //window
var obj = {
    test : test,
}
//隱式繫結
obj.test(); //obj
複製程式碼

可以明顯的看出,同一個函式預設呼叫的情況下this為window,隱式繫結的情況下this指向obj,很明顯隱式繫結改變了預設呼叫的this,所以隱式繫結的優先順序比預設呼叫的優先順序高

隱式繫結和顯示繫結

再來看一下隱式繫結

var temp = {
    a : 1,
}
var obj = {
    a : 2
    foo : function(){
        console.log(this.a);
    }
}
//隱式繫結
obj.foo()   //2
//顯示繫結
obj.foo.call(temp); //1
複製程式碼

我們可以發現顯示繫結改變了隱式繫結的this,即顯示繫結的優先順序大於隱式繫結

顯示繫結和構造呼叫

如果讓顯示繫結和構造呼叫相比的話,就需要bind了,因為call和apply都是直接呼叫,沒辦法和構造呼叫相比,而bing則是返回一個新函式,可以使用構造呼叫。

var obj = {
    a : 1,
}
function Test(){
    console.log(this.a);
}
//顯示繫結
var fun = Test.bind(obj); //此時函式已經繫結了obj物件。
fun();      //執行fun,輸出1
//構造呼叫
new fun(); //unfeinde
複製程式碼

我們發現,fun此時已經繫結了obj,輸出1,對的沒有關係,使用new fun怎麼就輸出undefined了呢,其實這裡輸出的是fun建構函式返回的物件。新建立的物件上沒有a屬性,我們來修改一行程式碼再測試下。

function Test(){
    console.log(this)
}
複製程式碼

重新呼叫

探索JavaScript的this機制
得出結論建構函式呼叫比顯示繫結的優先順序高,這裡涉及到bind原理的東西,目前沒有寫這類文章,喜歡探索的可以自行google下。

所以最後得出的結論是:建構函式呼叫>顯示繫結>隱式繫結>預設呼叫

判斷this規則

  1. 判斷是否是立即執行函式,如果是則this為window,如果不是移步第2步驟。
  2. 判斷this是否是通過建構函式呼叫,如果是this就繫結新建立的物件,如果不是移步第3步驟。
  3. 判斷是否是通過call、apply、bind等繫結,如果是this繫結指定物件,如果不是移步第4步驟。
  4. 判斷函式是夠是在某個上下文物件中呼叫,如果是this就繫結在上下文物件上,如果不是請移步第5步驟。
  5. 如果上面都不是的話,那麼函式使用預設呼叫,如果是嚴格模式,則this為undefined,如果是非嚴格模式,則this為window。

箭頭函式的this

起這個小標題說實話不太想起,因為箭頭函式中是沒有this的,怎麼說呢,請移步軟大大的箭頭函式。在其他函式中this是可變的,在箭頭函式中this是固定的,同樣也是因為箭頭函式中沒有this。在你不知道的JavaScript(上)中有那麼一句話:箭頭函式在涉及this繫結的行為和普通的行為完全不一致。它放棄了所有普通this繫結的規則,取而代之的是用當前的詞法作用域覆蓋了this本來的值。

這句話和上面其實是不矛盾的,所謂詞法通俗的將就是書寫程式碼的位置。箭頭函式書寫在什麼地方,它的this就是所處環境的執行上下文物件的this。

var a  = 2;
var obj = {
    a : 1,
    foo : ()=> {
        console.log(this.a);
    }
}
//此時obj定義在全域性內,箭頭函式中的this為window,故:輸出2
obj.foo();  //2
複製程式碼
var a  = 2;
var obj = {
    a : 1,
    fun : function(){
        var foo = ()=>{
            console.log(this.a);
        }
        foo();
    }
}
//此時foo定義在fun函式中,執行fun函式,fun函式中的this為obj,所以箭頭函式中的this為obj。故:輸出1
obj.fun();  //1
複製程式碼

總結

  1. 如果是箭頭函式,則就判斷箭頭函式定義在什麼地方,this就是什麼。
  2. 如果是立即執行函式,則this就是windows
  3. 其他形式則按照判斷this的規則來。

文章總結自:你不知道的JavaScript(上),安利一波,值得一讀。

原文發自:探索JavaScript中this機制

相關文章