涵義
this 關鍵字是一個非常重要的語法點。毫不誇張地說,不理解它的含義,大部分開發任務都無法完成。
首先, this 總是返回一個對象,簡單說,就是返回屬性或方法“當前”所在的對象。
this.property
上面代碼中, this 就代表 property 屬性當前所在的對象。
下面是一個實際的例子。
1 var person = { 2 name: '張三', 3 describe: function () { 4 return '姓名:'+ this.name; 5 } 6 }; 7 8 person.describe() 9 // "姓名:張三"
上面代碼中, this.name 表示 describe 方法所在的當前對象的 name 屬性。調用 person.describe 方法時, describe 方法所在的當前對象是 person ,所以就是調用 person.name 。
由於對象的屬性可以賦給另一個對象,所以屬性所在的當前對象是可變的,即 this 的指向是可變的。
1 var A = { 2 name: '張三', 3 describe: function () { 4 return '姓名:'+ this.name; 5 } 6 }; 7 8 var B = { 9 name: '李四' 10 }; 11 12 B.describe = A.describe; 13 B.describe() 14 // "姓名:李四"
上面代碼中, A.describe 屬性被賦給B
,於是 B.describe 就表示 describe 方法所在的當前對象是B
,所以 this.name 就指向 B.name 。
稍稍重構這個例子, this 的動態指向就能看得更清楚。
function f() { return '姓名:'+ this.name; } var A = { name: '張三', describe: f }; var B = { name: '李四', describe: f }; A.describe() // "姓名:張三" B.describe() // "姓名:李四"
上面代碼中,函數f
內部使用了 this 關鍵字,隨着f
所在的對象不同, this 的指向也不同。
只要函數被賦給另一個變量, this 的指向就會變。
1 var A = { 2 name: '張三', 3 describe: function () { 4 return '姓名:'+ this.name; 5 } 6 }; 7 8 var name = '李四'; 9 var f = A.describe; 10 f() // "姓名:李四"
上面代碼中, A.describe 被賦值給變量f,內部的 this 就會指向f
運行時所在的對象(本例是頂層對象)。
再看一個網頁編程的例子。
1 <input type="text" name="age" size=3 onChange="validate(this, 18, 99);"> 2 3 <script> 4 function validate(obj, lowval, hival){ 5 if ((obj.value < lowval) || (obj.value > hival)) 6 console.log('Invalid Value!'); 7 } 8 </script>
上面代碼是一個文本輸入框,每當用戶輸入一個值,就會調用 onChange 回調函數,驗證這個值是否在指定范圍。回調函數傳入 this ,就代表傳入當前對象(即文本框),然后就可以從 this.value 上面讀到用戶的輸入值。
總結一下,JavaScript語言之中,一切皆對象,運行環境也是對象,所以函數都是在某個對象之中運行, this 就是這個對象(環境)。這本來並不會讓用戶糊塗,但是JavaScript支持運行環境動態切換,也就是說, this 的指向是動態的,沒有辦法事先確定到底指向哪個對象,這才是最讓初學者感到困惑的地方。
如果一個函數在全局環境中運行,那么 this 就是指頂層對象(瀏覽器中為 window 對象)。
1 function f() { 2 return this; 3 } 4 5 f() === window // true
上面代碼中,函數f
在全局環境運行,它內部的 this 就指向頂層對象 window 。
可以近似地認為, this 是所有函數運行時的一個隱藏參數,指向函數的運行環境。
使用場合
this 的使用可以分成以下幾個場合。
(1)全局環境
在全局環境使用 this ,它指的就是頂層對象 window 。
1 this === window // true 2 function f() { 3 console.log(this === window); // true 4 }
上面代碼說明,不管是不是在函數內部,只要是在全局環境下運行, this 就是指頂層對象 window 。
(2)構造函數
構造函數中的 this ,指的是實例對象。
1 var Obj = function (p) { 2 this.p = p; 3 }; 4 5 Obj.prototype.m = function() { 6 return this.p; 7 };
上面代碼定義了一個構造函數Obj
。由於 this 指向實例對象,所以在構造函數內部定義 this.p ,就相當於定義實例對象有一個p
屬性;然后m
方法可以返回這個p屬性。
1 var o = new Obj('Hello World!'); 2 o.p // "Hello World!" 3 o.m() // "Hello World!"
(3)對象的方法
當A對象的方法被賦予B對象,該方法中的 this 就從指向A對象變成了指向B對象。所以要特別小心,將某個對象的方法賦值給另一個對象,會改變 this 的指向。
請看下面的代碼。
1 var obj ={ 2 foo: function () { 3 console.log(this); 4 } 5 }; 6 7 obj.foo() // obj
上面代碼中, obj.foo 方法執行時,它內部的 this 指向 obj 。
但是,只有這一種用法(直接在 obj 對象上調用 foo 方法), this 指向 obj ;其他用法時, this 都指向代碼塊當前所在對象(瀏覽器為 window 對象)。
1 // 情況一 2 (obj.foo = obj.foo)() // window 3 4 // 情況二 5 (false || obj.foo)() // window 6 7 // 情況三 8 (1, obj.foo)() // window
上面代碼中,obj.foo
先運算再執行,即使它的值根本沒有變化,this
也不再指向obj
了。
可以這樣理解,在JavaScript引擎內部,obj
和obj.foo
儲存在兩個內存地址,簡稱為M1
和M2
。只有obj.foo()
這樣調用時,是從M1
調用M2
,因此this
指向obj
。但是,上面三種情況,都是直接取出M2
進行運算,然后就在全局環境執行運算結果(還是M2
),因此this
指向全局環境。
上面三種情況等同於下面的代碼。
1 // 情況一 2 (obj.foo = function () { 3 console.log(this); 4 })() 5 6 // 情況二 7 (false || function () { 8 console.log(this); 9 })() 10 11 // 情況三 12 (1, function () { 13 console.log(this); 14 })()
同樣的,如果某個方法位於多層對象的內部,這時為了簡化書寫,把該方法賦值給一個變量,往往會得到意料之外的結果。
1 var a = { 2 b: { 3 m: function() { 4 console.log(this.p); 5 }, 6 p: 'Hello' 7 } 8 }; 9 10 var hello = a.b.m; 11 hello() // undefined
上面代碼中,m
是多層對象內部的一個方法。為求簡便,將其賦值給hello
變量,結果調用時,this
指向了頂層對象。為了避免這個問題,可以只將m
所在的對象賦值給hello
,這樣調用時,this
的指向就不會變。
1 var hello = a.b; 2 hello.m() // Hello
(4)Node
在Node中,this
的指向又分成兩種情況。全局環境中,this
指向全局對象global
;模塊環境中,this
指向module.exports。
1 // 全局環境 2 this === global // true 3 4 // 模塊環境 5 this === module.exports // true
使用注意點
(1)避免多層this
由於this
的指向是不確定的,所以切勿在函數中包含多層的this
。
1 var o = { 2 f1: function () { 3 console.log(this); 4 var f2 = function () { 5 console.log(this); 6 }(); 7 } 8 } 9 10 o.f1() 11 // Object 12 // Window
上面代碼包含兩層this
,結果運行后,第一層指向該對象,第二層指向全局對象。實際執行的是下面的代碼。
1 var temp = function () { 2 console.log(this); 3 }; 4 5 var o = { 6 f1: function () { 7 console.log(this); 8 var f2 = temp(); 9 } 10 }
一個解決方法是在第二層改用一個指向外層this
的變量。
1 var o = { 2 f1: function() { 3 console.log(this); 4 var that = this; 5 var f2 = function() { 6 console.log(that); 7 }(); 8 } 9 } 10 11 o.f1() 12 // Object 13 // Object
上面代碼定義了變量that
,固定指向外層的this
,然后在內層使用that
,就不會發生this
指向的改變。
事實上,使用一個變量固定this
的值,然后內層函數調用這個變量,是非常常見的做法,有大量應用,請務必掌握。
JavaScript 提供了嚴格模式,也可以硬性避免這種問題。在嚴格模式下,如果函數內部的this
指向頂層對象,就會報錯。
1 var counter = { 2 count: 0 3 }; 4 counter.inc = function () { 5 'use strict'; 6 this.count++ 7 }; 8 var f = counter.inc; 9 f() 10 // TypeError: Cannot read property 'count' of undefined
上面代碼中,inc
方法通過'use strict'
聲明采用嚴格模式,這時內部的this
一旦指向頂層對象,就會報錯。
(2)避免數組處理方法中的this
數組的map
和foreach
方法,允許提供一個函數作為參數。這個函數內部不應該使用this
。
1 var o = { 2 v: 'hello', 3 p: [ 'a1', 'a2' ], 4 f: function f() { 5 this.p.forEach(function (item) { 6 console.log(this.v + ' ' + item); 7 }); 8 } 9 } 10 11 o.f() 12 // undefined a1 13 // undefined a2
上面代碼中,foreach
方法的回調函數中的this
,其實是指向window
對象,因此取不到o.v
的值。原因跟上一段的多層this
是一樣的,就是內層的this
不指向外部,而指向頂層對象。
解決這個問題的一種方法,是使用中間變量。
1 var o = { 2 v: 'hello', 3 p: [ 'a1', 'a2' ], 4 f: function f() { 5 var that = this; 6 this.p.forEach(function (item) { 7 console.log(that.v+' '+item); 8 }); 9 } 10 } 11 12 o.f() 13 // hello a1 14 // hello a2
另一種方法是將this
當作foreach
方法的第二個參數,固定它的運行環境。
1 var o = { 2 v: 'hello', 3 p: [ 'a1', 'a2' ], 4 f: function f() { 5 this.p.forEach(function (item) { 6 console.log(this.v + ' ' + item); 7 }, this); 8 } 9 } 10 11 o.f() 12 // hello a1 13 // hello a2
(3)避免回調函數中的this
回調函數中的this
往往會改變指向,最好避免使用。
1 var o = new Object(); 2 3 o.f = function () { 4 console.log(this === o); 5 } 6 7 o.f() // true
上面代碼表示,如果調用o
對象的f方法,其中的this
就是指向o
對象。
但是,如果將f
方法指定給某個按鈕的click
事件,this的指向就變了。
$('#button').on('click', o.f);
點擊按鈕以后,控制台會顯示false
。原因是此時this
不再指向o
對象,而是指向按鈕的DOM對象,因為f
方法是在按鈕對象的環境中被調用的。這種細微的差別,很容易在編程中忽視,導致難以察覺的錯誤。
為了解決這個問題,可以采用下面的一些方法對this
進行綁定,也就是使得this
固定指向某個對象,減少不確定性。
綁定 this 的方法
this
的動態切換,固然為JavaScript創造了巨大的靈活性,但也使得編程變得困難和模糊。有時,需要把this
固定下來,避免出現意想不到的情況。JavaScript提供了call
、apply
、bind
這三個方法,來切換/固定this
的指向。
function.prototype.call()
函數實例的call
方法,可以指定函數內部this
的指向(即函數執行時所在的作用域),然后在所指定的作用域中,調用該函數。
1 var obj = {}; 2 3 var f = function () { 4 return this; 5 }; 6 7 f() === this // true 8 f.call(obj) === obj // true
上面代碼中,在全局環境運行函數f
時,this
指向全局環境;call
方法可以改變this
的指向,指定this
指向對象obj
,然后在對象obj
的作用域中運行函數f
。
call
方法的參數,應該是一個對象。如果參數為空、null
和undefined
,則默認傳入全局對象。
1 var n = 123; 2 var obj = { n: 456 }; 3 4 function a() { 5 console.log(this.n); 6 } 7 8 a.call() // 123 9 a.call(null) // 123 10 a.call(undefined) // 123 11 a.call(window) // 123 12 a.call(obj) // 456
上面代碼中,a
函數中的this
關鍵字,如果指向全局對象,返回結果為123
。如果使用call
方法將this
關鍵字指向obj
對象,返回結果為456
。可以看到,如果call
方法沒有參數,或者參數為null
或undefined
,則等同於指向全局對象。
如果call
方法的參數是一個原始值,那么這個原始值會自動轉成對應的包裝對象,然后傳入call
方法。
1 var f = function () { 2 return this; 3 }; 4 5 f.call(5) 6 // Number {[[PrimitiveValue]]: 5}
上面代碼中,call
的參數為5
,不是對象,會被自動轉成包裝對象(Number
的實例),綁定f
內部的this
。
call
方法還可以接受多個參數。
func.call(thisValue, arg1, arg2, ...)
call
的第一個參數就是this
所要指向的那個對象,后面的參數則是函數調用時所需的參數。
1 function add(a, b) { 2 return a + b; 3 } 4 5 add.call(this, 1, 2) // 3
上面代碼中,call
方法指定函數add
內部的this
綁定當前環境(對象),並且參數為1
和2
,因此函數add
運行后得到3
。
call
方法的一個應用是調用對象的原生方法。
1 var obj = {}; 2 obj.hasOwnProperty('toString') // false 3 4 // 覆蓋掉繼承的 hasOwnProperty 方法 5 obj.hasOwnProperty = function () { 6 return true; 7 }; 8 obj.hasOwnProperty('toString') // true 9 10 Object.prototype.hasOwnProperty.call(obj, 'toString') // false
上面代碼中,hasOwnProperty
是obj
對象繼承的方法,如果這個方法一旦被覆蓋,就不會得到正確結果。call
方法可以解決這個方法,它將hasOwnProperty
方法的原始定義放到obj
對象上執行,這樣無論obj
上有沒有同名方法,都不會影響結果。
function.prototype.apply()
apply
方法的作用與call
方法類似,也是改變this
指向,然后再調用該函數。唯一的區別就是,它接收一個數組作為函數執行時的參數,使用格式如下。
func.apply(thisValue, [arg1, arg2, ...])
apply
方法的第一個參數也是this
所要指向的那個對象,如果設為null或undefined,則等同於指定全局對象。第二個參數則是一個數組,該數組的所有成員依次作為參數,傳入原函數。原函數的參數,在call
方法中必須一個個添加,但是在apply
方法中,必須以數組形式添加。
請看下面的例子。
1 function f(x,y){ 2 console.log(x+y); 3 } 4 5 f.call(null,1,1) // 2 6 f.apply(null,[1,1]) // 2
上面的f函數本來接受兩個參數,使用apply方法以后,就變成可以接受一個數組作為參數。
利用這一點,可以做一些有趣的應用。
(1)找出數組最大元素
JavaScript不提供找出數組最大元素的函數。結合使用apply方法和Math.max方法,就可以返回數組的最大元素。
1 var a = [10, 2, 4, 15, 9]; 2 Math.max.apply(null, a) 3 // 15
(2)將數組的空元素變為undefined
通過apply
方法,利用Array構造函數將數組的空元素變成undefined。
1 Array.apply(null, ["a",,"b"]) 2 // [ 'a', undefined, 'b' ]
空元素與undefined
的差別在於,數組的forEach
方法會跳過空元素,但是不會跳過undefined
。因此,遍歷內部元素的時候,會得到不同的結果。
1 var a = ['a', , 'b']; 2 3 function print(i) { 4 console.log(i); 5 } 6 7 a.forEach(print) 8 // a 9 // b 10 11 Array.apply(null, a).forEach(print) 12 // a 13 // undefined 14 // b
(3)轉換類似數組的對象
另外,利用數組對象的slice
方法,可以將一個類似數組的對象(比如arguments
對象)轉為真正的數組。
1 Array.prototype.slice.apply({0:1,length:1}) 2 // [1] 3 4 Array.prototype.slice.apply({0:1}) 5 // [] 6 7 Array.prototype.slice.apply({0:1,length:2}) 8 // [1, undefined] 9 10 Array.prototype.slice.apply({length:1}) 11 // [undefined]
上面代碼的apply
方法的參數都是對象,但是返回結果都是數組,這就起到了將對象轉成數組的目的。從上面代碼可以看到,這個方法起作用的前提是,被處理的對象必須有length屬性,以及相對應的數字鍵。
(4)綁定回調函數的對象
上一節按鈕點擊事件的例子,可以改寫成
1 var o = new Object(); 2 3 o.f = function () { 4 console.log(this === o); 5 } 6 7 var f = function (){ 8 o.f.apply(o); 9 // 或者 o.f.call(o); 10 }; 11 12 $('#button').on('click', f);
點擊按鈕以后,控制台將會顯示true
。由於apply
方法(或者call
方法)不僅綁定函數執行時所在的對象,還會立即執行函數,因此不得不把綁定語句寫在一個函數體內。更簡潔的寫法是采用下面介紹的bind
方法。
function.prototype.bind()
bind
方法用於將函數體內的this
綁定到某個對象,然后返回一個新函數。
1 var d = new Date(); 2 d.getTime() // 1481869925657 3 4 var print = d.getTime; 5 print() // Uncaught TypeError: this is not a Date object.
上面代碼中,我們將d.getTime
方法賦給變量print
,然后調用print
就報錯了。這是因為getTime
方法內部的this
,綁定Date
對象的實例,賦給變量print
以后,內部的this
已經不指向Date
對象的實例了。
bind
方法可以解決這個問題,讓log
方法綁定console
對象。
1 var print = d.getTime.bind(d); 2 print() // 1481869925657
上面代碼中,bind
方法將getTime
方法內部的this
綁定到d
對象,這時就可以安全地將這個方法賦值給其他變量了。
下面是一個更清晰的例子。
1 var counter = { 2 count: 0, 3 inc: function () { 4 this.count++; 5 } 6 }; 7 8 counter.count // 0 9 counter.inc() 10 counter.count // 1
上面代碼中,counter.inc
內部的this
,默認指向counter
對象。如果將這個方法賦值給另一個變量,就會出錯。
1 var counter = { 2 count: 0, 3 inc: function () { 4 this.count++; 5 } 6 }; 7 8 var func = counter.inc; 9 func(); 10 counter.count // 0 11 count // NaN
上面代碼中,函數func
是在全局環境中運行的,這時inc
內部的this
指向頂層對象window
,所以counter.count
是不會變的,反而創建了一個全局變量count
。因為window.count
原來等於undefined
,進行遞增運算后undefined++
就等於NaN
。
為了解決這個問題,可以使用this
方法,將inc
內部的this
綁定到counter
對象。
1 var func = counter.inc.bind(counter); 2 func(); 3 counter.count // 1
上面代碼中,bind
方法將inc
方法綁定到counter
以后,再運行func
就會得到正確結果。
this
綁定到其他對象也是可以的。
1 var obj = { 2 count: 100 3 }; 4 var func = counter.inc.bind(obj); 5 func(); 6 obj.count // 101
上面代碼中,bind
方法將inc
方法內部的this
,綁定到obj
對象。結果調用func
函數以后,遞增的就是obj
內部的count
屬性。
bind
比call
方法和apply
方法更進一步的是,除了綁定this
以外,還可以綁定原函數的參數。
1 var add = function (x, y) { 2 return x * this.m + y * this.n; 3 } 4 5 var obj = { 6 m: 2, 7 n: 2 8 }; 9 10 var newAdd = add.bind(obj, 5); 11 12 newAdd(5) 13 // 20
上面代碼中,bind
方法除了綁定this
對象,還將add
函數的第一個參數x
綁定成5
,然后返回一個新函數newAdd
,這個函數只要再接受一個參數y
就能運行了。
如果bind
方法的第一個參數是null
或undefined
,等於將this
綁定到全局對象,函數運行時this
指向頂層對象(在瀏覽器中為window
)。
1 function add(x, y) { 2 return x + y; 3 } 4 5 var plus5 = add.bind(null, 5); 6 plus5(10) // 15
上面代碼中,函數add
內部並沒有this
,使用bind
方法的主要目的是綁定參數x
,以后每次運行新函數plus5
,就只需要提供另一個參數y
就夠了。而且因為add
內部沒有this
,所以bind
的第一個參數是null
,不過這里如果是其他對象,也沒有影響。
對於那些不支持bind
方法的老式瀏覽器,可以自行定義bind
方法。
1 if(!('bind' in Function.prototype)){ 2 Function.prototype.bind = function(){ 3 var fn = this; 4 var context = arguments[0]; 5 var args = Array.prototype.slice.call(arguments, 1); 6 return function(){ 7 return fn.apply(context, args); 8 } 9 } 10 }
bind 方法有一些使用注意點。
(1)每一次返回一個新函數
bind 方法每運行一次,就返回一個新函數,這會產生一些問題。比如,監聽事件的時候,不能寫成下面這樣。
element.addEventListener('click', o.m.bind(o));
上面代碼中,click
事件綁定bind
方法生成的一個匿名函數。這樣會導致無法取消綁定,所以,下面的代碼是無效的。
element.removeEventListener('click', o.m.bind(o));
正確的方法是寫成下面這樣:
1 var listener = o.m.bind(o); 2 element.addEventListener('click', listener); 3 // ... 4 element.removeEventListener('click', listener);
(2)結合回調函數使用
回調函數是JavaScript最常用的模式之一,但是一個常見的錯誤是,將包含 this 的方法直接當作回調函數。
1 var counter = { 2 count: 0, 3 inc: function () { 4 'use strict'; 5 this.count++; 6 } 7 }; 8 9 function callIt(callback) { 10 callback(); 11 } 12 13 callIt(counter.inc) 14 // TypeError: Cannot read property 'count' of undefined
上面代碼中, counter.inc 方法被當作回調函數,傳入了callIt
,調用時其內部的 this 指向 callIt 運行時所在的對象,即頂層對象 window ,所以得不到預想結果。注意,上面的 counter.inc 方法內部使用了嚴格模式,在該模式下, this 指向頂層對象時會報錯,一般模式不會。
解決方法就是使用bind
方法,將 counter.inc 綁定 counter 。
1 callIt(counter.inc.bind(counter)); 2 counter.count // 1
還有一種情況比較隱蔽,就是某些數組方法可以接受一個函數當作參數。這些函數內部的 this 指向,很可能也會出錯。
1 var obj = { 2 name: '張三', 3 times: [1, 2, 3], 4 print: function () { 5 this.times.forEach(function (n) { 6 console.log(this.name); 7 }); 8 } 9 }; 10 11 obj.print() 12 // 沒有任何輸出
上面代碼中, obj.print 內部 this.times 的 this 是指向obj
的,這個沒有問題。但是, forEach 方法的回調函數內部的 this.name 卻是指向全局對象,導致沒有辦法取到值。稍微改動一下,就可以看得更清楚。
1 obj.print = function () { 2 this.times.forEach(function (n) { 3 console.log(this === window); 4 }); 5 }; 6 7 obj.print() 8 // true 9 // true 10 // true
解決這個問題,也是通過 bind 方法綁定 this 。
1 obj.print = function () { 2 this.times.forEach(function (n) { 3 console.log(this.name); 4 }.bind(this)); 5 }; 6 7 obj.print() 8 // 張三 9 // 張三 10 // 張三
(3)結合call
方法使用
利用 bind 方法,可以改寫一些JavaScript原生方法的使用形式,以數組的 slice 方法為例。
1 [1, 2, 3].slice(0, 1) 2 // [1] 3 4 // 等同於 5 6 Array.prototype.slice.call([1, 2, 3], 0, 1) 7 // [1]
上面的代碼中,數組的 slice 方法從 [1, 2, 3] 里面,按照指定位置和長度切分出另一個數組。這樣做的本質是在 [1, 2, 3] 上面調用 Array.prototype.slice 方法,因此可以用 call 方法表達這個過程,得到同樣的結果。
call 方法實質上是調用 Function.prototype.call 方法,因此上面的表達式可以用 bind 方法改寫。
1 var slice = Function.prototype.call.bind(Array.prototype.slice); 2 3 slice([1, 2, 3], 0, 1) // [1]
可以看到,利用 bind 方法,將 [1, 2, 3].slice(0, 1) 變成了 slice([1, 2, 3], 0, 1) 的形式。這種形式的改變還可以用於其他數組方法。
1 var push = Function.prototype.call.bind(Array.prototype.push); 2 var pop = Function.prototype.call.bind(Array.prototype.pop); 3 4 var a = [1 ,2 ,3]; 5 push(a, 4) 6 a // [1, 2, 3, 4] 7 8 pop(a) 9 a // [1, 2, 3]
如果再進一步,將 Function.prototype.call 方法綁定到 Function.prototype.bind 對象,就意味着 bind 的調用形式也可以被改寫。
1 function f() { 2 console.log(this.v); 3 } 4 5 var o = { v: 123 }; 6 7 var bind = Function.prototype.call.bind(Function.prototype.bind); 8 9 bind(f, o)() // 123
上面代碼表示,將 Function.prototype.call 方法綁定 Function.prototype.bind 以后, bind 方法的使用形式從 f.bind(o) ,變成了 bind(f, o) 。