javascript中“this”機制的小結
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的繫結就寫到這兒,如果有不同看法的朋友,歡迎溝通交流。