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
指的是全域性物件。