JavaScript中this 詳解


涵義

 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引擎內部,objobj.foo儲存在兩個內存地址,簡稱為M1M2。只有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

數組的mapforeach方法,允許提供一個函數作為參數。這個函數內部不應該使用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提供了callapplybind這三個方法,來切換/固定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方法的參數,應該是一個對象。如果參數為空、nullundefined,則默認傳入全局對象。

 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方法沒有參數,或者參數為nullundefined,則等同於指向全局對象。

如果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綁定當前環境(對象),並且參數為12,因此函數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

上面代碼中,hasOwnPropertyobj對象繼承的方法,如果這個方法一旦被覆蓋,就不會得到正確結果。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屬性。

bindcall方法和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方法的第一個參數是nullundefined,等於將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) 。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM