Stories

Detail Return Return

深入理解JavaScript之this指針 - Stories Detail

相信每一個前端的朋友都會遇到過this.xxx is undefined或者this.xxx is not a function的錯誤,明明我們定義了這個xxx,但是還是要報錯?令人百思不得其解,其實就是因為this指針的引用對象中,沒有找到這個定義xxx導致的,因此今天來總結一下this指針的幾種常見的指向問題。

由於this的定義中提到了上下文,因此我們在這裏先簡單的梳理一下Js中的上下文。

一、執行上下文

上下文分為:

  • 全局上下文:全局執行上下文是在代碼執行時就創建了,函數執行上下文是在函數調用的時候創建的。
  • 函數上下文:同一個函數,在不同時刻調用,會創建不同的執行上下文。

    變量和函數的上下文決定了它們可以訪問哪些數據以及他們的行為。
    每個函數調用都有自己的上下文。當代碼執行流進入函數時,函數的上下文被推到一個上下文棧上。在函數執行完之後,上下文棧會彈出函數的上下文。---《JavaScript 高級程序設計》

無論是否在嚴格模式下,在全局執行環境中(在任何函數體外部)this 都指向全局對象。然而,在嚴格模式下,如果進入執行環境時沒有設置 this 的值,this 會保持為 undefined。如下代碼:

  function f() {
    "use strict"; // 這裏是嚴格模式
    return this;
  }
  console.log(f() === undefined); // true

在瀏覽器中全局上下文也是window對象,node.js中的全局對象是global。

  console.log(this === window); // true
  a = 1;
  console.log(window.a); // 1
  this.b = "小白";
  console.log(window.b)  // "小白"
  console.log(b)         // "小白"

二、this指針的引用問題

this是函數中一個特殊的對象,它在標準函數和箭頭函數中的行為並不相同。

1.this在標準函數中的指向:

在標準函數的調用中,基本上可以分為以下三類:
①作為對象調用時,指針指向所屬對象。
②作為函數調用時,指針指向window。
③作為構造函數調用時,指針指向實例對象。

  var obj = {
    a: 1,
    b: function () {
      console.log(this);
    },
    c: function (a) {
      this.a = a;
      this.func = function () {
        console.log(this.a)//2
        console.log(this)//c {a: 2, func: ƒ}
      }
    }
  }
  //①作為對象調用時,指針指向所屬對象。
  obj.b();//{a: 1, b: ƒ,c: ƒ}
  //②作為函數調用時,指針指向window。
  var fun = obj.b;
  fun();//Window
  //③作為構造函數調用時,指針指向實例對象。
  var obj1 = new obj.c(2);
  obj1.func();

解釋①:在①例子中,將b作為方法調用的對象是obj,因此this指針引用的是obj對象。
解釋②:在②例子中,相當於將obj.b的函數賦值給fun,因此fun是被作為函數調用,因此this指針引用的是window對象。
解釋③:在例子③中,使用new構造了一個obj.c的實例obj1,調用func()方法的對象是obj1,因此打印的是2和對象obj1。
擴展:

  function Person(name,age){
    this.name = name;
    this.age = age;
  }
  var person1 = new Person("張三",18);
  console.log(person1);//Person{name: '張三', age: 18}
  console.log(person1.name,person1.age);//張三 18
  var person2 = Person("李四",12);
  console.log(person2);//undefined
  console.log(window.name,window.age);//李四 12

  在這裏補充一下new關鍵字的作用:
    a.在構造函數代碼開始執行前,創建一個空的對象
    b.修改this的指向:把this指向剛剛創建出來的空對象
    c.執行函數的代碼
    d.在函數完成之後,返回給this引用的對象,即創建出來的對象
  1.此時Person相當於構造函數,使用new關鍵字後,創建一個空對象person1,並將this指向這個對象person1,所以打印時person1是一個對象,裏面有name和age屬性
  2.為什麼person2是undefined呢?此時的Person(“李四”,12) 相當於函數執行,由於函數沒有返回值,所以person2為undefined。
  3.為什麼window中有name和age屬性可以打印?因為此時Person(“李四”,12)是在window對象中調用的,因此相當於window.Person(“李四”,12),所以Person函數執行時,將name和age屬性添加到window對象中去了。

2.this在箭頭函數中的指向:

在箭頭函數中,this引用的是定義箭頭函數的上下文。

  var obj = {
    d: function () {
      let a = () => {
        console.log(this)
      }
      a();
    }
  }
  //箭頭函數,指針始終指向定義箭頭函數的上下文。
  obj.d();//{d: ƒ}
  var funTest = obj.d;
  funTest()//Window{window: Window, self: Window, document: document, name: '', location: Location,…}

解釋:第一次obj.b()是由對象調用b中定義的方法,因此this指針在匿名函數中指向對象obj,定義箭頭函數a的時候指針指向obj,因此打印的是{d: ƒ},而第二次是作為函數調用funTest,因此,匿名函數中this指向window,因此打印的是window對象。

  var color = 'red';
  let sayColor = () => {
    console.log(this.color);
  }
  let obj = {
    color: 'yellow',
    sayColor: sayColor,
    objSayColor: function () {
      console.log(this.color)
    },
    objSayColor2: function () {
      return () => {
        console.log(this.color);
      }
    }
  }
  obj.sayColor();//red
  obj.objSayColor();//yellow
  obj.objSayColor2()();//yellow

3.this在閉包中的指向:

閉包指的是那些引用了另一個函數作用域中變量的函數,通常是在嵌套函數中實現的。---《JavaScript高級程序設計》

具體閉包從上下文、作用域鏈角度是如何實現的,我後續會寫一篇文章記錄詳解,在此就不多贅述了。
現在我們知道了this的引用,標準函數①當作為對象調用時,this指向調用的對象;②作為普通函數調用時,this指向window;③作為構造函數調用時,this指向生成的實例對象。箭頭函數中,this始終指向定義箭頭函數的上下文。

  var identity = 'The Window';
  let object = {
    identity: 'The Object',
    getIdentity() {
      return function () {
        console.log(this.identity);
      }
    }
  }
  object.getIdentity()();//The Window

大家看一下,這個結果和大家想的是否一樣呢?
在這個例子中,雖然getIdentity()函數是在對象中調用,所以,getIdentity()方法中的this指針指向object,但是返回的匿名函數,後面加上()相當於執行返回的匿名函數,如②作為普通函數調用時,this指針指向window;
可是,這樣就真的沒有辦法訪問object中的identity對象了嗎?其實不然,要知道每個函數在調用時都會自動創建兩個特殊變量:this和arguments內部函數雖然永遠不可能直接訪問外部函數的這兩個變量,但是,如果把this保存到閉包可以訪問的其他變量中,則是行得通的,代碼如下:

  var identity = 'The Window';
  let object = {
    identity: 'The Object',
    getIdentity() {
      let that = this;
      return function () {
        console.log(that.identity);
      }
    }
  }
  object.getIdentity()();//The Object

4.this在事件調用函數中的指向:

  • 當函數作為dom事件的處理函數時:this 指向觸發事件的元素。

    <button onclick="console.log(this)">
    Show this
    </button>
    //<button onclick="console.log(this)">Show this</button>
  • 當函數作為一個內聯事件處理函數:它的this指向監聽器所在的DOM元素:

    <button onclick="alert(this.tagName.toLowerCase());">
    Show this
    </button>//button

    但需注意下面這種情況,當函數調用時,它是屬於被獨立調用的函數,所以函數裏面的this指向的是window。

    <button onclick="alert((function(){return this})());">
    Show inner this
    </button>//[object Window]

    三、改變this指針指向的方法(apply、bind、call)

  • call方法:

  var a = 'The Window';
  var obj={
    a:'The Obj',
  };
  function sayA(){
    console.log(this.a);
  }
  sayA()//The Window
  sayA.call(obj)//The Obj
function func (a,b,c) {}

func.call(obj, 1,2,3)
// func 接收到的參數實際上是 1,2,3

func.call(obj, [1,2,3])
// func 接收到的參數實際上是 [1,2,3],undefined,undefined

需要注意以下幾點:
調用 call 的對象,必須是個函數 Function。
1)call 的第一個參數,是一個對象。 Function 的調用者,將會指向這個對象。 如果不傳,則默認為全局對象 window。
2)第二個參數開始,可以接收任意個參數。每個參數會映射到相應位置的 Function 的參數上。但是如果將所有的參數作為數組傳入,它們會作為一個整體映射到 Function 對應的第一個參數上,之後參數都為空。

  • apply方法:和call的作用是一樣的,需要注意的是:

1)它的調用者必須是函數 Function,並且只接收兩個參數,第一個參數的規則與 call 一致。
2)第二個參數,必須是數組或者類數組,它們會被轉換成類數組,傳入 Function 中,並且會被映射到 Function 對應的參數上。這也是 call 和 apply 之間,很重要的一個區別。

知識點總結:

apply、call的共同點:都能夠改變函數執行時的上下文(this指針指向),將一個對象的方法交給另一個對象來執行,並且是立即執行的。
apply、call的區別:傳入參數的方式,call接收單個參數,例如:func.call(obj, 1,2,3);而apply接受參數數組或者類數組,例如:func.apply(obj, [1,2,3])。

apply的巧用:

  let arr1 = [1, 2, 3];
  let arr2 = [4, 5, 6];
  Array.prototype.push.apply(arr1, arr2);//實現兩個數組合並。
  console.log(arr1); // [1, 2, 3, 4, 5, 6]
  let max = Math.max.apply(null, arr1);//結合Math.max返回數組最大值
  console.log(max)//6
  let min = Math.min.apply(null, arr1);//結合Math.max返回數組最小值
  console.log(min)//1
  • bind方法:

    bind() 方法創建一個新的函數,在 bind() 被調用時,這個新函數的 this 被指定為 bind() 的第一個參數,而其餘參數將作為新函數的參數,供調用時使用。 ---MDN

其實,bind 方法 與 apply 和 call 方法功能類似,也能改變函數體內的 this 指向。不同的是,bind 方法的返回值是函數,並且需要稍後調用,才會執行,而 apply 和 call 則是立即調用。

  var a = 'The Window';
  var obj={
    a:'The Obj',
  };
  function sayA(){
    console.log(this);
  }
  let CopyFunction=sayA.bind(obj)//不打印,此處相當於返回方法。
  CopyFunction();//{a: 'The Obj'}
  sayA.bind(obj)()//{a: 'The Obj'}
  sayA.bind()()//Window{window: Window, self: Window, document: document, name: '', location: Location,…}

參考資料:

  • 「乾貨」細説 call、apply 以及 bind 的區別和用法
  • MDN
  • 徹底理解js中this的指向問題
user avatar littlelyon Avatar zourongle Avatar anchen_5c17815319fb5 Avatar shuirong1997 Avatar jiavan Avatar zzd41 Avatar nqbefgvs Avatar nznznz Avatar yangxiansheng_5a1b9b93a3a44 Avatar congjunhua Avatar DolphinScheduler Avatar b_a_r_a_n Avatar
Favorites 60 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.