this 全面解析

西嵐發表於2019-08-20

寫在前面

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.顯示繫結

分為兩種情況:

  1. 硬繫結
  2. 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"
複製程式碼

這些內建函式其實就是通過callapply實現了顯示繫結,我們可以自己實現一個


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 來呼叫函式,或者說發生建構函式呼叫時,會自動執行下面的操作。

  1. 建立(或者說構造)一個全新的物件。
  2. 這個新物件會被執行[[原型]]連線。
  3. 這個新物件會繫結到函式呼叫的this。
  4. 如果函式沒有返回其他物件,那麼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內部也是通過applycall實現,藉助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步來:

  1. 函式是否在new中呼叫(new繫結)?如果是的話this繫結的是新建立的物件。 如:var bar = new foo()

  2. 函式是否通過call、apply(顯式繫結)或者硬繫結呼叫?如果是的話,this繫結的是 指定的物件。 var bar = foo.call(obj2)

  3. 函式是否在某個上下文物件中呼叫(隱式繫結)?如果是的話,this 繫結的是那個上 下文物件。 var bar = obj1.foo()

  4. 如果都不是的話,使用預設繫結。如果在嚴格模式下,就繫結到undefined,否則繫結到 全域性物件。 var bar = foo()

  5. 除此之外還要記住當有賦值情況的時候會造成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>
            )
          }
        }
      ]
    }
  }



複製程式碼

總而言之,希望讀到這篇文章的人能有所收穫

如有不正確的地方歡迎斧正,謝謝各位

相關文章