this是我們在日常寫js代碼中所經常用到的,用起來确實友善很多,有的剛剛接觸到js開發這一塊的朋友,可能還不清楚this指向的問題,是以周末閑來無事,就this指向問題做個小結。
在了解this之前,首先需要了解函數調用位置,調用位置就是函數在代碼中被調用的位置,而非你申明函數的位置,每個函數的this是在調用的時候被綁定的(ES6中箭頭函數除外,下文會詳細說明),完全取決于函數的調用位置(也就是函數的調用方法)。
一、this綁定規則
一般this指向有四條原則,一般你必須先找到調用位置,然後根據判斷需要應用下面四條規則中的那一條。閑話不多說,直接上幹貨,四條規則如下所示。
- 預設綁定:一般适用于獨立函數調用,也可以吧這條規則看做是無法應用其他規則時候的預設規則。
- 隐式綁定:函數的調用位置是否有上下文對象。一般适用于在某個對象中調用函數。
- 顯示綁定:即用call()、apply()、bind()。顯示的給某個方法運作時綁定一個上下文對象,也稱之為“硬綁定”。
- new 綁定:即用關鍵字new去調用函數的時候,被調用的函數稱之為構造函數調用,this會指向構造函數傳回的執行個體。
下面逐一分析講解各條規則:
1.1預設綁定:
說預設綁定之前,我們應該先知道,申明在全局作用域下的變量(比如 var age = 27)就是全局對象的一個同名屬性,他們本質上就是一個東西。
我們看如下代碼:
<script>
var str = '我是在全局作用域之下定義的變量';
function foo() {
console.log(this.str)
console.log(window.str)
}
foo()
</script>
上述代碼的運作結果我們可以看到是會列印出str這個字元串。因為在此時this适用的是我們上面所羅列出的第一條,即this預設綁定了全局對象window,是以列印出的this.str與window.str是一樣的。
但是需要注意的一點是,若是js使用了‘use strict’指令運作在嚴格模式下的話,那麼此時的this是不指向window的。此時的this是undefined 是以如果還是從this上取str這個變量的話,回報一個Type Error類型的錯誤。
code:
<script>
'use strict'
var str = '我是在全局作用域之下定義的變量';
function foo() {
console.log(this.str) //運作在嚴格模式下 this 是undefined
console.log(window.str)
}
foo()
</script>
1.2隐式綁定
我們首先看一下如下代碼示例:
<script>
var str = '我是在全局作用域之下定義的變量';
var obj = {
str:'我是在obj之下定義的變量',
foo:function () {
console.log(this.str)
}
}
obj.foo();
</script>
運作結果如下:
可以看到,我們在調用了obj中的foo()之後,輸出的是定義在obj内部的str,因為當函數引用有上下文對象的時候,隐式綁定規則會把函數調用中的this綁定到這個上下文。此時this被綁定到obj上,是以此時this.a和obj.a是一樣的。
需要注意的是,對象屬性的引用鍊中隻有最後一層的調用位置起作用,舉個栗子:
code:
<script>
var str = '我是在全局作用域之下定義的變量';
var obj = {
str:'我是在obj之下定義的變量',
foo:function () {
console.log(this.str)
}
}
var obj1 = {
str:'我是在obj1之下定義的變量',
obj:obj
}
obj1.obj.foo()
</script>
可以看到上面foo函數是通過兩個對象(obj和obj1),obj1.obj中的foo調用的,而此時this會綁定在“最後一層調用位置起作用”(在這兒就是obj)。
隐式綁定中還需要注意的另外一個問題就是有可能我們會在無意識中“丢失”了綁定對象,此時會采用預設綁定原則。是綁定到(window還是undefined則取決于代碼是否運作在嚴格模式下)
如下代碼:
<script>
var fn;
var str = '我是在全局作用域之下定義的變量';
var obj = {
str:'我是在obj之下定義的變量',
foo:function () {
console.log(this.str)
}
}
fn = obj.foo;
fn()
</script>
上述代碼我們可以看到,将函數foo從對象obj中取出來,然後指派給全局變量fn,雖然fn是obj.foo的一個引用,但是fn所引用的其實是foo函數本身。因為在obj對象中foo是一個函數,而函數是一個引用類型的值,是以雖然foo函數的定義是寫在obj函數中的(不管是直接在obj中定義還是先定義在添加為引用屬性),這個函數嚴格來說都不屬于obj對象。(這一塊需要多了解了解)是以在這個地方使用的是預設綁定的規則,即this綁定在了window上(非嚴格模式)故在此輸出的是window.str。
1.3:顯示綁定(硬綁定)
就像隐式綁定一樣,我們會給this綁定一個上下文,這個上下文(即調用對象)是可以變得,那如果我們不想在對象函數内部包含函數引用,而是想在某個對象上強制調用函數,該怎額辦呢。?這就需要用到this的硬綁定了。
所用的方式就是用call() apply() bind()這三個方法來實作“硬綁定”。其中call()和apply()函數從綁定的角度來說是一樣的,差別在于傳參數的方式。(call和apply第一個參數都是this綁定的目标對象,call函數需要将傳入的參數一個一個的寫在後面,apply是用數組的方式傳遞參數)
請看如下示例代碼:
<script>
var str= '我是在全局作用域中定義的';
var obj = {
str:'我是在obj對象中定義的'
}
function getStr() {
console.log(this.str)
}
getStr()
getStr.call(obj) //call硬綁定
</script>
運作結果如下所示:
可以看到運作結果,如上所示,直接運作getStr() this會使用預設綁定,是以輸出的是window.str,而使用call則可以強制的将this指向call()中的參數obj,是以getStr()輸出的結果等于obj.str,bind()函數也是一樣的,bind函數内部是調用了apply函數,然後傳回的是一個函數執行個體,用來實作将this綁定到某個對象上。
<script>
var fn;
var foo;
var str= '我是在全局作用域中定義的';
var obj = {
str:'我是在obj對象中定義的',
getStr:function () {
console.log(this.str)
}
}
fn = obj.getStr;
foo = obj.getStr.bind(obj)
fn();
foo();
</script>
運作結果如下所示:
可以看到将obj.getStr函數分别指派給fn和foo,fn沒有使用bind綁定,使用的是預設規則,而foo使用了bind将this指向了obj屬性,是以一個輸出的是window.str一個輸出的是obj.str。
1.4 new 綁定
最後一個this綁定規則是new 綁定。
但是我想先給大家澄清一個非常常見的關于js中函數和對象的誤解。
在傳統的面向類的語言中,“構造函數”是類中一些特殊的方法,使用new初始化是會調動類中的構造函數。
在js中也有一個new操作符,使用的方法看起來也和哪些面向類的語言一樣,但是,js中的new的機制實際上和面向類的語言完全不同。在js中任何一個函數都可以被new操作符調用而成為“構造函數”,也就是說,其實在js中的“構造函數”就是普通函數,隻不過一個普通函數在被用new 操作符調用後 都可以成為構造函數。那麼在js中使用new操作符來調用函數,或者說發生了構造函數調用時,會發生一些什麼事情呢。?一般來說會發生如下幾件事情:
- 建立(或者說構造)一個全新的對象。
- 這個新對象會被執行[[Prototype]]連接配接。
- 這個新對象會綁定到函數調用的this。
- 如果函數沒有傳回其他值,那麼new 表達式找那個的函數調用會自動傳回這個新的對象。
下面看一段代碼:
<script>
function foo() {
this.str = '我是在foo構造函數中被定義的'
}
var a = new foo();
console.log(a.str)
</script>
運作結果如下:
可以看到這兒由于使用了new 是以此時的this指向的是new 調用構造函數所傳回的執行個體對象。是以在foo函數中this指向的是所傳回的執行個體對象。
一般函數中如果用到了this,就可以用上述講述的方法去分别判斷适用于那種類型的綁定,他們之間也是有一定的優先級的關系,一般來說他們之間的優先級如下:new 綁定 > 顯示綁定 > 隐式綁定 > 預設綁定
二、不按套路出牌的箭頭函數
在ES6中引入了箭頭函數的寫法,箭頭函數中的this規則不适用于上面講述的那一套,箭頭函數中的this是固定的,就是在定義時所在的對象,而非在使用時所在的對象。
看如下代碼示例:
<script>
var str = '我是在全局作用域中定義的str'
function foo() {
setTimeout(()=> console.log(this.str))
,1000}
function fn() {
setTimeout(function () {
console.log(this.str)
},1000)
}
foo.call({str:'我是foo函數傳入參數的str'})
fn.call({str:'我是fn函數傳入參數的str'})
運作結果如下所示:
可以看到fn函數和foo函數中的函數功能是一樣的,都是延時1s之後輸出一個字元串,一個使用箭頭函數寫法,一個使用普通函數寫法,foo.call和fn.call先是分别用call将foo函數和fn函數中的this指向了傳入的參數對象,然後延時一秒輸出,在1s中之後,使用普通函數寫法中的setTimeout中的this就指向了window全局對象,而箭頭函數中的setTimeout中的this依然指向傳入的參數對象。是以可以看出,箭頭函數中的this總是指向函數定義生效時所在的對象。
實際上箭頭函數之是以有這樣的特點即this指向固化,并不是因為箭頭函數内部有什麼綁定this的機制,實際原因在于箭頭函數壓根就沒有自己的this,導緻内部代碼層的this就是外層代碼塊的this。也正是因為他沒有自己的this,是以不能用箭頭函數來當構造函數使用。
好了,今天關于this的綁定就寫到這兒,如果有不同看法的朋友,歡迎溝通交流。