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
雖然在obj
的bar
函式中,但foo
函式仍然是獨立執行的,foo
中的this
依舊指向window
物件。
5、方法內執行
var a = 1 function outer () { var a = 2 functioninner () { 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前面什麼都沒有,肯定不是隱式繫結。
函式作為引數傳遞
用函式預編譯的知識來解答這個問題:函式預編譯四部曲前兩步分別是:
- 找形參和變數宣告,值賦予
undefined
- 將形參與實參相統一,也就是將實參的值賦予形參。
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
所以如果我們想在setTimeout
或setInterval
中使用外界的this
,需要提前儲存一下,避免this
的丟失。
顯式繫結
顯式繫結比較好理解,就是通過call()、apply()、bind()
等方法,強行改變this
指向。
上面的方法雖然都可以改變this
指向,但使用起來略有差別:
-
call()和apply()
函式會立即執行 -
bind()
函式會返回新函式,不會立即執行函式 -
call()和apply()
的區別在於call
接受若干個引數,apply
接受陣列。
三種呼叫方式
-
foo()
: 預設繫結。 -
foo.call(obj)
: 顯示繫結,foo
的this
指向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()
執行的返回值執行call
,foo
返回值為undefined
,執行call()
會報錯
注意call的位置2
-
foo()
: 預設繫結 -
foo.call(obj)
: 顯式繫結 -
foo().call(obj)
:foo()
執行,列印2
,返回匿名函式通過call
將this
指向obj
,列印1
。
這裡千萬注意:最後一個foo().call(obj)
有兩個函式執行,會列印2個值。
bind
call會立即執行函式,而bind會返回一個新函式,但不會執行
首先我們要先確定,最後會輸出幾個值?bind
不會執行函式,因此只有兩個foo()
會列印a
。
-
foo()
: 預設繫結,列印2
-
foo.bind(obj)
: 返回新函式,不會執行函式,無輸出 -
foo().bind(obj)
: 第一層foo()
,預設繫結,列印2
,後bind
將foo()
返回的匿名函式this
指向obj
,不執行
外層this與內層this
如果使用call、bind
等修改了外層函式的this
,那內層函式的this
會受影響嗎?
foo.call(obj)
: 第一層函式foo
通過call
將this
指向obj
,列印1
;第二層函式為匿名函式,預設繫結,列印2
。
物件中的call
-
obj.foo()()
: 第一層obj.foo()
執行為隱式繫結,打印出foo:obj
;第二層匿名函式為預設繫結,列印inner:window
-
obj.foo.call(obj2)()
: 類似題目4.7
,第一層obj.foo.call(obj2)
使用call
將obj.foo
的this
指向obj2
,列印foo: obj2
;第二層匿名函式預設繫結,列印inner:window
-
obj.foo().call(obj2)
: 類似題目4.5
,第一層隱式繫結,列印:foo: obj
,第二層匿名函式使用call
將this
指向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
來構建函式,會執行如下四部操作:
- 建立一個空的簡單
JavaScript
物件(即{}
); - 為新建立的空物件新增屬性
__proto__
,將該屬性連結至建構函式的原型物件 ; - 為新建立的空物件作為
this
的上下文 ; - 如果該函式沒有返回物件,則返回
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()
,呼叫返回值為undefined
,new 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
而非執行時。
this指向外層作用域的this
: 箭頭函式沒有this
繫結,但它可以通過作用域鏈查到外層作用域的this
指向函式定義時的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()
相同,返回值為箭頭函式,外層作用域intro
的this
指向window
,列印window,window
new碰上箭頭函式
zc
是new User
例項,因此建構函式User
的this
指向zc
zc.intro()
: 列印My name is zc
zc.howOld()
:howOld
為箭頭函式,箭頭函式this由外層作用域決定,且指向函式定義時的this,外層作用域為User
,this
指向zc
,列印My age is 24
call碰上箭頭函式
箭頭函式由於沒有
this
,不能通過call\apply\bind
來修改this
指向,但可以通過修改外層作用域的this
來達成間接修改
obj1.intro.call(obj2)()
: 第一層函式為普通函式,通過call
修改this
為obj2
,列印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
修改this
為obj2
,列印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]()
: 這種執行方式看起來就怪怪的,咱們把它展開來看看:
arguments
是一個類陣列,arguments
展開,應該是下面這樣:
arguments: { 0: fn, 1: 1, length: 2 }
arguments[0]
: 這是訪問物件的屬性0?0不好理解,咱們把它稍微一換,方便一下理解:
arguments: { fn: fn, 1: 1, length: 2 }
- 到這裡大家應該就懂了,隱式繫結,
fn
函式this
指向arguments
,列印2壓軸題
fn.call(null)
或者fn.call(undefined)
都相當於fn()
obj.fn
為立即執行函式: 預設繫結,this
指向window
我們來一句一句的分析:
var number
: 立即執行函式的AO
中新增number
屬性,值為undefined
this.number *= 2
:window.number = 10
number = number * 2
: 立即執行函式AO
中number
值為undefined
,賦值後為NaN
number = 3
:AO
中number
值由NaN
修改為3
- 返回匿名函式,形成閉包
此時的obj可以類似的看成以下程式碼(注意存在閉包):
obj = { number: 3, fn: function () { var num = this.number; this.number *= 2; console.log(num); number *= 3; console.log(number); } } 複製程式碼
myFun.call(null)
: 相當於myFun()
,隱式繫結丟失,myFun
的this
指向window
。依舊一句一句的分析:
var num = this.number
:this
指向window
,num = window.num = 10
this.number *= 2
:window.number = 20
console.log(num)
: 列印10number *= 3
: 當前AO
中沒有number
屬性,沿作用域鏈可在立即執行函式的AO
中查到number
屬性,修改其值為9
console.log(number)
: 列印立即執行函式AO
中的number
,列印9
obj.fn()
: 隱式繫結,fn
的this
指向obj
繼續一步一步的分析:
var num = this.number
:this->obj
,num = obj.num = 3
this.number *= 2
:obj.number *= 2 = 6
console.log(num)
: 列印num
值,列印3number *= 3
: 當前AO
中不存在number
,繼續修改立即執行函式AO
中的number
,number *= 3 = 27
console.log(number)
: 列印27
console.log(window.number)
: 列印20轉載於https://juejin.cn/post/7019470820057546766這裡解釋一下,為什麼
myFun.call(null)
執行時,找不到number
變數,是去找立即執行函式AO
中的number
,而不是找window.number
: JavaScript採用的靜態作用域,當定義函式後,作用域鏈就已經定死。(更詳細的解釋文章最開始的推薦中有)總結
- 預設繫結: 非嚴格模式下
this
指向全域性物件,嚴格模式下this
會繫結到undefined
- 隱式繫結: 滿足
XXX.fn()
格式,fn
的this
指向XXX
。如果存在鏈式呼叫,this永遠指向最後呼叫它的那個物件- 隱式繫結丟失:起函式別名,通過別名執行;函式作為引數會造成隱式繫結丟失。
- 顯示繫結: 通過
call/apply/bind
修改this
指向new
繫結: 通過new
來呼叫建構函式,會生成一個新物件,並且把這個新物件繫結為呼叫函式的this
。- 箭頭函式繫結: 箭頭函式沒有
this
,它的this
是通過作用域鏈查到外層作用域的this
,且指向函式定義時的this
而非執行時