1. 程式人生 > 其它 >JavaScript this 關鍵詞

JavaScript this 關鍵詞

當一個函式呼叫時,會建立一個執行上下文,這個上下文包括函式呼叫的一些資訊(呼叫棧,傳入引數,呼叫方式),this就指向這個執行上下文。

this不是靜態的,也並不是在編寫的時候繫結的,而是在執行時繫結的。它的繫結和函式宣告的位置沒有關係,只取決於函式呼叫的方式。

this的繫結規則一共有五種:

1、預設繫結

2、隱式繫結

3、顯示繫結

4、new繫結

5、ES6新增箭頭函式繫結

預設繫結

預設繫結通常是指函式獨立呼叫,不涉及其他繫結規則。在嚴格模式和非嚴格模式下,this的指向是不同的

1、非嚴格模式

非嚴格模式,print()獨立呼叫即為預設繫結,this指向window,所以列印this就是列印window。

其實定義的foo和print方法都是在window下的,this指向的是window,所以為this.foo賦值後,之前定義的foo就會變為234

2、嚴格模式

 嚴格模式下,函式內部的this指向undefined

 3、let/const

 let/const定義的變數存在暫時性死區,而且不會掛載到window物件上,因此print中是無法獲取到a和b的。

4、物件內執行

 foo雖然在objbar函式中,但foo函式仍然是獨立執行的,foo中的this依舊指向window物件。

5、方法內執行

var a = 1
function outer () {
  var a = 2
  function
inner () { console.log(this.a) // 1 console.log(this) } inner() } outer()

同4

6、自執行函式

 預設情況下,自執行函式的this指向window

隱式繫結

1、隱式繫結

  • foo(): 預設繫結,列印1和window
  • obj.foo(): 隱式繫結,列印2和obj

obj是通過var定義的,obj會掛載到window之上的,obj.foo()就相當於window.obj.foo(),這也印證了this永遠指向最後呼叫它的那個物件規則。

2、物件鏈式呼叫

 可以看出this指向的是最後呼叫它的obj2
3、隱式繫結的丟失

隱式繫結可是個調皮的東西,一不小心它就會發生繫結的丟失。一般會有兩種常見的丟失:

  • 使用另一個變數作為函式別名,之後使用別名執行函式
  • 將函式作為引數傳遞時會被隱式賦值

隱式繫結丟失之後,this的指向會啟用預設繫結。

取函式別名

JavaScript對於引用型別,其地址指標存放在棧記憶體中,真正的本體是存放在堆記憶體中的。

上面將obj.foo賦值給foo,就是將foo也指向了obj.foo所指向的堆記憶體,此後再執行foo,相當於直接執行的堆記憶體的函式,與obj無關,foo為預設繫結。籠統的記,只要fn前面什麼都沒有,肯定不是隱式繫結


函式作為引數傳遞

 用函式預編譯的知識來解答這個問題:函式預編譯四部曲前兩步分別是:

  1. 找形參和變數宣告,值賦予undefined
  2. 將形參與實參相統一,也就是將實參的值賦予形參。

obj.foo作為實參,在預編譯時將其值賦值給形參fn,是將obj.foo指向的地址賦給了fn,此後fn執行不會與obj產生任何關係。fn為預設繫結。

回撥函式

var name='zcxiaobao';
function introduce(){
    console.log('Hello,My name is ', this.name);
}
const Tom = {
    name: 'TOM',
    introduce: function(){
        setTimeout(function(){
            console.log(this)
            console.log('Hello, My name is ',this.name);
        })
    }
}
const Mary = {
    name: 'Mary',
    introduce
}
const Lisa = {
    name: 'Lisa',
    introduce
}

Tom.introduce();
setTimeout(Mary.introduce, 100);
setTimeout(function(){
    Lisa.introduce();
},200);

Tom.introduce()執行: console位於setTimeout的回撥函式中,是獨立執行的,所以此時this指向window
Mary.introduce直接作為setTimeout的函式引數,實際不受穿入引數影響,會發生上面說的隱式繫結丟失,變為預設繫結
Lisa.introduce執行雖然位於setTimeout的回撥函式中,但保持xxx.fn()模式,為隱式繫結,this此時指向xxx,即Lisa

所以如果我們想在setTimeoutsetInterval中使用外界的this,需要提前儲存一下,避免this的丟失。

顯式繫結

 顯式繫結比較好理解,就是通過call()、apply()、bind()等方法,強行改變this指向。

上面的方法雖然都可以改變this指向,但使用起來略有差別:

  • call()和apply()函式會立即執行
  • bind()函式會返回新函式,不會立即執行函式
  • call()和apply()的區別在於call接受若干個引數,apply接受陣列。

三種呼叫方式

  • foo(): 預設繫結。
  • foo.call(obj): 顯示繫結,foothis指向obj
  • foo.apply(obj): 同call
  • foo.bind(obj): 顯式繫結,但不會立即執行函式,沒有返回值

通過顯式繫結修復隱式繫結丟失

 1、首先修正講doFoo的this指向obj

2、修正fn的this

回撥函式與call

 注意call的位置1

  • foo(): 預設繫結
  • foo.call(obj): 顯式繫結
  • foo().call(obj): 對foo()執行的返回值執行callfoo返回值為undefined,執行call()會報錯

 注意call的位置2

  • foo(): 預設繫結
  • foo.call(obj): 顯式繫結
  • foo().call(obj): foo()執行,列印2,返回匿名函式通過callthis指向obj,列印1

這裡千萬注意:最後一個foo().call(obj)有兩個函式執行,會列印2個值

bind

call會立即執行函式,而bind會返回一個新函式,但不會執行

首先我們要先確定,最後會輸出幾個值?bind不會執行函式,因此只有兩個foo()會列印a

  • foo(): 預設繫結,列印2
  • foo.bind(obj): 返回新函式,不會執行函式,無輸出
  • foo().bind(obj): 第一層foo(),預設繫結,列印2,後bindfoo()返回的匿名函式this指向obj,不執行

外層this與內層this
如果使用call、bind等修改了外層函式的this,那內層函式的this會受影響嗎?

 foo.call(obj): 第一層函式foo通過callthis指向obj,列印1;第二層函式為匿名函式,預設繫結,列印2

物件中的call

  • obj.foo()(): 第一層obj.foo()執行為隱式繫結,打印出foo:obj;第二層匿名函式為預設繫結,列印inner:window
  • obj.foo.call(obj2)(): 類似題目4.7,第一層obj.foo.call(obj2)使用callobj.foothis指向obj2,列印foo: obj2;第二層匿名函式預設繫結,列印inner:window
  • obj.foo().call(obj2): 類似題目4.5,第一層隱式繫結,列印:foo: obj,第二層匿名函式使用callthis指向obj2,列印inner: obj2

帶引數的call

要注意call執行的位置:

  • obj.foo(a).call(obj2, 1):
    • obj.foo(a): foo的AO中b值為傳入的a(形參與實參相統一),值為2,返回匿名函式fn
    • 匿名函式fn.call(obj2, 1): fn的this指向為obj2,c值為1
    • this.a + b + c = obj2.a + FooAO.b + c = 3 + 2 + 1 = 6
  • obj.foo.call(obj2)(1):
    • obj.foo.call(obj2): obj.foo的this指向obj2,未傳入引數,b = this.a = obj2.a = 3;返回匿名函式fn
    • 匿名函式fn(1): c = 1,預設繫結,this指向window
    • this.a + b + c = window.a + obj2.a + c = 2 + 3 + 1 = 6

顯式繫結擴充套件

上面提了很多call/apply可以改變this指向,但都沒有太多實用性。下面來一起學幾個常用的call與apply使用。

apply求陣列最值

JavaScript中沒有給陣列提供類似max和min函式,只提供了Math.max/min,用於求多個數的最值,所以可以藉助apply方法,直接傳遞陣列給Math.max/min

類陣列轉為陣列

 ES6未釋出之前,沒有Array.from方法可以將類陣列轉為陣列,採用Array.prototype.slice.call(arguments)[].slice.call(arguments)將類陣列轉化為陣列。

es6 的Array.from

陣列高階函式

日常編碼中,我們會經常用到forEach、map等,但這些陣列高階方法,它們還有第二個引數thisArg,每一個回撥函式都是顯式繫結在thisArg上的。

例如下面這個例子

這個例子裡的function顯示繫結在第二個引數,即obj上

new繫結

使用new來構建函式,會執行如下四部操作:

  1. 建立一個空的簡單JavaScript物件(即{});
  2. 為新建立的空物件新增屬性__proto__,將該屬性連結至建構函式的原型物件 ;
  3. 為新建立的空物件作為this的上下文 ;
  4. 如果該函式沒有返回物件,則返回this
通過new來呼叫建構函式,會生成一個新物件,並且把這個新物件繫結為呼叫函式的this。

new繫結

 屬性加方法

  • zc.introduce(): zc是new建立的例項,this指向zc,列印zc
  • zc.howOld()(): zc.howOld()返回一個匿名函式,匿名函式為預設繫結,因此列印18

new界的天王山

 分析後面三個列印結果之前,先補充一些運算子優先順序方面的知識

從上圖可以看到,部分優先順序如下:new(帶引數列表) = 成員訪問 = 函式呼叫 > new(不帶引數列表)

new Foo.getName()

首先從左往右看:new Foo屬於不帶引數列表的new(優先順序19),Foo.getName屬於成員訪問(優先順序20),getName()屬於函式呼叫(優先順序20),同樣優先順序遵循從左往右執行。

  • Foo.getName執行,獲取到Foo上的getName屬性
  • 此時原表示式變為new (Foo.getName)()new (Foo.getName)()為帶引數列表(優先順序20),(Foo.getName)()屬於函式呼叫(優先順序20),從左往右執行
  • new (Foo.getName)()執行,列印2,並返回一個以Foo.getName()為建構函式的例項

這裡有一個誤區:很多人認為這裡的new是沒做任何操作的的,執行的是函式呼叫。那麼如果執行的是Foo.getName(),呼叫返回值為undefinednew undefined會發生報錯,並且我們可以驗證一下該表示式的返回結果。

 可見在成員訪問之後,執行的是帶引數列表格式的new操作。

new Foo().getName()

  • 同上一樣分析,先執行new Foo(),返回一個以Foo為建構函式的例項
  • Foo的例項物件上沒有getName方法,沿原型鏈查詢到Foo.prototype.getName方法,列印3

new new Foo().getName()

從左往右分析: 第一個new不帶引數列表(優先順序19),new Foo()帶引數列表(優先順序20),剩下的成員訪問和函式呼叫優先順序都是20

  • new Foo()執行,返回一個以Foo為建構函式的例項
  • 在執行成員訪問,Foo例項物件在Foo.prototype查詢到getName屬性
  • 執行new (new Foo().getName)(),返回一個以 Foo.prototype.getName()為建構函式的例項,列印3

箭頭函式

箭頭函式沒有自己的this,它的this指向外層作用域的this,且指向函式定義時的this而非執行時。

  1. this指向外層作用域的this: 箭頭函式沒有this繫結,但它可以通過作用域鏈查到外層作用域的this
  2. 指向函式定義時的this而非執行時: JavaScript是靜態作用域,就是函式定義之後,作用域就定死了,跟它執行時的地方無關。更詳細的介紹見JavaScript之靜態作用域與動態作用域

物件方法使用箭頭函式

 上文說到,箭頭函式的this通過作用域鏈查到,intro函式的上層作用域為window

箭頭函式與普通函式比較

  • obj.intro2()(): 不做贅述,列印My name is tom
  • obj.intro()(): obj.intro()返回箭頭函式,箭頭函式的this取決於它的外層作用域,因此箭頭函式的this指向obj,列印My name is zc

箭頭函式與普通函式的巢狀

  • obj1.intro()(): 類似題目7.2,列印obj1,obj1
  • obj2.intro()(): obj2.intro()為箭頭函式,this為外層作用域this,指向window。返回匿名函式為預設繫結。列印window,window
  • obj3.intro()(): obj3.intro()obj2.intro()相同,返回值為箭頭函式,外層作用域introthis指向window,列印window,window

new碰上箭頭函式


  • zcnew User例項,因此建構函式Userthis指向zc
  • zc.intro(): 列印My name is zc
  • zc.howOld(): howOld為箭頭函式,箭頭函式this由外層作用域決定,且指向函式定義時的this,外層作用域為Userthis指向zc,列印My age is 24

call碰上箭頭函式

箭頭函式由於沒有this,不能通過call\apply\bind來修改this指向,但可以通過修改外層作用域的this來達成間接修改

  • obj1.intro.call(obj2)(): 第一層函式為普通函式,通過call修改thisobj2,列印obj2。第二層函式為箭頭函式,它的this與外層this相同,同樣列印obj2
  • obj1.intro().call(obj2): 第一層函式列印obj1,第二次函式為箭頭函式,call無效,它的this與外層this相同,列印obj1
  • obj1.intro2.call(obj2)(): 第一層為箭頭函式,call無效,外層作用域為window,列印window;第二次為普通匿名函式,預設繫結,列印window
  • obj1.intro2().call(obj2): 與上同,列印window;第二層為匿名函式,call修改thisobj2,列印obj2

箭頭函式擴充套件

總結

  • 箭頭函式沒有this,它的this是通過作用域鏈查到外層作用域的this,且指向函式定義時的this而非執行時。
  • 不可以用作建構函式,不能使用new命令,否則會報錯
  • 箭頭函式沒有arguments物件,如果要用,使用rest引數代替
  • 不可以使用yield命令,因此箭頭函式不能用作Generator函式。
  • 不能用call/apply/bind修改this指向,但可以通過修改外層作用域的this來間接修改。
  • 箭頭函式沒有prototype屬性。

避免使用場景

箭頭函式定義物件方法

箭頭函式不能作為建構函式

綜合題

物件綜合體

隱式繫結丟失

  • foo.bar(): 隱式繫結,列印20
  • (foo.bar)(): 上面提到過運算子優先順序的知識,成員訪問與函式呼叫優先順序相同,預設從左到右,因此括號可有可無,隱式繫結,列印20
  • (foo.bar = foo.bar)():隱式繫結丟失,給foo.bar起別名,雖然名字沒變,但是foo.bar上已經跟foo無關了,預設繫結,列印10
  • (foo.bar, foo.bar)(): 隱式繫結丟失,起函式別名,將逗號表示式的值(第二個foo.bar)賦值給新變數,之後執行新變數所指向的函式,預設繫結,列印10

上面那說法有可能有幾分難理解,隱式繫結有個定性條件,就是要滿足XXX.fn()格式,如果破壞了這種格式,一般隱式繫結都會丟失。

arguments

這個題要注意一下,有坑。

  • fn(): 預設繫結,列印10

  • arguments[0](): 這種執行方式看起來就怪怪的,咱們把它展開來看看:

    1. arguments是一個類陣列,arguments展開,應該是下面這樣:
    arguments: {
        0: fn,
        1: 1,
        length: 2
    }
    
    1. arguments[0]: 這是訪問物件的屬性0?0不好理解,咱們把它稍微一換,方便一下理解:
    arguments: {
        fn: fn,
        1: 1,
        length: 2
    }
    
    1. 到這裡大家應該就懂了,隱式繫結,fn函式this指向arguments,列印2

壓軸題

fn.call(null) 或者 fn.call(undefined) 都相當於fn()

  1. obj.fn為立即執行函式: 預設繫結,this指向window

    我們來一句一句的分析:

    • var number: 立即執行函式的AO中新增number屬性,值為undefined
    • this.number *= 2: window.number = 10
    • number = number * 2: 立即執行函式AOnumber值為undefined,賦值後為NaN
    • number = 3: AOnumber值由NaN修改為3
    • 返回匿名函式,形成閉包

    此時的obj可以類似的看成以下程式碼(注意存在閉包):

    obj = {
       number: 3,
       fn: function () {
            var num = this.number;
            this.number *= 2;
            console.log(num);
            number *= 3;
            console.log(number);
        }
    }
    複製程式碼
  2. myFun.call(null): 相當於myFun(),隱式繫結丟失,myFunthis指向window

    依舊一句一句的分析:

    • var num = this.number: this指向windownum = window.num = 10
    • this.number *= 2: window.number = 20
    • console.log(num): 列印10
    • number *= 3: 當前AO中沒有number屬性,沿作用域鏈可在立即執行函式的AO中查到number屬性,修改其值為9
    • console.log(number): 列印立即執行函式AO中的number,列印9
  3. obj.fn(): 隱式繫結,fnthis指向obj

    繼續一步一步的分析:

    • var num = this.number: this->objnum = obj.num = 3
    • this.number *= 2: obj.number *= 2 = 6
    • console.log(num): 列印num值,列印3
    • number *= 3: 當前AO中不存在number,繼續修改立即執行函式AO中的numbernumber *= 3 = 27
    • console.log(number): 列印27
  4. console.log(window.number): 列印20

這裡解釋一下,為什麼myFun.call(null)執行時,找不到number變數,是去找立即執行函式AO中的number,而不是找window.number: JavaScript採用的靜態作用域,當定義函式後,作用域鏈就已經定死。(更詳細的解釋文章最開始的推薦中有)

總結

  • 預設繫結: 非嚴格模式下this指向全域性物件,嚴格模式下this會繫結到undefined
  • 隱式繫結: 滿足XXX.fn()格式,fnthis指向XXX。如果存在鏈式呼叫,this永遠指向最後呼叫它的那個物件
  • 隱式繫結丟失:起函式別名,通過別名執行;函式作為引數會造成隱式繫結丟失。
  • 顯示繫結: 通過call/apply/bind修改this指向
  • new繫結: 通過new來呼叫建構函式,會生成一個新物件,並且把這個新物件繫結為呼叫函式的this
  • 箭頭函式繫結: 箭頭函式沒有this,它的this是通過作用域鏈查到外層作用域的this,且指向函式定義時的this而非執行時
轉載於https://juejin.cn/post/7019470820057546766