动态

详情 返回 返回

JavaScript中的this指向哪? - 动态 详情

大家好,我是歸思君

一、引言

this可以説是前端開發中比較常見的一個關鍵字,由於其指向是在運行時才確定,所以大家在開發中判斷其方向時也會很模糊,今天就把this的指向問題拆開了,揉碎了,好好講一講。
先來看一個場景,看看該處的 this 應該指向哪:首先在 request.js 中定義一個 getAction 函數

export function getAction(url,parameter) {
  let headers = {}
  if (this && this.realReferer && this.realReferer !== '') {
    headers.realReferer = window.location.origin + this.realReferer
  }
  return axios({
    url: url,
    method: 'get',
    params: parameter,
    headers
  });
}

然後在 test.vue 文件中引用該 getAction函數並使用

import { getAction } from '@api/request'
export default {
  methods: {
    getTableData() {
      getAction(this.url.requestUrl).then(res => {
        //1.這個時候getAction中的this將打印出什麼
        //2.在該處打印this,會輸出什麼
        console.log(this);
      })
    },
  }
}

現在有兩個問題:

  1. test.vue中調用 getAction()時,此時其內部,也就是request.js 中的 this 指向什麼?
  2. getAction() then 後的箭頭函數中的 this 指向什麼?

思考一下能判斷出這兩個this的指向嗎?先賣個關子,等咱們再講完this的相關原理後再來解答這兩個問題。這篇文章會從這幾個方面講解:

  • 什麼是this,this和執行上下文的關係
  • this中的默認、隱式、顯式和new的綁定規則
  • 箭頭函數中的this指向問題

    二、什麼是this?

    this 其實就是一個JavaScript語言中的一個關鍵字,  它的值是某個對象引用值,其指代的是當前執行上下文中的對象。那麼為何需要this?我們先來看看一個例子:

    var testObj = {
      name: "testObj",
      print: function () {
          console.log(name)
      }
    } 
    var name = "global name";
    //想通過調用print()來調用testObj中的name
    testObj.print();//global name

    從結果可知,最後print() 輸出"global name", 而不是 testObj 中的 name。為何出現這種情況?
    這是因為 JavaScript 語言的作用域鏈是由詞法作用域決定的,而詞法作用域是由代碼結構來確定的:

  • testObj.print()執行時,這段代碼的詞法作用域是全局作用域,所以這個時候 js 引擎會去全局作用域中尋找 name,最後打印出“global name”。
  • 因此為了避免這種情況,JavaScript 設計者引入了 this 機制,來調用對象的內部屬性,如下代碼:

    var testObj = {
      name: "testObj",
      print: function () {
          console.log(this.name)
      }
    } 
    var name = "global name";
    testObj.print();//testObj

    最後就能夠通過testObj.print() 來調用對象內部的屬性了。
    不同於詞法作用域鏈,this的指向是在運行時才能確定,實際上當執行上下文創建後,會生成一個this引用值,指向當前執行上下文對象,如下圖所示:

    而 js 引擎在執行代碼時的運行時上下文主要有三種:全局執行上下文、函數執行上下文和 eval 執行上下文。不同場景的this指向如下:

    //全局執行上下文,當前對象是window
    console.log(this);//window
    //函數執行上下文外部對象是全局對象,所以指向全局對象window
    function test(){
    console.log(this);//window
    }
    //函數執行上下文外部對象是test,因此指向當前的對象test
    var test = {
    test: function(){
      console.log(this);//test{...}對象
    }
    }
    //eval執行上下文,根據默認綁定規則,指向全局對象window
    eval(`console.log(this); `) //window

    正是因為this在運行中才得以確定其指向的上下文對象,所以為了規範和標準化this的指向方式,規定了一系列的綁定規則,來決定什麼情況下this會綁定到哪個對象。下面就來聊聊this的綁定規則

    三、this 綁定規則

    this的綁定大致可以劃分為默認、隱式、顯式和new四種場景下的綁定規則:

    1. 默認綁定

    當函數被獨立調用時,會將this綁定到全局對象。瀏覽器環境下是window, 嚴格模式是undefined主要有以下幾種場景:

    //1. 定義在全局對象下的函數被獨立調用
    function test(){
      console.log("global:", this);
    }
    test();//window
    //2. 定義在某個對象下的函數被獨立調用
    var testObj = {
    test: function(){
      console.log("testObj:", this);
    }
    }
    var testfun = testObj.test;
    testfun();//window
    //3. 定義在某個函數下的函數被獨立調用
    function testFun(fn){
      fn();
    }
    testFun(testObj.test); //window

    2. 隱式綁定

    當函數作為對象的方法被調用時,隱式綁定規則會將this綁定到調用該方法的對象,也就是"誰調用,就指向誰"。

    const obj = {
    name: 'innerObj',
    fn:function(){
      return this.name;
    }
    }
    //調用者是obj, this指向obj
    console.log(obj.fn());//innerObj
    
    const obj2 = {
    name: 'innerObj2',
    fn: function() {
      return obj.fn(); //此時是obj調用fn,所以此時this指向obj
    }
    }
    //調用者是obj, this指向obj
    console.log(obj2.fn())//innerObj

    現在我們可以回答引言中的問題1:在request.jsgetAction() 中this指向test.vue 中的全局vue對象,因為import {getAction} from '@api/request' 後,相當於vue對象調用了getAction(),因此其內部的this方向符合隱式綁定規則,所以指向調用者——test.vue 中的全局vue對象

    3. 顯式綁定

    顯式綁定主要指通過call、apply和bind方法可以顯式地綁定this的值:

    call 方法

    語法: function.call(thisArg, arg1, arg2, ...) :
    參數: thisArg 表示 this 指向的上下文對象, arg1...argn  表示一系列參數
    功能:  無返回值立即調用 function 函數

    var test = {
    }
    function test2(){
      console.log(this);
    }
    //此時是獨立函數,因此指向全局對象
    test2();//window
    //call顯式綁定,將函數內部的this綁定至call中指定的引用對象
    test2.call(test);//test

    apply 方法

    語法: function.apply(thisArg, [argsArray]) :
    參數: thisArg 表示 this 指向的上下文對象, argsArray  表示參數數組
    功能: 沒有返回值, 立即調用函數
    apply 和 call 的區別在於傳參,call 傳的是一系列參數,apply 傳的是參數數組

    var test = {
    }
    function test2(name){
      console.log(this);
      console.log(name);
    }
    //此時是獨立函數,因此指向全局對象
    test2();//window
    //call顯式綁定,將函數內部的this綁定至call中指定的引用對象
    test2.apply(test, ["name"]);//test, name
    test2.call(test, "name"); //test

    bind 方法

    語法:function.bind(thisArg[, arg1[, arg2[, ...]]])
    參數: thisArg 表示 this 指向的上下文對象; arg1, arg2, ...表示 要傳遞給函數的參數。這些參數將按照順序傳遞給函數,並在調用函數時作為函數參數使用
    功能: 返回原函數 function 的拷貝, 這個拷貝的 this 指向 thisArg

    var test = {
      fun: function(){
          console.log(this);
          var test = function(){
              console.log("test", this);
          }
          //1. 因為test.fun()在全局作用域中,所以屬於獨立函數調用,默認綁定規則指向全局對象
          test(); //window
          //2. bind方法會創建一個原函數的拷貝,並將拷貝中的this指向bind參數中的上下文對象
          var test2 = test.bind(this);
          test2();//test
          //3. apply方法會將this指向參數中的上下文,並立即執行函數
          test.apply(this);//test
          
      }
    }
    test.fun();

    4. new 綁定

    主要是在使用構造函數創建對象時,new 綁定規則會將 this 綁定到新創建的實例對象,因此構造函數中用 this 指向的屬性值和參數也會被賦給實例對象:

    function funtest(){
      this.name = "funtest"
    }
    var tete = new funtest();
    console.log(tete.name); //"funtest"

    new 操作符實際上的操作步驟:

  • 創建一個新的對象 {}
  • 將構造函數中的 this 指向這個新創建的對象
  • 為這個新對象添加屬性、方法等
  • 返回這個新對象

等價於如下代碼:

var obj = {}
obj._proto_ = funtest.prototype
funtest.call(obj)

5. 綁定規則的優先級

上述的綁定規則有時會一起出現,因此需要判斷不同規則之間的優先級,然後再來確定其 this 指向:
a. 首先是默認綁定和隱式綁定,執行以下代碼:

function testFun(){
    console.log(this);
}
var testobj = {
    name:"testobj",
    fun:testFun
}
//若輸出window,則證明優先級默認綁定大於隱式綁定;
//若輸出testobj,則證明優先級隱式綁定大於默認綁定;
testobj.fun()//testobj

輸出為 testobj 對象,所以隱式綁定的優先級高於默認綁定
b. 下面來看一下隱式綁定和顯式綁定,執行以下代碼:

function testFun(){
    console.log(this);
}
var testobj = {
    name:"testobj",
    fun:testFun
}
//若輸出testobj,則證明優先級隱式綁定大於顯式綁定
//若輸出{}, 則證明優先級顯式綁定大於隱式綁定
testobj.fun.call({})//{}

結果輸出 { },説明顯式綁定優先級大於隱式綁定
c. 顯式綁定的 call, apply,bind 的優先級相同,與先後順序有關,看以下代碼:

function testFun(){
    console.log(this);
}
var testobj = {
    name:"testobj",
    fun:testFun
}
//若輸出testobj,則證明優先級隱式綁定大於顯式綁定
//若輸出{}, 則證明優先級顯式綁定大於隱式綁定
testobj.fun.call({})//{}
testobj.fun.call(testobj)

d. 最後來看看顯式綁定和 new 綁定的優先級,執行以下代碼:

function testFun(){
    console.log(this.name);
}
var testobj = {
    name:"testobj",
}
testFun.call(testobj);//testobj
//new 操作符創建了一個新的對象,並將this重新指向新對象
//覆蓋了testFun原來綁定的testobj對象
var instance = new testFun();
console.log(instance.name) //undefined

從結果可知,new 綁定的優先級大於顯式綁定
最後總結一下 this 綁定的 優先級是:

fn()(全局環境)(默認綁定)< obj.fn()(隱式綁定) < fn.call(obj)=fn.apply(obj) = fn.bind(obj)(顯式綁定)< new fn()

6. 綁定的丟失

有時 this 綁定可能會在某些情況下丟失,導致 this 值的指向變得不確定:

賦值給變量後調用

當使用一個變量作為函數的引用值,並使用變量名執行函數時,會發生綁定丟失,此時 this 會默認綁定到全局對象或變成 undefined(嚴格模式下)

var lostObj = {
  name: "lostObj",
  fun: function(){
    console.log(this);
    }
}

var lostfun = lostObj.fun;
lostfun();//window
lostObj.fun();//lostObj

從結果發現,lostfun 雖然指向對象中的方法,但是在調用時發生了 this 綁定丟失。因為當賦值給變量時,對象中的 fun就失去了與對象的關聯,變成了一個獨立函數,所以此時執行 lostfun也就相當於執行獨立函數,默認綁定到全局對象。
那如果通過對象來執行呢?看如下代碼:

var lostObj = {
  name: "lostObj",
  fun: function(){
    console.log(this);
    }
}
var lostObj2 = {
  name: "lostObj2",
  fun: lostObj.fun
}
var lostfun = lostObj.fun;
lostfun();//window
lostObj.fun();//lostObj
lostObj2.fun();//lostObj2

同樣,一旦將方法賦值給變量後,其內部與對象的關聯就此丟失,默認綁定到全局對象。但是將變量放到對象中後,就與該對象進行關聯。所以該方法執行後的 this 執行了 lostObj2對象。

函數作為參數傳遞

將函數作為參數傳遞到新函數中,並在新函數中執行該參數函數:

var lostObj3 = {
  name: "lostObj3",
  fun: function(){
    console.log(this.name);
    }
}
var name = "global"
function doFun(fn){
  fn();
}
doFun(lostObj3.fun);//global

從結果可知,當函數作為參數傳遞後,其形參 fn 被賦值為 lostObj3.fun。實際上也相當於賦值給變量後調用這種情況,而且 doFun()作為獨立函數調用,所以其 this 也就指向全局對象了

回調函數

如果將對象方法作為回調函數傳遞給其他函數,this 綁定也可能丟失

var lostObj4 = {
  name: 'lostObj4',
  fun: function() {
    setTimeout(function() {
      console.log(`Hello, ${this.name}!`);
    });
  }
};
lostObj4.fun(); // Hello, undefined!

因為 setTimeout 的回調函數最後會以普通函數的形式調用,所以其 this 指向的是全局對象,所以即便是 lostObj4調用 fun(),最後其內部的 this 仍然會丟失。

嵌套函數

當某個函數是嵌套在另一個函數內部的函數時,內部函數中的 this 綁定會丟失,並且會綁定到全局對象或 undefined(嚴格模式下):

var lostObj5 = {
  name: 'lostObj5',
  fun: function() {
    function innerFun() {
      console.log(`Hello, ${this.name}!`);
    };
    innerFun();
  }
};
lostObj5.fun();// Hello, undefined!

從結果可以發現,嵌套函數 innerFun()中的 this 此時是指向全局環境。所以從這個案例可以説明作用域鏈和 this 沒有關係,作用域鏈不影響 this 的綁定。
原因是當innerFun()被調用時,是作為普通函數調用,不像 fun()屬於對象 lostObj5的內部方法而調用,因此最後其內部的 this 指向全局對象。
其實 this 丟失可以通過箭頭函數來解決,下面就來聊聊箭頭函數

四、箭頭函數中的 this

箭頭函數是 ES6 增加的一種編寫函數的方法,它用簡潔的方式來表達函數
語法:()=>{}
參數:(): 函數的參數,{}: 函數的執行體

1. 箭頭函數中的 this 指向

箭頭函數中的this是在定義時確定的,它是繼承自外層詞法作用域。而不是在運行時才確定,如以下代碼:

var testObj2 = {
    name: "testObj2", 
    fun: function(){
        setTimeout(()=>{
            console.log(this);
        })
    }
}
var testObj3 = {
    name: "testObj3", 
    fun: function(){
        setTimeout(function(){
            console.log(this);
        })
    }
}
//即使獨立調用函數,箭頭函數內的this指向是在定義時就已經確定
testObj2.fun();//testObj
testObj3.fun();//window

實際上箭頭函數中沒有 this 綁定,它是繼承自外層作用域的 this 值。因此在許多情況下,箭頭函數能解決 this 在運行時函數的綁定問題。

2. 箭頭函數與普通函數中的 this 差異

從 上面的例子可以看出箭頭函數和普通函數在 this 的處理上存在很大的差異,主要有:

this 綁定方式

普通函數的 this 是在運行時確定的;箭頭函數的 this 值是函數定義好後就已經確定,它繼承自包含箭頭函數的外層作用域

作用域

普通函數是具有動態作用域,其 this 值在運行時基於函數的調用方式動態確定。箭頭函數具有詞法作用域,其 this 值在定義時就已經確定,並繼承外部作用域

綁定 this 的對象

普通函數中 this 可以通過函數的調用方式(如對象方法、構造函數、函數調用等)來綁定到不同的對象,而箭頭函數沒有自己的 this 綁定;箭頭函數沒有自己的 this 綁定,它只能繼承外部作用域的 this 值,無法在運行時改變綁定對象,而且也無法通過顯式綁定來改變 this 的指向。

var testObj4 = {
  arrowFun: ()=>{
    console.log(this);
  },
  normalFun: function(){
    console.log(this);
  }
}
//此時箭頭函數的this繼承全局上下文的this,顯式綁定無法修改箭頭函數中的this值
testObj4.arrowFun();//window
testObj4.arrowFun.apply({});//window
testObj4.normalFun();//testObj4
testObj4.normalFun.apply({});//{}

下面我們就可以解答引言中的問題 2 了。箭頭函數中的 this 指向其上層的作用域,也就是 getAction() 中的 this 值,而從隱式綁定調用規則,當前是 vue 實例調用 getTableData()然後再調用 getAction(),因此 this 值指向當前 vue 實例。

五、 this 中的面試題

手寫實現一個 bind 函數

通過分析 bind 函數的語法和參數來:function.bind(thisArg[, arg1[, arg2[, ...]]])

  • 返回值是一個函數
  • 參數 thisArg 指向

我們暫時不考慮原型問題,實現如下代碼:

Function.prototype.mybind = function (thisArg) {
  //1.隱式綁定,當前的this指向目標函數
  var targetFn = this;
  //將參數列表轉換為數組,並刪除第一個參數
  var args = Array.prototype.slice.call(arguments, 1);
  //2.返回值一個函數
  return function bound() {
     var innerArgs = Array.prototype.slice.call(arguments);
     var finalArgs = args.concat(innerArgs);
    //解決返回函數使用new後,綁定this忽略問題
     var _this = targetFn instanceof this ? this: thisArg;
     return targetFn.apply(thisArg, finalArgs)
   }
 }
}

總結

文章回顧 this 的概念和 this 指向的判斷綁定規則,

  1. 首先是綁定規則:
  • 獨立函數調用執行時,使用默認綁定規則,this 指向 window
  • 當函數作為對象方法被調用,使用隱式綁定規則,this 指向這個對象
  • 當函數作為構造方法時,使用 new 綁定規則,this 指向返回的對象
  • apply/call/bind 要注意參數的傳遞和返回值不同
  • 箭頭函數要看該箭頭函數在哪個作用域下,this 就指向誰
  1. 綁定規則的優先級:

    fn()(全局環境)(默認綁定)< obj.fn()(隱式綁定) < fn.call(obj)=fn.apply(obj) = fn.bind(obj)(顯式綁定)< new fn()
  2. 此外要注意綁定失效的情況,善用箭頭函數來保證 this 的指向穩定
user avatar tianmiaogongzuoshi_5ca47d59bef41 头像 grewer 头像 Leesz 头像 alibabawenyujishu 头像 haoqidewukong 头像 smalike 头像 yinzhixiaxue 头像 front_yue 头像 jingdongkeji 头像 littlelyon 头像 zourongle 头像 leexiaohui1997 头像
点赞 251 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.