JS 的 this 指來指去到底指向哪?(call, apply, bind 改變 this 指向)

跟風人發表於2019-04-23

前言:
this指向問題一直是js中最容易犯的錯誤之一。
今天就寫下這篇博文,談一下我對this的理解。
如果大家覺得有幫助,請點個贊關注我吧。
如果有不對的地方,歡迎大家指正!

先來看個思維導圖吧:

JS 的 this 指來指去到底指向哪?(call, apply, bind 改變 this 指向)

一、ES5中funciton的this指向

  1. 函式通過new 建構函式建立出來的例項物件this指向的是該例項物件。
function Person() {
    console.log(this, '1') //Person
    this.say = function (){
      console.log( this, '2'); //Person
    }
  }
  // 在建構函式中,this指向的是new出來的例項,所以兩處this都指向Person
  var per = new Person()
  per.say()
複製程式碼


2. 因為建構函式本身是無法訪問其自身的值,例項化物件可以。 這裡為了比較,做了以下的寫法:

   function Person() {
      console.log(this, '1') //Window
      this.say = function (){
        console.log( this, '2'); //Window
        this.eat = function () {
          console.log(this, '3'); //Window
        }
        eat()
      }
      say()
    }
    Person()
複製程式碼

解析:用函式名()呼叫時,無論巢狀多少層,this預設指向的也是window。


3. 在物件中建立的函式this指向是:this 永遠指向最後呼叫它的那個物件!this 永遠指向最後呼叫它的那個物件!this 永遠指向最後呼叫它的那個物件!(重要的話說3次)牢記這條就不會出錯。

//eg1:  
var a = 2
var obj = {
  a: 1,
  b: function() {
    console.log(this.a) //1  這裡是obj呼叫的,所以this指向obj
  }
}
obj.b()
複製程式碼

假如我們假如了setTimeout呢,情況會是什麼樣的呢?

//eg2:  
var a = 2
var obj = {
  a: 1,
  b: function() {
    window.setTimeout(function() {
      console.log(this.a) //2  
    })
    console.log(this.a) //1 
  }
}
obj.b()
複製程式碼

解析:setTimeout的執行環境是Window,所以會把this指向改變到Window上。


4. 那假如在物件中巢狀多層呢?情況會是怎麼樣的呢?

    const obj = {
      a: function() { console.log(this) },
      b: {
        c: function() {console.log(this)}
      }
    }
    obj.a()          // obj, obj呼叫的a()方法
    obj.b.c()        //obj.b, obj.b呼叫的a()方法
複製程式碼

解析:還是那句話:this 永遠指向最後呼叫它的那個物件!,這下記住了吧。


5. 還有一個情況就是閉包中的this指向,這裡也講解一下。 (面試的時候可能會出這種變形題。)

//eg1:
var name = '張' 
function Person() {
  this.name = '柳';
  let say = function (){
    console.log( this.name + ' do Something');//張 do Something
  }
  say()
}
var per = new Person()
複製程式碼

我們可以把這道題換種寫法:

//eg2: 
var name = '張' 
function Person() {
  this.name = '柳';
  return function (){  //本質都是匿名函式的自執行
    console.log( this.name + ' do Something');//張 do Something
  }
}
var per = new Person()
per()
複製程式碼

解析:看到這就明白了,其實這是一道閉包題。閉包函式具有匿名函式自執行的特性,預設this指向是掛在Window下的。



二、 ES6箭頭函式中的this

《深入淺出ES6》(65頁)中關於箭頭函式this的解釋:
    箭頭函式中沒有this繫結,必須通過查詢作用域鏈來決定其值。
    如果箭頭函式被非箭頭函式包圍,那麼this繫結的是最近一層非箭頭函式的this;
    否則,this的值會被設定為undefined。
複製程式碼

我的理解:

如果箭頭函式被非箭頭函式包圍,那麼this繫結的是最近一層非箭頭函式的this,我理解的這句話是說,如果箭頭函式的父級是function,那麼箭頭函式的this指向會與funciton中的this指向保持一致。
如果上面的聽不太明白,那麼可以參考大家的理解方式:為箭頭函式的this是在定義的時候繫結的。那麼問題來了:

1.何為定義時繫結?

//eg1:
var x=11;
var obj={
  x:22,
  say:function(){
    console.log(this.x)
  }
}
obj.say();
//console.log輸出的是2
複製程式碼

解析:一般的定義函式跟我們的理解是一樣的,執行的時候決定this的指向,我們可以知道當執行obj.say()時候,this指向的是obj這個物件。

//eg2:
var x=11;
var obj={
 x:22,
 say:()=>{
   console.log(this.x);
 }
}
obj.say();
//輸出的值為11

複製程式碼

解析:
這樣就非常奇怪了如果按照之前function定義應該輸出的是22,而此時輸出了11,那麼如何解釋ES6中箭頭函式中的this是定義時的繫結呢?

所謂的定義時候繫結,this繫結的是最近一層非箭頭函式的this;因為箭頭函式是在obj中執行的,obj的執行環境就是window,因此這裡的this.x實際上表示的是window.x,因此輸出的是11。


2. 理解了物件中的this定義後,還有幾點要提一下:
物件中箭頭函式的this指向的是window。且無法改變其指向。
原因:函式的this可以用call方法來手動指定,是為了減少this的複雜性,箭頭函式無法用call等方法來指定this。

//eg3:
  var a = 2
  const obj = {
    a: 1,
    b: () => {
      console.log(this.a)
    }
  }
  obj.b()//2 預設繫結外層this
  obj.b.call(obj.a)//2 不能用call方法修改裡面的this
複製程式碼
  1. 假如在window.setTimeout中使用箭頭函式呢?
//eg4:
const obj = {
    a: function() {
        console.log(this)
        window.setTimeout(() => { 
            console.log(this) 
        }, 100)
    }
  }
  obj.a()  //第一個this是obj物件,第二個this還是obj物件
複製程式碼

解析:函式obj.a沒有使用箭頭函式,因為它的this還是obj;
而setTimeout裡的函式使用了箭頭函式,所以它會和外層的this保持一致,也是obj;
如果setTimeout裡的函式沒有使用箭頭函式,那麼它列印出來的應該是window物件。

  1. 多層物件巢狀裡函式的this
 const obj = {
    a: function() { console.log(this) },
    b: {
    	c: () => {console.log(this)}
    }
  }
  obj.a()   //沒有使用箭頭函式打出的是obj
  obj.b.c()  //打出的是window物件!!
複製程式碼

解析:obj.a呼叫後打出來的是obj物件,而obj.b.c呼叫後打出的是window物件而非obj, 這表示多層物件巢狀裡箭頭函式裡this是和最最外層保持一致的。



三、改變this指向的辦法

  1. call()、apply()、bind()改變this指向
//eg1:
const obj = {
    log: function() {
      console.log(this)
    }
  }
  
//1.不用變數接收:this預設指向為obj
 obj.log() //obj
  obj.log.call(window)  //window  call和apply都會返回一個新函式
  obj.log.apply(window)  //window
  obj.log.bind(window)() //window bind則是返回改變了上下文後的一個函式,要想執行的話,還需要加個括號呼叫。
  
//2.用變數接收:用變數接收的話,this的預設指向就變成全域性了
  var objName = obj.log
  objName()  //王
  // call、apply、bind的用法還和上面的一樣,這裡就不重複了。
複製程式碼
  1. 除了call、apply、bind這三種改變this指向的辦法, 還有常規的用一個變數儲存當前this指向,然後在去呼叫。
eg2:
  var _this = this 
  const obj = {
    log: function() {
      console.log(_this) 
    }
  }
  obj.log()//此時this的當前指向不再是obj,而是window了。
複製程式碼

小結: 3種改變this指向方法的區別

 1、call和apply改變了函式的this上下文後便執行該函式,而bind則是返回改變了上下文後的一個函式。
 2、call、apply的區別: 他們倆之間的差別在於引數的區別,
    call和aplly的第一個引數都是要改變上下文的物件,而call從第二個引數開始以引數列表的形式展現,
    apply則是把除了改變上下文物件的引數放在一個陣列裡面作為它的第二個引數
複製程式碼
  1. call()、apply()的一些其他用處:求陣列中的最大、最小值
  let arr1 = [1, 2, 19, 6];
  //例子:求陣列中的最值
  console.log(Math.max.call(null, 1,2,19,6)); // 19
  console.log(Math.max.call(null, arr1)); // NaN
  console.log(Math.max.apply(null, arr1)); //  19 直接可以用arr1傳遞進去

複製程式碼

合併陣列:

  var arr1=[1,2,3];
  var arr2=[4,5,6];
  arr1.push.apply(arr1,arr2);
  alert(arr1)//1,2,3,4,5,6 
  alert(arr2)//4,5,6 
複製程式碼

解析:同理,apply將陣列裝換為引數列表的集合。

結尾: 以上就是對js中的this指向,和call()、apply()方法一些需要知道的小技巧。
如果覺得對你有幫助,請給作者一點小小的鼓勵,點個贊或者收藏吧。
有需要溝通的請聯絡我:微信( wx9456d ) 郵箱( allan_liu986@163.com )

相關文章