一、涵義
this
關鍵字是一個非常重要的文法點。毫不誇張地說,不了解它的含義,大部分開發任務都無法完成。
this
可以用在構造函數之中,表示執行個體對象。除此之外,
this
還可以用在别的場合。但不管是什麼場合,
this
都有一個共同點:它總是傳回一個對象。簡單說,
this
就是屬性或方法“目前”所在的對象。
this.property
上面代碼中,
this
就代表
property
屬性目前所在的對象。下面是一個實際的例子。
var person = {
name: '張三',
describe: function () {
return '姓名:'+ this.name;
}
};
person.describe()
// "姓名:張三"
this.name
表示
name
屬性所在的那個對象。由于
this.name
是在
describe
方法中調用,而
describe
方法所在的目前對象是
person
,是以
this
指向
person
,
this.name
就是
person.name
。由于對象的屬性可以賦給另一個對象,是以屬性所在的目前對象是可變的,即
this
的指向是可變的。
var A = {
name: '張三',
describe: function () {
return '姓名:'+ this.name;
}
};
var B = {
name: '李四'
};
B.describe = A.describe;
B.describe()
// "姓名:李四"
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
的指向就會變。
var A = {
name: '張三',
describe: function () {
return '姓名:'+ this.name;
}
};
var name = '李四';
var f = A.describe;
f() // "姓名:李四
A.describe
被指派給變量
f
,内部的
this
就會指向
f
運作時所在的對象(本例是頂層對象)。再看一個網頁程式設計的例子。
<input type="text" name="age" size=3 onChange="validate(this, 18, 99);">
<script>
function validate(obj, lowval, hival){
if ((obj.value < lowval) || (obj.value > hival))
console.log('Invalid Value!');
}
</script>
上面代碼是一個文本輸入框,每當使用者輸入一個值,就會調用
onChange
回調函數,驗證這個值是否在指定範圍。浏覽器會向回調函數傳入目前對象,是以
this
就代表傳入目前對象(即文本框),然後就可以從
this.value
上面讀到使用者的輸入值。
總結一下,JavaScript 語言之中,一切皆對象,運作環境也是對象,是以函數都是在某個對象之中運作,
this
就是函數運作時所在的對象(環境)。這本來并不會讓使用者糊塗,但是 JavaScript 支援運作環境動态切換,也就是說,
this
的指向是動态的,沒有辦法事先确定到底指向哪個對象,這才是最讓初學者感到困惑的地方。
二、實質
JavaScript 語言之是以有 this 的設計,跟記憶體裡面的資料結構有關系。
var obj = { foo: 5 };
上面的代碼将一個對象指派給變量
obj
。JavaScript 引擎會先在記憶體裡面,生成一個對象
{ foo: 5 }
,然後把這個對象的記憶體位址指派給變量
obj
。也就是說,變量
obj
是一個位址(reference)。後面如果要讀取
obj.foo
,引擎先從
obj
拿到記憶體位址,然後再從該位址讀出原始的對象,傳回它的
foo
屬性。
原始的對象以字典結構儲存,每一個屬性名都對應一個屬性描述對象。舉例來說,上面例子的
foo
屬性,實際上是以下面的形式儲存的。
{
foo: {
[[value]]: 5
[[writable]]: true
[[enumerable]]: true
[[configurable]]: true
}
}
注意,
foo
屬性的值儲存在屬性描述對象的
value
屬性裡面。
這樣的結構是很清晰的,問題在于屬性的值可能是一個函數。
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 f = function () {
console.log(this.x);
}
上面代碼中,函數體裡面的
this.x
就是指目前運作環境的
x
。
var f = function () {
console.log(this.x);
}
var x = 1;
var obj = {
f: f,
x: 2,
};
// 單獨執行
f() // 1
// obj 環境執行
obj.f() // 2
f
在全局環境執行,
this.x
指向全局環境的
x
;在
obj
環境執行,
this.x
obj.x
三、使用場合
this
主要有以下幾個使用場合。
(1)全局環境
全局環境使用
this
,它指的就是頂層對象
window
this === window // true
function f() {
console.log(this === window);
}
f() // true
上面代碼說明,不管是不是在函數内部,隻要是在全局環境下運作,
this
就是指頂層對象
window
(2)構造函數
構造函數中的
this
,指的是執行個體對象。
var Obj = function (p) {
this.p = p;
};
上面代碼定義了一個構造函數
Obj
。由于
this
指向執行個體對象,是以在構造函數内部定義
this.p
,就相當于定義執行個體對象有一個
p
var o = new Obj('Hello World!');
o.p // "Hello World!"
(3)對象的方法
如果對象的方法裡面包含
this
this
的指向就是方法運作時所在的對象。該方法指派給另一個對象,就會改變
this
的指向。
但是,這條規則很不容易把握。請看下面的代碼。
var obj ={
foo: function () {
console.log(this);
}
};
obj.foo() // obj
obj.foo
方法執行時,它内部的
this
obj
但是,下面這幾種用法,都會改變
this
// 情況一
(obj.foo = obj.foo)() // window
// 情況二
(false || obj.foo)() // window
// 情況三
(1, obj.foo)() // window
obj.foo
就是一個值。這個值真正調用的時候,運作環境已經不是
obj
了,而是全局環境,是以
this
不再指向
obj
可以這樣了解,JavaScript 引擎内部,
obj
和
obj.foo
儲存在兩個記憶體位址,稱為位址一和位址二。
obj.foo()
這樣調用時,是從位址一調用位址二,是以位址二的運作環境是位址一,
this
obj
。但是,上面三種情況,都是直接取出位址二進行調用,這樣的話,運作環境就是全局環境,是以
this
指向全局環境。上面三種情況等同于下面的代碼。
// 情況一
(obj.foo = function () {
console.log(this);
})()
// 等同于
(function () {
console.log(this);
})()
// 情況二
(false || function () {
console.log(this);
})()
// 情況三
(1, function () {
console.log(this);
})()
如果
this
所在的方法不在對象的第一層,這時
this
隻是指向目前一層的對象,而不會繼承更上面的層。
var a = {
p: 'Hello',
b: {
m: function() {
console.log(this.p);
}
}
};
a.b.m() // undefined
a.b.m
方法在
a
對象的第二層,該方法内部的
this
不是指向
a
,而是指向
a.b
,因為實際執行的是下面的代碼。
var b = {
m: function() {
console.log(this.p);
}
};
var a = {
p: 'Hello',
b: b
};
(a.b).m() // 等同于 b.m()
如果要達到預期效果,隻有寫成下面這樣。
var a = {
b: {
m: function() {
console.log(this.p);
},
p: 'Hello'
}
};
如果這時将嵌套對象内部的方法指派給一個變量,
this
依然會指向全局對象。
var a = {
b: {
m: function() {
console.log(this.p);
},
p: 'Hello'
}
};
var hello = a.b.m;
hello() // undefined
m
是多層對象内部的一個方法。為求簡便,将其指派給
hello
變量,結果調用時,
this
指向了頂層對象。為了避免這個問題,可以隻将
m
所在的對象指派給
hello
,這樣調用時,
this
的指向就不會變。
var hello = a.b;
hello.m() // Hello
四、使用注意點
(一)、避免多層 this
由于
this
的指向是不确定的,是以切勿在函數中包含多層的
this
var o = {
f1: function () {
console.log(this);
var f2 = function () {
console.log(this);
}();
}
}
o.f1()
// Object
// Window
上面代碼包含兩層
this
,結果運作後,第一層指向對象
o
,第二層指向全局對象,因為實際執行的是下面的代碼。
var temp = function () {
console.log(this);
};
var o = {
f1: function () {
console.log(this);
var f2 = temp();
}
}
一個解決方法是在第二層改用一個指向外層
this
的變量。
var o = {
f1: function() {
console.log(this);
var that = this;
var f2 = function() {
console.log(that);
}();
}
}
o.f1()
// Object
// Object
上面代碼定義了變量
that
,固定指向外層的
this
,然後在内層使用
that
,就不會發生
this
指向的改變。事實上,使用一個變量固定
this
的值,然後内層函數調用這個變量,是非常常見的做法,請務必掌握。JavaScript 提供了嚴格模式,也可以硬性避免這種問題。嚴格模式下,如果函數内部的
this
指向頂層對象,就會報錯。
var counter = {
count: 0
};
counter.inc = function () {
'use strict';
this.count++
};
var f = counter.inc;
f()
// TypeError: Cannot read property 'count' of undefined
inc
方法通過
'use strict'
聲明采用嚴格模式,這時内部的
this
一旦指向頂層對象,就會報錯。
(二)、避免數組處理方法中的 this
數組的
map
foreach
方法,允許提供一個函數作為參數。這個函數内部不應該使用
this
var o = {
v: 'hello',
p: [ 'a1', 'a2' ],
f: function f() {
this.p.forEach(function (item) {
console.log(this.v + ' ' + item);
});
}
}
o.f()
// undefined a1
// undefined a2
foreach
方法的回調函數中的
this
,其實是指向
window
對象,是以取不到
o.v
的值。原因跟上一段的多層
this
是一樣的,就是内層的
this
不指向外部,而指向頂層對象。
解決這個問題的一種方法,就是前面提到的,使用中間變量固定
this
var o = {
v: 'hello',
p: [ 'a1', 'a2' ],
f: function f() {
var that = this;
this.p.forEach(function (item) {
console.log(that.v+' '+item);
});
}
}
o.f()
// hello a1
// hello a2
另一種方法是将
this
當作
foreach
方法的第二個參數,固定它的運作環境。
var o = {
v: 'hello',
p: [ 'a1', 'a2' ],
f: function f() {
this.p.forEach(function (item) {
console.log(this.v + ' ' + item);
}, this);
}
}
o.f()
// hello a1
// hello a2
(三)、避免回調函數中的 this
回調函數中的
this
往往會改變指向,最好避免使用。
var o = new Object();
o.f = function () {
console.log(this === o);
}
// jQuery 的寫法
$('#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
的指向(即函數執行時所在的作用域),然後在所指定的作用域中,調用該函數。
var obj = {};
var f = function () {
return this;
};
f() === window // true
f.call(obj) === obj // true
上面代碼中,全局環境運作函數
f
時,
this
指向全局環境(浏覽器為
window
對象);
call
方法可以改變
this
的指向,指定
this
指向對象
obj
,然後在對象
obj
的作用域中運作函數
f
call
方法的參數,應該是一個對象。如果參數為空、
null
undefined
,則預設傳入全局對象。
var n = 123;
var obj = { n: 456 };
function a() {
console.log(this.n);
}
a.call() // 123
a.call(null) // 123
a.call(undefined) // 123
a.call(window) // 123
a.call(obj) // 456
a
函數中的
this
關鍵字,如果指向全局對象,傳回結果為
123
。如果使用
call
方法将
this
關鍵字指向
obj
對象,傳回結果為
456
。可以看到,如果
call
方法沒有參數,或者參數為
null
或
undefined
,則等同于指向全局對象。
call
方法的參數是一個原始值,那麼這個原始值會自動轉成對應的包裝對象,然後傳入
call
方法。
var f = function () {
return this;
};
f.call(5)
// Number {[[PrimitiveValue]]: 5}
call
的參數為
5
,不是對象,會被自動轉成包裝對象(
Number
的執行個體),綁定
f
内部的
this
call
方法還可以接受多個參數。
func.call(thisValue, arg1, arg2, ...)
call
的第一個參數就是
this
所要指向的那個對象,後面的參數則是函數調用時所需的參數。
function add(a, b) {
return a + b;
}
add.call(this, 1, 2) // 3
call
方法指定函數
add
this
綁定目前環境(對象),并且參數為
1
2
,是以函數
add
運作後得到
3
call
方法的一個應用是調用對象的原生方法。
var obj = {};
obj.hasOwnProperty('toString') // false
// 覆寫掉繼承的 hasOwnProperty 方法
obj.hasOwnProperty = function () {
return true;
};
obj.hasOwnProperty('toString') // true
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
方法中,必須以數組形式添加。
function f(x, y){
console.log(x + y);
}
f.call(null, 1, 1) // 2
f.apply(null, [1, 1]) // 2
f
函數本來接受兩個參數,使用
apply
方法以後,就變成可以接受一個數組作為參數。
利用這一點,可以做一些有趣的應用。
(1)找出數組最大元素
JavaScript 不提供找出數組最大元素的函數。結合使用
apply
方法和
Math.max
方法,就可以傳回數組的最大元素。
var a = [10, 2, 4, 15, 9];
Math.max.apply(null, a) // 15
(2)将數組的空元素變為
undefined
通過
apply
方法,利用
Array
構造函數将數組的空元素變成
undefined
Array.apply(null, ['a', ,'b'])
// [ 'a', undefined, 'b' ]
空元素與
undefined
的差别在于,數組的
forEach
方法會跳過空元素,但是不會跳過
undefined
。是以,周遊内部元素的時候,會得到不同的結果。
var a = ['a', , 'b'];
function print(i) {
console.log(i);
}
a.forEach(print)
// a
// b
Array.apply(null, a).forEach(print)
// a
// undefined
// b
(3)轉換類似數組的對象
另外,利用數組對象的
slice
方法,可以将一個類似數組的對象(比如
arguments
對象)轉為真正的數組。
Array.prototype.slice.apply({0: 1, length: 1}) // [1]
Array.prototype.slice.apply({0: 1}) // []
Array.prototype.slice.apply({0: 1, length: 2}) // [1, undefined]
Array.prototype.slice.apply({length: 1}) // [undefined]
上面代碼的
apply
方法的參數都是對象,但是傳回結果都是數組,這就起到了将對象轉成數組的目的。從上面代碼可以看到,這個方法起作用的前提是,被處理的對象必須有
length
屬性,以及相對應的數字鍵。
(4)綁定回調函數的對象
前面的按鈕點選事件的例子,可以改寫如下。
var o = new Object();
o.f = function () {
console.log(this === o);
}
var f = function (){
o.f.apply(o);
// 或者 o.f.call(o);
};
// jQuery 的寫法
$('#button').on('click', f);
上面代碼中,點選按鈕以後,控制台将會顯示
true
apply
方法(或者
call
方法)不僅綁定函數執行時所在的對象,還會立即執行函數,是以不得不把綁定語句寫在一個函數體内。更簡潔的寫法是采用下面介紹的
bind
(三)、Function.prototype.bind()
bind
方法用于将函數體内的
this
綁定到某個對象,然後傳回一個新函數。
var d = new Date();
d.getTime() // 1481869925657
var print = d.getTime;
print() // Uncaught TypeError: this is not a Date object.
上面代碼中,我們将
d.getTime
方法賦給變量
print
,然後調用
print
就報錯了。這是因為
getTime
方法内部的
this
,綁定
Date
對象的執行個體,賦給變量
print
以後,内部的
this
已經不指向
Date
對象的執行個體了。
bind
方法可以解決這個問題。
var print = d.getTime.bind(d);
print() // 1481869925657
bind
getTime
this
綁定到
d
對象,這時就可以安全地将這個方法指派給其他變量了。
bind
方法的參數就是所要綁定
this
的對象,下面是一個更清晰的例子。
var counter = {
count: 0,
inc: function () {
this.count++;
}
};
var func = counter.inc.bind(counter);
func();
counter.count // 1
counter.inc
方法被指派給變量
func
。這時必須用
bind
inc
this
,綁定到
counter
,否則就會出錯。
this
綁定到其他對象也是可以的。
var counter = {
count: 0,
inc: function () {
this.count++;
}
};
var obj = {
count: 100
};
var func = counter.inc.bind(obj);
func();
obj.count // 101
bind
inc
this
obj
對象。結果調用
func
函數以後,遞增的就是
obj
count
bind
還可以接受更多的參數,将這些參數綁定原函數的參數。
var add = function (x, y) {
return x * this.m + y * this.n;
}
var obj = {
m: 2,
n: 2
};
var newAdd = add.bind(obj, 5);
newAdd(5) // 20
bind
方法除了綁定
this
對象,還将
add
函數的第一個參數
x
綁定成
5
,然後傳回一個新函數
newAdd
,這個函數隻要再接受一個參數
y
就能運作了。
bind
方法的第一個參數是
null
undefined
,等于将
this
綁定到全局對象,函數運作時
this
指向頂層對象(浏覽器為
window
)。
function add(x, y) {
return x + y;
}
var plus5 = add.bind(null, 5);
plus5(10) // 15
add
内部并沒有
this
,使用
bind
方法的主要目的是綁定參數
x
,以後每次運作新函數
plus5
,就隻需要提供另一個參數
y
就夠了。而且因為
add
内部沒有
this
bind
的第一個參數是
null
,不過這裡如果是其他對象,也沒有影響。
bind
方法有一些使用注意點。
(1)每一次傳回一個新函數
bind
方法每運作一次,就傳回一個新函數,這會産生一些問題。比如,監聽事件的時候,不能寫成下面這樣。
element.addEventListener('click', o.m.bind(o));
click
事件綁定
bind
方法生成的一個匿名函數。這樣會導緻無法取消綁定,是以,下面的代碼是無效的。
element.removeEventListener('click', o.m.bind(o));
正确的方法是寫成下面這樣:
var listener = o.m.bind(o);
element.addEventListener('click', listener);
// ...
element.removeEventListener('click', listener);
(2)結合回調函數使用
回調函數是 JavaScript 最常用的模式之一,但是一個常見的錯誤是,将包含
this
的方法直接當作回調函數。解決方法就是使用
bind
方法,将
counter.inc
綁定
counter
var counter = {
count: 0,
inc: function () {
'use strict';
this.count++;
}
};
function callIt(callback) {
callback();
}
callIt(counter.inc.bind(counter));
counter.count // 1
callIt
方法會調用回調函數。這時如果直接把
counter.inc
傳入,調用時
counter.inc
this
就會指向全局對象。使用
bind
counter.inc
counter
以後,就不會有這個問題,
this
總是指向
counter
還有一種情況比較隐蔽,就是某些數組方法可以接受一個函數當作參數。這些函數内部的
this
指向,很可能也會出錯。
var obj = {
name: '張三',
times: [1, 2, 3],
print: function () {
this.times.forEach(function (n) {
console.log(this.name);
});
}
};
obj.print()
// 沒有任何輸出
obj.print
内部
this.times
的
this
是指向
obj
的,這個沒有問題。但是,
forEach
方法的回調函數内部的
this.name
卻是指向全局對象,導緻沒有辦法取到值。稍微改動一下,就可以看得更清楚。
obj.print = function () {
this.times.forEach(function (n) {
console.log(this === window);
});
};
obj.print()
// true
// true
// true
解決這個問題,也是通過
bind
方法綁定
this
obj.print = function () {
this.times.forEach(function (n) {
console.log(this.name);
}.bind(this));
};
obj.print()
// 張三
// 張三
// 張三
(3)結合
call
方法使用
利用
bind
方法,可以改寫一些 JavaScript 原生方法的使用形式,以數組的
slice
方法為例。
[1, 2, 3].slice(0, 1) // [1]
// 等同于
Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]
上面的代碼中,數組的
slice
方法從
[1, 2, 3]
裡面,按照指定位置和長度切分出另一個數組。這樣做的本質是在
[1, 2, 3]
上面調用
Array.prototype.slice
方法,是以可以用
call
方法表達這個過程,得到同樣的結果。
call
方法實質上是調用
Function.prototype.call
方法,是以上面的表達式可以用
bind
方法改寫。
var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0, 1) // [1]
上面代碼的含義就是,将
Array.prototype.slice
變成
Function.prototype.call
方法所在的對象,調用時就變成了
Array.prototype.slice.call
。類似的寫法還可以用于其他數組方法。
var push = Function.prototype.call.bind(Array.prototype.push);
var pop = Function.prototype.call.bind(Array.prototype.pop);
var a = [1 ,2 ,3];
push(a, 4)
a // [1, 2, 3, 4]
pop(a)
a // [1, 2, 3]
如果再進一步,将
Function.prototype.call
方法綁定到
Function.prototype.bind
對象,就意味着
bind
的調用形式也可以被改寫。
function f() {
console.log(this.v);
}
var o = { v: 123 };
var bind = Function.prototype.call.bind(Function.prototype.bind);
bind(f, o)() // 123
Function.prototype.bind
方法綁定在
Function.prototype.call
上面,是以
bind
方法就可以直接使用,不需要在函數執行個體上使用。
參考資料:https://wangdoc.com/javascript/oop/this.html