寫在前面
this 指向可能是新手經常會碰到的問題, 記得之前在一些部落格上面看到這麼一句話
ES5 function裡面的this誰呼叫它就指向誰,ES6箭頭函式的this是在哪裡定義就指向哪裡
暫且先不討論這句話的正確與否,先往下面看
1.普通函式和箭頭函式的this差別
普通函式:
function say(){
console.log(this.a)
}
var a='window'
var obj={
a:'inside obj',
say:say
}
obj.say() //inside obj
複製程式碼
箭頭函式:
var say=()=>{
console.log(this.a)
}
var a='window'
var obj={
a:'inside obj',
say:say
}
obj.say() // window
複製程式碼
普通函式下:因為obj.say()
是obj去呼叫say方法,所以say裡面的this繫結在obj
箭頭函式下: 因為say
方法是定義在window全域性環境,因此它的this永遠指向window,值得一提的是,箭頭函式的this無法通過call bind apply
方法改變this的繫結物件,一經定義,無法改變
我們可以做個嘗試:
var say=()=>{
console.log(this.a)
}
var a='window'
var obj={
a:'inside obj',
say:say
}
say.call(obj) // window
複製程式碼
發現他還是指向了window
另外還有一種情況,比較繞:
function say(){
return ()=>{
console.log(this.a)
}
}
var a = 'a in window'
var obj1 ={
a:'a in obj1'
}
var obj2 ={
a:'a in obj2'
}
var d = say.call(obj1)
d.call(obj2) // 'a in obj1'
複製程式碼
發現列印出的 'a in obj1'
,說明this繫結在obj1上面。
一般情況下, 內部建立的箭頭函式會捕獲呼叫時 say()
的 this,也就是window,這時如果我們呼叫
var s= say(); s() ;
就會列印出 'a in window'
但是當say函式的this繫結在obj1上時,箭頭函式的this也會跟著繫結在obj1。
總之牢記一句話:箭頭函式會捕獲其所在的上下文的this值,作為自己的this值,無法改變指向
2.隱式丟失和隱式賦值
- 隱式丟失就是this繫結丟失,多見於賦值操作
- 隱式賦值就是this繫結預設的全域性物件window或者undefiend,取決於是否嚴格模式
我們把上述普通函式例子進行一個改變:
function say(fn){
fn() //把say函式改成通過接受一個函式名稱,並且在say函式體內執行這個傳入進來的函式
}
function getWord(){
console.log(this.a) //定義一個列印出this.a的函式
}
var a='window'
var obj={
a:'inside obj',
getWord:getWord //把getWord作為obj的一個屬性
}
say(obj.getWord) //window
複製程式碼
把obj.getWord
作為函式名放到say
方法裡面執行,其實say
就是相當於回掉函式,這時候發現他居然指向了window?
別急,我們在看一個比較簡單例子:
var a='a in window'
var obj={
a:'a in obj',
say:function(){
console.log(this.a)
}
}
var s = obj.say //賦值操作的時候,this繫結丟失
s() // 'a in window' a指向了window,因為這是使用了預設this繫結,也就是隱式賦值
複製程式碼
this在它的隱式繫結函式丟失了繫結物件,也就是this隱式丟失,這時候它會應用預設繫結,也就是隱式賦值,從而把this繫結在window全域性物件或者undefiend上,這取決於是否是嚴格模式下。
在函式say(obj.getWord)
的時候,obj.getWord
相當進行了個賦值操作,因此丟失了this的繫結
相當於
function say(fn){
fn = obj.getWord //這麼看是不是瞬間就明白了
}
複製程式碼
這種隱式丟失多見於回撥函式裡面,比如
setTimeout(obj.getWord,1000)
也會造成隱式丟失問題
3.顯示繫結
分為兩種情況:
- 硬繫結
- api上下文的繫結
1,硬繫結
其實說白了就是呼叫apply call
方法對this進行指向繫結,
比如上述的例子,只要我們把say
函式方法改成
function say(fn){
fn.call(obj) //顯示繫結,把this繫結在obj
}
function getWord(){
console.log(this.a) //定義一個列印出this.a的函式
}
var obj ={
a:'a in obj',
getWord:getWord
}
say(obj.getWord) // 理所當然列印出 'a in obj'
複製程式碼
另外一提,就是通過call
apply
在函式題內部改變this繫結的函式方法,在呼叫時不能夠再次改變
看例子:
function say() {
console.log(this.a)
}
function doSay() {
say.call(obj)
}
var a = 'window'
var obj = { a: 'a inside obj' }
doSay.call(window) // 還是列印出 'a inside obj' ,並沒有指向window全域性環境
複製程式碼
2,API的上下文
第三方庫的許多函式,以及 JavaScript 語言和宿主環境中許多新的內建函式,都提供了一 個可選的引數,通常被稱為“上下文”(context),其作用和 bind(..) 一樣,確保你的回撥 函式使用指定的 this。 ---《你不知道的JAvascript》
比如forEach
這個陣列方法,查了下mdn,他有兩個引數:
- 1,callback:forEach的回撥函式,也就是forEach(function(item,index)...)
- 2,thisArg:可選引數。當執行回撥函式時用作 this 的值(參考物件)。
看下例子:
function say(item){
console.log(item,this.a)
}
var obj={
a:'this a is inside obj'
};
[4,2,21].forEach(say,obj)
//4 "this a is inside obj"
//2 "this a is inside obj"
//21 "this a is inside obj"
複製程式碼
這些內建函式其實就是通過call
或apply
實現了顯示繫結,我們可以自己實現一個
Array.prototype.myForEach=function(fn,thisArg){
var arr = this // 這個是myForEach的this繫結在呼叫他的陣列
for (var i = 0; i < arr.length;i++){
fn.apply(thisArg, [arr[i], i, arr]) //這個是fn的this,指向thisArg
}
}
var obj ={
a:'this is in obj'
}
function say(item,i,arr){
console.log(
'當前item:'+item,
'當前i:' + i,
'當前arr:' + arr,
'this.a:' + this.a,
)
}
[4,2,1,5].myForEach(say,obj)
// 當前item: 4 當前i: 0 當前arr: (4)[4, 2, 1, 5] this.a: this is in obj
// 當前item: 2 當前i: 1 當前arr: (4)[4, 2, 1, 5] this.a: this is in obj
// 當前item: 1 當前i: 2 當前arr: (4)[4, 2, 1, 5] this.a: this is in obj
// 當前item: 5 當前i: 3 當前arr: (4)[4, 2, 1, 5] this.a: this is in obj
複製程式碼
4,軟繫結
其實軟繫結也算是顯示繫結的一種,單獨拿出來講是因為它屬於通過騷操作實現對this繫結丟失情況的容錯處理。
比如在賦值的時候可能造成this繫結丟失情況,這時候我希望它this繫結丟失的時候不要繫結在全域性物件上(非嚴格模式下),而是能夠把它的this繫結在某個位置上,所以我們就要用到軟繫結:
Function.prototype.softBind = function (obj) {
var fn = this; //呼叫方法
var curried = Array.prototype.slice.call(arguments, 1); // 獲取除了繫結物件引數外的其餘引數
function bound () {
var thisArgs = !this||this===(window||global)?obj:this // 如果指向window則指向obj本身
var args = Array.prototype.slice.call(arguments)
return fn.apply(thisArgs, curried.concat(args)) //引數合併
};
bound.prototype = Object.create(fn.prototype); // 這個是繼承fn,作為他的子類
return bound;
};
複製程式碼
來試驗下
function say(){
console.log(this.a)
}
var a = 'a in window'
var obj1={
a:'a in obj1,預設繫結'
}
var obj2={
a:'a in obj2'
}
var obj3={
a:'a in obj3'
}
var handleSay = say.softBind(obj1) // 預設繫結obj1
handleSay() // 'a in obj1,預設繫結'
obj2.handleSay = say.softBind(obj1) // 預設繫結obj1
obj2.handleSay()//'a in obj2'
handleSay.call(obj3) // 'a in obj3'
複製程式碼
解釋下,可能這裡有點繞,或許你們會覺得這個跟bind沒啥區別,但是仔細看下,
比如obj2.handleSay = say.softBind(obj1)
我這裡給他了一個預設繫結物件,但是我呼叫obj2.handleSayde
的時候,它的this還是繫結在了obj2上,其作用程式碼就是:
var thisArgs = !this||this===(window||global)?obj:this
這一行程式碼
,
這個功能bind
是無法實現的,因此這就softBind軟繫結的作用
5,new 繫結
new 的呼叫也能改變this的指向,先說一說 new 做了什麼事就明白了
使用 new 來呼叫函式,或者說發生建構函式呼叫時,會自動執行下面的操作。
- 建立(或者說構造)一個全新的物件。
- 這個新物件會被執行[[原型]]連線。
- 這個新物件會繫結到函式呼叫的this。
- 如果函式沒有返回其他物件,那麼new表示式中的函式呼叫會自動返回這個新物件。
用程式碼來表示就是
function myNew(ClassFn){
var obj ={} //1,建立(或者說構造)一個全新的物件。
obj.__proto__ = ClassFn.prototype //2,這個新物件會被執行[[原型]]連線。
ClassFn.call(obj) //3,這個新物件會繫結到函式呼叫的this。
return obj //4,如果函式沒有返回其他物件,那麼new表示式中的函式呼叫會自動返回這個新物件,意思就是說,如果建構函式ClassFn本身就有返回的東西則返回它,否則返回例項化的這個obj
}
複製程式碼
補充說明第4點,意思是:
function Test(){
this.a='text'
return {}
}
var s = new Text()// s為空物件,因為Test建構函式已經返回了空物件
複製程式碼
另外補充說明一個知識點,就是我們之前沒有提及到的bind
,其實bind
內部也是通過apply
或call
實現,藉助bind
我們可以實現函式柯里化等騷操作:
Function.prototype.myBind=function(){
var self =this //誰呼叫指向誰
var args = Array.prototype.slice.call(arguments)
var thisArgs = args[0] //獲取傳進來的繫結物件,也就是第一個引數,比如 .bind(obj)
args = args.slice(1) //獲取除了繫結物件外的其餘引數
return function(){
return self.apply(thisArgs,args.concat(Array.prototype.slice.call(arguments)))
}
}
複製程式碼
嘗試一下,發現可以正常工作:
function say(p){
this.p = p
}
var obj={}
var s= say.bind(obj)
s(2)
obj//{p:2}
複製程式碼
但是,這只是實現bind的部分功能,還有另外一部分是當bind繫結後返回的函式作為建構函式時,會有不同的表現,有興趣可以自行了解developer.mozilla.org/zh-CN/docs/…
7,判斷this
判斷this繫結物件(this指向)可以按照下面5步來:
-
函式是否在new中呼叫(new繫結)?如果是的話this繫結的是新建立的物件。 如:
var bar = new foo()
-
函式是否通過call、apply(顯式繫結)或者硬繫結呼叫?如果是的話,this繫結的是 指定的物件。
var bar = foo.call(obj2)
-
函式是否在某個上下文物件中呼叫(隱式繫結)?如果是的話,this 繫結的是那個上 下文物件。
var bar = obj1.foo()
-
如果都不是的話,使用預設繫結。如果在嚴格模式下,就繫結到undefined,否則繫結到 全域性物件。
var bar = foo()
-
除此之外還要記住當有賦值情況的時候會造成this繫結丟失情況
function go(fn){fn=obj.say()}//具體可以檢視上述2隱式丟失的內容
另外,ES6 中的箭頭函式並不會使用四條標準的繫結規則,而是根據當前的詞法作用域來決定 this,具體來說,箭頭函式會繼承外層函式呼叫的 this 繫結(無論 this 繫結到什麼)。這 其實和 ES6 之前程式碼中的 self = this 機制一樣。
寫在最後
回到文章開頭那句話
ES5 function裡面的this誰呼叫它就指向誰,ES6箭頭函式的this是在哪裡定義就指向哪裡
雖然看起來不太嚴謹,但是它的確是正確的,在我們日常開發中,比如是剛入門不久的前端開發者,不會頻繁接觸到函式柯里化這種寫法,更多的是在使用框架的時候分辨this的指向,比如vue框架+iview,使用Table組建的時候,如果想自定義表格內容,其中一個方法就得藉助render
函式,寫成普通函式的話,這一行
onClick={this.viewItem.bind(this, params)}
就會報錯,因為this指向錯誤,被繫結到了呼叫render函式的物件,也就是Table元件上,
需要手動賦值this才能解決,但是使用箭頭函式就能完全規避這個問題
<template>
<div>
<Table
:columns="tableHead"
:data="dataList"
></Table>
</div>
</template>
複製程式碼
export default {
data() {
return {
dataList: [],
tableHead: [
{
title: '操作',
render: (h, params) => {
// 這裡的this 永遠指向 VueComponent物件,其實就是data(){}的this繫結物件
return (
<div>
<i-button
type="primary"
size="small"
style=" marginRight:5px"
onClick={this.viewItem.bind(this, params)}
>
檢視
</i-button>
</div>
)
}
}
]
}
}
複製程式碼
總而言之,希望讀到這篇文章的人能有所收穫
如有不正確的地方歡迎斧正,謝謝各位