什麼是this?深入解析JavaScript中的this

2022-08-04 18:02:16
什麼是this?下面本篇文章給大家介紹一下JavaScript中的this,並聊聊this在函數不同呼叫方式下的區別,希望對大家有所幫助!

JavaScript中的this格外的不一樣,比如Java語言中的this是在程式碼的執行階段是不可更改,而JavaScript的this是在呼叫階段進行繫結。因為這一性質所以給了this很大的發揮空間。但其在嚴格模式和非嚴格模式下又有些不一樣,在函數的不同呼叫方式也導致this有些區別。

什麼是this?

首先對this的下個定義:this是在執行上下文建立時確定的一個在執行過程中不可更改的變數。

所謂執行上下文,就是JavaScript引擎在執行一段程式碼之前將程式碼內部會用到的一些變數函數this提前宣告然後儲存在變數物件中的過程。這個'程式碼片段'包括:全域性程式碼(script標籤內部的程式碼)、函數內部程式碼eval內部程式碼。而我們所熟知的作用域鏈也會在儲存在這裡,以一個類陣列的形式儲存在對應函數的[[Scopes]]屬性中。

this只在函數呼叫階段確定,也就是執行上下文建立的階段進行賦值,儲存在變數物件中。這個特性也導致了this的多變性:即當函數在不同的呼叫方式下都可能會導致this的值不同。

上面我們說過了在嚴格模式下和非嚴格模式下this表現不同:

var a = 1;
function fun() {
   'use strict';
    var a = 2;
      return this.a;
}
fun();//報錯 Cannot read property 'a' of undefined
  • 嚴格模式下,this指向undefined;

var a = 1;
function fun() {
    var a = 2;
      return this.a;
}
fun();//1
  • 非嚴格模式下this指向window;

上面同一段程式碼,在不同模式下之所以有不同表現,就是因為this在嚴格模式,非嚴格模式下的不同。

結論:當函數獨立呼叫的時候,在嚴格模式下它的this指向undefined,在非嚴格模式下,當this指向undefined的時候,自動指向全域性物件(瀏覽器中就是window)

多提一句,在全域性環境下,this就是指向自己,再看一個例子:

this.a = 1;
var b = 1;
c = 1;
console.log(this === window)//true
//這三種都能得到想要的結果,全域性上下文的變數物件中存在這三個變數

再多提一句,當this不在函數中用的時候會怎樣?看一個例子:

var a = 1000;
var obj = {
    a: 1,
      b: this.a + 1
}
function fun() {
    var obj = {
          a: 1,
        c: this.a + 2 //嚴格模式下這塊報錯 Cannot read property 'a' of undefined
    }
    return obj.c;
}
console.log(fun());//1002
console.log(obj.b);//1001

這種情況下this還是指向了window。那麼我們可以單獨下個結論:

當obj在全域性宣告的時候,obj內部屬性中的this指向全域性物件,當obj在一個函數中宣告的時候,嚴格模式下this會指向undefined,非嚴格模式自動轉為指向全域性物件。

好了,剛剛小試牛刀下,知道了嚴格模式和非嚴格模式下this的區別,然而我們日常應用最多的還是在函數中用this,上面也說過了this在函數的不同呼叫方式還有區別,那麼函數的呼叫方式都有哪些呢?四種:

  • 在全域性環境或是普通函數中直接呼叫
  • 作為物件的方法
  • 使用apply和call
  • 作為建構函式

下面分別就四種情況展開:

直接呼叫

上面的例子,其實就是直接呼叫的,不過我決定再寫一個例子:

var a = 1;
var obj  =  {
    a: 2,
      b: function () {
        function fun() {
          return this.a
        }
       console.log(fun());
    }
} 
obj.b();//1

fun函數雖然在obj.b方法中定義,但它還是一個普通函數,直接呼叫在非嚴格模式下指向undefined,又自動指向了全域性物件,正如預料,嚴格模式會報錯undefined.a不成立,a未定義。

重要的事情再說一遍:當函數獨立呼叫的時候,在嚴格模式下它的this指向undefined,在非嚴格模式下,當this指向undefined的時候,自動指向全域性物件(瀏覽器中就是window)。

作為物件的方法

var a = 1;
var obj = {
  a: 2,
  b: function() {
    return this.a;
  }
}
console.log(obj.b())//2

b所參照的匿名函數作為obj的一個方法呼叫,這時候this指向呼叫它的物件。這裡也就是obj。那麼如果b方法不作為物件方法呼叫呢?啥意思呢,就是這樣:

var a = 1;
var obj = {
  a: 2,
  b: function() {
    return this.a;
  }
}
var t = obj.b;
console.log(t());//1

如上,t函數執行結果竟然是全域性變數1,為啥呢?這就涉及Javascript的記憶體空間了,就是說,obj物件的b屬性儲存的是對該匿名函數的一個參照,可以理解為一個指標。當賦值給t的時候,並沒有單獨開闢記憶體空間儲存新的函數,而是讓t儲存了一個指標,該指標指向這個函數。相當於執行了這麼一段虛擬碼:

var a = 1;
function fun() {//此函數儲存在堆中
    return this.a;
}
var obj = {
  a: 2,
  b: fun //b指向fun函數
}
var t = fun;//變數t指向fun函數
console.log(t());//1

此時的t就是一個指向fun函數的指標,呼叫t,相當於直接呼叫fun,套用以上規則,列印出來1自然很好理解了。

使用apply,call

關於apply和call是幹什麼的怎麼用本文不涉及,請移駕:applycall

這是個萬能公式,實際上上面直接呼叫的程式碼,我們可以看成這樣的:

function fun() {
      return this.a;
}
fun();//1
//嚴格模式
fun.call(undefined)
//非嚴格模式
fun.call(window)

這時候我們就可以解釋下,為啥說在非嚴格模式下,當函數this指向undefined的時候,會自動指向全域性物件,如上,在非嚴格模式下,當呼叫fun.call(undefined)的時候列印出來的依舊是1,就是最好的證據。

為啥說是萬能公式呢?再看函數作為物件的方法呼叫:

var a = 1;
var obj = {
  a: 2,
  b: function() {
    return this.a;
  }
}
obj.b()
obj.b.call(obj)

如上,是不是很強大,可以理解為其它兩種都是這個方法的語法糖罷了,那麼apply和call是不是真的萬能的呢?並不是,ES6的箭頭函數就是特例,因為箭頭函數的this不是在呼叫時候確定的,這也就是為啥說箭頭函數好用的原因之一,因為它的this固定不會變來變去的了。關於箭頭函數的this我們稍後再說。

作為建構函式

何為建構函式?所謂建構函式就是用來new物件的函數,像FunctionObjectArrayDate等都是全域性定義的建構函式。其實每一個函數都可以new物件,那些批次生產我們需要的物件的函數就叫它建構函式罷了。注意,建構函式首字母記得大寫。

function Fun() {
  this.name = 'Damonre';
  this.age = 21;
  this.sex = 'man';
  this.run = function () {
    return this.name + '正在跑步';
  }
}
Fun.prototype = {
  contructor: Fun,
  say: function () {
    return this.name + '正在說話';
  }
}
var f = new Fun();
f.run();//Damonare正在跑步
f.say();//Damonare正在說話

如上,如果函數作為建構函式用,那麼其中的this就代表它即將new出來的物件。為啥呢?new做了啥呢?

虛擬碼如下:

function Fun() {
  //new做的事情
  var obj = {};
  obj.__proto__ = Fun.prototype;//Base為建構函式
  obj.name = 'Damonare';
  ...//一系列賦值以及更多的事
  return obj
}

也就是說new做了下面這些事:

  • 建立一個臨時物件
  • 給臨時物件繫結原型
  • 給臨時物件對應屬性賦值
  • 將臨時物件return

也就是說new其實就是個語法糖,this之所以指向臨時物件還是沒逃脫上面說的幾種情況。

當然如果直接呼叫Fun(),如下:

function Fun() {
  this.name = 'Damonre';
  this.age = 21;
  this.sex = 'man';
  this.run = function () {
    return this.name + '正在跑步';
  }
}
Fun();
console.log(window)

其實就是直接呼叫一個函數,this在非嚴格模式下指向window,你可以在window物件找到所有的變數。

另外還有一點,prototype物件的方法的this指向範例物件,因為範例物件的__proto__已經指向了原型函數的prototype。這就涉及原型鏈的知識了,即方法會沿著物件的原型鏈進行查詢。

箭頭函數

剛剛提到了箭頭函數是一個不可以用call和apply改變this的典型。

我們看下面這個例子:

var a = 1;
var obj = {
  a: 2
};
var fun = () => console.log(this.a);
fun();//1
fun.call(obj)//1

以上,兩次呼叫都是1。

那麼箭頭函數的this是怎麼確定的呢?箭頭函數會捕獲其所在上下文的 this 值,作為自己的 this,也就是說箭頭函數的this在詞法層面就完成了繫結。apply,call方法只是傳入引數,卻改不了this。

var a = 1;
var obj = {
  a: 2
};
function fun() {
    var a = 3;
    let f = () => console.log(this.a);
      f();
};
fun();//1
fun.call(obj);//2

如上,fun直接呼叫,fun的上下文中的this值為window,注意,這個地方有點繞。fun的上下文就是此箭頭函數所在的上下文,因此此時f的this為fun的this也就是window。當fun.call(obj)再次呼叫的時候,新的上下文建立,fun此時的this為obj,也就是箭頭函數的this值。

再來一個例子:

function Fun() {
    this.name = 'Damonare';
}
Fun.prototype.say = () => {
    console.log(this);
}
var f = new Fun();
f.say();//window

有的同學看到這個例子會很懵逼,感覺上this應該指向f這個範例物件啊。不是的,此時的箭頭函數所在的上下文是__proto__所在的上下文也就是Object函數的上下文,而Object的this值就是全域性物件。

那麼再來一個例子:

function Fun() {
    this.name = 'Damonare';
      this.say = () => {
        console.log(this);
    }
}
var f = new Fun();
f.say();//Fun的範例物件

如上,this.say所在的上下文,此時箭頭函數所在的上下文就變成了Fun的上下文環境,而因為上面說過當函數作為建構函式呼叫的時候(也就是new的作用)上下文環境的this指向範例物件。

【相關推薦:

以上就是什麼是this?深入解析JavaScript中的this的詳細內容,更多請關注TW511.COM其它相關文章!