1. 程式人生 > 實用技巧 >JavaScript中的this指向問題

JavaScript中的this指向問題

this是 JavaScript 語言的一個關鍵字。它是函式執行時,在函式體內部自動生成的一個物件,只能在函式體內部使用。

在JavaScript語言之中,一切皆物件,執行環境也是物件,所以函式都是在某個物件下執行的,而this就是函式執行時所在的物件(環境)。這本來並不會讓我們糊塗,但是JavaScript支援執行環境動態切換,也就是說,this的指向是動態的,很難事先確定到底指向哪個物件,這才是最讓我們感到困惑的地方。

總之,this永遠指向一個物件,但是this的指向在函式定義的時候是確定不了的,只有函式執行的時候才能確定this到底指向誰實際上this最終指向的是那個呼叫它的物件。

記憶體的資料結構

JavaScript 語言之所以有this的設計,跟記憶體裡面的資料結構有關係。

var obj = { foo:  5 }

上面的程式碼將一個物件賦值給變數obj,JavaScript 引擎會先在記憶體裡面,生成一個物件{ foo: 5 },然後再把這個物件的記憶體地址賦值給變數obj。

也就是說,變數obj是一個地址。後面如果要讀取obj.foo,引擎會先從obj拿到記憶體地址,然後再從該地址讀出原始的物件,返回它的foo屬性。

原始的物件以字典結構儲存,每一個屬性名都對應一個屬性描述物件。舉例來說,上面例子的foo屬性,實際上是以下面的形式儲存的。

{
  foo: {
    [[value]]: 
5 [[writable]]: true [[enumerable]]: true [[configurable]]: true } }

注意,foo屬性的值儲存在屬性描述物件的value屬性裡面。

這樣的結構是很清晰的,問題在於屬性的值可能是一個函式。

首先我們應該知道,在JS中,陣列、函式、物件都是引用型別,在引數傳遞時也就是引用傳遞。

var obj = { foo: function () {} }

這時,引擎會將函式單獨儲存在記憶體中,然後再將函式的地址賦值給foo屬性的value屬性。

{
  foo: {
    [[value]]: 函式的地址
    ...
  }
}

由於函式是一個單獨的值,所以它可以在不同的環境(上下文)執行。

var f = function () {}
var obj = { f: f }

// 單獨執行
f()

// obj 環境執行
obj.f()

環境變數

JavaScript 允許在函式體內部,引用當前環境的其他變數。

var f = function () {
  console.log(x)
}

上面程式碼中,函式體裡面使用了變數x,該變數由執行環境提供。

現在問題就來了,由於函式可以在不同的執行環境執行,所以需要有一種機制,能夠在函式體內部獲得當前的執行環境(context)。所以,this就出現了,它設計的目的就是在函式體內部,指代函式當前的執行環境。

var x = 1
var f = function () {
  console.log(this.x)
}
var obj = {
  foo: f,
  x: 2
}
// 單獨執行
f() // 1
// obj 環境執行
obj.foo() // 2

上面程式碼中,函式f在全域性環境執行,this.x指向全域性環境的x

obj環境執行,this.x指向obj.x

函式的不同使用場合,this有不同的值

純粹的函式呼叫

這是函式的最通常用法,屬於全域性性呼叫,因此this就代表全域性物件。請看下面這段程式碼,它的執行結果是1。

var x = 1
function test() {
   console.log(this.x)
}
test() // 1

作為物件方法的呼叫

函式還可以作為某個物件的方法呼叫,這時this就指這個上級物件。

var obj = {
    user:"sun",
    fn:function(){
        console.log(this.user)  //sun
    }
}
obj.fn()

obj.fn()是通過obj找到fn,所以就是在obj環境執行。一旦var fn = obj.fn,變數fn就直接指向函式本身,所以fn()就變成在全域性環境執行。

var obj = {
    user:"sun",
    fn:function(){
        console.log(this.user) //undefined
}
} var fn =
obj.fn fn()

所以,this永遠指向的是最後呼叫它的物件,也就是看它執行的時候是誰呼叫的。

還有一種情況需要注意:

var obj = {
    user:"sun",
    fn:function(){
        console.log(this.user)  //sun
    }
}
window.obj.fn()

首先,window是js中的全域性物件,我們建立的變數實際上是給window新增屬性,所以這裡可以用window點obj物件。

但是這裡的this為什麼不是指向window,如果按照上面的理論,最終this指向的是呼叫它的物件。

這裡我們補充一下:

情況1:如果一個函式中有this,但是它沒有被上一級的物件所呼叫,那麼this指向的就是window,這裡需要說明的是在js的嚴格版中預設的this不再是window,而是undefined。

情況2:如果一個函式中有this,這個函式有被上一級的物件所呼叫,那麼this指向的就是上一級的物件。

情況3:如果一個函式中有this,這個函式中包含多個物件,儘管這個函式被最外層的物件所呼叫,this指向的也只是它上一級的物件。

事件繫結中的this

事件繫結共有三種方式:行內繫結、動態繫結、事件監聽。

行內繫結的兩種情況:

<input type="button" value="按鈕" onclick="click()">
<script>
    function click(){
        this //此函式的執行環境在全域性window物件下,因此this指向window
    }
</script>
<input type="button" value="按鈕" onclick="this">
<!-- 執行環境在節點物件中,因此this指向本節點物件 -->

行內繫結事件的語法是在html節點內,以節點屬性的方式繫結,屬性名是事件名稱前面加'on',屬性的值則是一段可執行的 JS 程式碼段,而屬性值最常見的就是一個函式呼叫。

當事件觸發時,屬性值就會作為JS程式碼被執行,當前執行環境下沒有click函式,因此瀏覽器就需要跳出當前執行環境,在整個環境中尋找一個叫click的函式並執行這個函式,所以函式內部的this就指向了全域性物件window。如果不是一個函式呼叫,直接在當前節點物件環境下使用this,那麼顯然this就會指向當前節點物件。

動態繫結與事件監聽:

<input type="button" value="按鈕" id="btn">
<script>
    var btn = document.getElementById('btn')
    btn.onclick = function(){
        this   // this指向本節點物件
    }
</script>

因為動態繫結的事件本就是為節點物件的屬性(事件名稱前面加'on')重新賦值為一個匿名函式,因此函式在執行時就是在節點物件的環境下,this自然就指向了本節點物件。

事件監聽中this指向的原理與動態繫結基本一致,所以不再闡述。

建構函式中的this

所謂建構函式,就是通過這個函式,可以生成一個新物件。這時,this就指這個新物件。

var x = '2'
function Pro(){
    this.x = '1'
    this.y = function(){}
}
var p = new Pro()
p.x  // '1'
x  // '2'

這裡之所以物件p可以點出函式Pro裡面的x是因為new關鍵字可以改變this的指向,將這個this指向物件p,為什麼我說p是物件,因為用了new關鍵字就是建立一個物件例項,我們這裡用變數p建立了一個Pro的例項(相當於複製了一份Pro到物件p裡面),此時僅僅只是建立,並沒有執行,而呼叫這個函式Pro的是物件p,那麼this指向的自然是物件p,那麼為什麼物件p中會有x,因為你已經複製了一份Pro函式到物件p中,用了new關鍵字就等同於複製了一份。

new 一個建構函式並執行函式內部程式碼的過程就是這個五個步驟,當 JS 引擎指向到第3步的時候,會強制的將this指向新創建出來的這個物件。基本不需要理解,因為這本就是 JS 中的語法規則,記住就可以了。

當this碰到return時:

function Fn()  {  
     this.user = 'sun' 
     return {}
}
var a = new Fn()
console.log(a.user) //undefined

function Fn()  {  
     this.user = 'sun' 
     return function(){}
}
var a = new Fn()
console.log(a.user) //undefined
function Fn()  {  
     this.user = 'sun' 
     return 1
}
var a = new Fn()
console.log(a.user) //sun
function Fn()  {  
     this.user = 'sun' 
     return undefined
}
var a = new Fn()
console.log(a.user) //sun

function Fn()  {  
     this.user = 'sun' 
     return null
}
var a = new Fn()
console.log(a.user) //sun

什麼意思呢?如果返回值是一個物件,那麼this指向的就是那個返回的物件,如果返回值不是一個物件那麼this還是指向函式的例項。

雖然null也是物件,但是在這裡this還是指向那個函式的例項,因為null比較特殊。

window定時器中的this

var obj = {
    fun:function(){
        console.log(this) 
    }
}
​
setInterval(obj.fun,1000)  // this指向window物件
setInterval('obj.fun()',1000)  // this指向obj物件

setInterval()是window物件下內建的一個方法,接受兩個引數,第一個引數允許是一個函式或者是一段可執行的 JS 程式碼,第二個引數則是執行前面函式或者程式碼的時間間隔;

在上面的程式碼中,setInterval(obj.fun,1000)的第一個引數是obj物件的fun,因為 JS 中函式可以被當做值來做引用傳遞,實際就是將這個函式的地址當做引數傳遞給了setInterval方法,換句話說就是setInterval的第一引數接受了一個函式,那麼此時1000毫秒後,函式的執行就已經是在window物件下了,也就是函式的呼叫者已經變成了window物件,所以其中的this則指向的全域性window物件。

而在setInterval('obj.fun()',1000)中的第一個引數,實際則是傳入的一段可執行的 JS 程式碼。1000毫秒後當 JS 引擎來執行這段程式碼時,則是通過obj物件來找到fun函式並呼叫執行,那麼函式的執行環境依然在 物件obj內,所以函式內部的this也就指向了obj物件。

函式物件的call()、apply() 方法

call和apply的作用一致,區別僅僅在函式引數傳遞的方式上。這個兩個方法的最大作用就是用來強制指定函式呼叫時this的指向。

以apply()為例:

var x = 0
function test() {
 console.log(this.x)
}

var obj = { x : 1 }
test.apply(obj) // 1

但是,apply()的引數為空時,預設呼叫全域性物件:

var x = 0
function test() {
 console.log(this.x)
}

var obj = { x : 1 }
obj.m = test
obj.m.apply() // 0 這時的執行結果為0,證明this指的是全域性物件。