什麼是this
JavaScript中的this是什麼?
定義:this是包含它的函數作為方法被調用時所屬的對象。
function fn1(){
this.name = "halo";
}
fn1();
- 我們将定義拆分一下
- 包含它的函數:包含this的函數是fn1。
- 作為方法被調用:fn1(); 此處fn1函數被調用。
- 所屬的對象:函數式調用函數預設所屬的對象是window。
通過上面三點分析,很容易知道fn1函數裡的this指向的是window。
那麼如果是更複雜的場景我們如何判斷this的指向呢?
this到底指向誰
如果想用一句話總結this的指向,稍微了解一些
this
指向的人都能脫口而出
誰調用它,this就指向誰。
也就是說
this
的指向是在調用時确定的,而不是在定義時确定的。這麼說沒有錯,但是并不全面。
其實,調用函數會建立新的術語函數自身的執行上下文。執行上下文的調用建立階段會決定
this
的指向。是以更加準确的總結應該是:
this的指向,是在調用函數時根據執行上下文所動态确定的。
在es6箭頭函數之前,想要判斷一個函數内部this指向誰,就根據以下四種方式來決定的。
- 函數式調用
- 上下文對象調用
- 構造函數調用
- bind、call、apply改變this指向
1、函數式調用
先來看一種相對簡單的情況,函數在全局環境中被直接調用,嚴格模式下函數内this指向undefined,非嚴格模式下函數内this指向window。如下
function fn1() {
console.log(this)
}
function fn2() {
'use strict'
console.log(this)
}
fn1() // window
fn2() // undefined
再看下面例子:
const age = 18;
const p = {
age:15,
say:function(){
console.log(this)
console.log(this.age)
}
}
var s1 = p.say
s1()
這裡
say
方法内的
this
仍然指向
window
,因為
p
中的
say
函數指派給
s1
後,
s1
的執行仍然是在
window
的全局環境中。是以上面的代碼最後輸出
window
和
undefined
。
這裡可能有人會有疑問,如果是在全局環境中,那
this.age
不是應該輸出18麼?這是因為使用
const
聲明的變量不會挂載到
window
全局對象上,是以
this
指向
window
時找不到
window
上的
age
。換成var聲明即可輸出18.
如果想讓代碼輸出
p
中的
age
,并且讓say函數中的this指向對象
p
。隻需要改變函數的調用方式,如下
const age = 18;
const p = {
age:15,
say:function(){
console.log(this)
console.log(this.age)
}
}
p.say()
輸出
{age: 15, say: ƒ}
15
因為此刻
say
函數内部的
this
指向的是最後調用它的對象。再次驗證了那句話,this的指向,是在調用函數時根據執行上下文所動态确定的。
2、上下文對象調用
const p = {
age: 18,
fn: function() {
return this
}
}
console.log(p.fn() === p)
輸出
true
如果第一節的函數式調用了解了。那麼這裡應該也不會有疑問。
我們再重複一遍this的指向,是在調用函數時根據執行上下文所動态确定的。
記住這句話後遇到更複雜的場景也可以很容易的确定this指向。
如下:
const p = {
age: 20,
child: {
age: 18,
fn: function() {
return this.age
}
}
}
console.log(p.child.fn())
不論淺套關系如何變化,this都隻想最後調用它的對象,是以輸出18。
再更新下代碼:
const o1 = {
text: 'o1',
fn: function() {
return this.text
}
}
const o2 = {
text: 'o2',
fn: function() {
return o1.fn()
}
}
const o3 = {
text: 'o3',
fn: function() {
var fn = o1.fn
return fn()
}
}
console.log(o1.fn())
console.log(o2.fn())
console.log(o3.fn())
輸出結果
o1
o1
undefined
- 第一個,應該沒有問題,直接找到調用this的那個函數。
- 第二個,看似調用
,其實内部調用的是o2.fn()
,是以還是輸出o1.fn()
。o1
- 第三個,指派後調用
,相當于在全局環境調用函數。fn()
指向this
window
。
如果現在的需求是想讓
輸出o2,代碼該如何修改?
如下:
const o1 = {
text: 'o1',
fn: function() {
return this.text
}
}
const o2 = {
text: 'o2',
fn: o1.fn
}
console.log(o2.fn())
3、構造函數調用
function Foo() {
this.age = 18
}
const instance = new Foo()
console.log(instance.age)
輸出18。知道輸出結果并不難,但是
new
操作符調用構造函數時都做了什麼呢?
- 建立一個新對象;
- 将構造函數的
指向這個新對象;this
- 為新對象添加屬性、方法;
- 傳回新對象。
需要注意的是,如果在構造函數中出現顯式的
return
,那麼就要分為兩種場景分析。
function Foo(){
this.age = 18
const o = {}
return o
}
const instance = new Foo()
console.log(instance.age)
将會輸出
undefined
,因為如果在構造函數中出現顯式的
return
,并且傳回一個對象時,那麼建立的構造函數執行個體就是
return
傳回的對象,這裡
instance
就是傳回的空對象
o
。
function Foo(){
this.age = 18
return 1
}
const instance = new Foo()
console.log(instance.age)
将會輸出
18
,因為如果構造函數中出現顯式
return
,但是傳回一個非對象的值時,那麼
this
還是指向執行個體。
總結:當構造函數顯式傳回一個值,并且傳回的是一個對象,那麼
this
就指向這個傳回的對象。如果傳回的不是一個對象,
this
仍然指向執行個體。
4、bind、call、apply改變this指向
關于基礎用法,這裡不再贅述。需要知道的是
bind/call/apply
三者都是改變函數
this
指向的,
call/apply
是改變的同時直接進行函數調用,而
bind
隻是改變
this
指向,并且傳回一個新的函數,不會調用函數。
call
和
apply
的差別就是參數格式不同。詳見如下代碼:
const target = {}
fn.call(target, 'arg1', 'arg2')
上述代碼等同于如下代碼
const target = {}
fn.apply(target, ['arg1', 'arg2'])
可以看出知識調用的參數形式不同而已,改寫成bind如下所示
const target = {}
fn.bind(target, 'arg1', 'arg2')()
不光要調用bind傳入參數,還是在調用bind後再次執行函數。
明白call/apply/bind的使用後,再來看一段代碼:
const foo = {
age: 18,
showAge: function() {
console.log(this.age)
}
}
const target = {
age: 22
}
console.log(foo.showAge.call(target))
結果輸出22,隻要掌握了
call/apply/bind
的基本用法,對于輸出結果并不難了解。我們往往會遇到多種方式同時出現的情況,我們在說完箭頭函數的
this
後會再詳細說明
this
優先級相關内容。
5、箭頭函數this指向
熟悉es6的人應該會知道箭頭函數中的
this
指向,不再遵從上述的規制,而是根據外層的上下文來決定。
es5代碼:
const foo = {
fn: function () {
setTimeout(function() {
console.log(this)
})
}
}
foo.fn() // Window{……}
this
出現在
setTimeout()
中的匿名函數裡時,
this
指向
window
對象。這種特性勢必會給我們的開發帶來一些坑,es6的箭頭函數就很好的解決了這個問題。
es6代碼:
const foo = {
fn: function () {
setTimeout(() => {
console.log(this)
})
}
}
foo.fn() // {fn: ƒ}
箭頭函數中的
this
指向,不再适用上面的标準,而是找到外層上下文,這段代碼中
this
在箭頭函數中,則找到外層的上下文的調用對象——
foo
。是以這裡的
this
指向的就是
foo
。
注意:當箭頭函數改變了
this
指向後,那麼該
this
指向就不再受任何影響,也就是說不會再次發生改變,具體在
this
優先級章節中會舉例說明。
總結:
- 通過
等改成call、apply、bind、new
指向的操作稱為顯式綁定;this
- 根據上下文關系确定的
指向成為隐式綁定。this
如果一段代碼中即出現顯式綁定又有隐式綁定,該如何确定
this
指向呢?
往下看
6、this優先級
function foo (age) {
console.log(this.age)
}
const o1 = {
age: 1,
foo: foo
}
const o2 = {
age: 2,
foo: foo
}
o1.foo.call(o2)
o2.foo.call(o1)
如果隐式綁定優先級高于顯式綁定,那麼應該輸出
1,2
。但是運作代碼發現結果輸出
2,1
。這也就說明了顯式綁定中的
call、apply
優先級更高。
再看:
function foo (age) {
this.age = age
}
const o1 = {}
var fn = foo.bind(o1)
fn(18)
console.log(o1.age) // 18
var f1 = new fn(22)
console.log(f1.age); // 22
分析下上面代碼,
fn
是
foo
函數調用
bind
方法傳回的函數,也就相當于是傳回
foo
函數,并且将
this
指向
o1
對象。執行了
fn(18)
後
o1
對象的
age
值就是18了,是以第一個輸出結果是18。
然後通過
new
調用
fn
函數,這時
fn
函數作為構造函數被調用,
this
就會指向傳回的執行個體,進而與
o1
對象解綁。
是以得出結論:
new
的優先級高于
bind
。
還記得上一節提到的箭頭函數特行麼?箭頭函數影響的this指向無法被修改。看下面代碼:
function foo() {
return () => {
console.log(this.age)
};
}
const o1 = {
age: 2
}
const o2 = {
age: 3
}
const fn = foo.call(o1)
console.log(fn.call(o2))
輸出為2,
foo
的
this
指向了
o1
,
fn
接收的箭頭函數的
this
自然也會指向
o1
。而箭頭函數的this是不會再次改變的,是以盡管用顯式綁定
call
去改變
this
指向,也是不起作用的。
結束啦!
this涉及知識點繁多,碰到優先級問題也是讓人頭疼。
沒有什麼捷徑,唯有“死記硬背”+“慢慢了解”