(注1:如果有問題歡迎留言探討,一起學(xué)習(xí)!轉(zhuǎn)載請注明出處,喜歡可以點(diǎn)個(gè)贊哦!)
(注2:更多內(nèi)容請查看我的目錄。)
1. 簡介
老樣子,我們列一下執(zhí)行上下文的三大屬性:
- 變量對象(Variable object,VO)
- 作用域鏈(Scope chain)
- this
this是一個(gè)非常容易讓人混淆的概念。首先我們思考一下JS中為什么會有this。
2. this的作用
看一下如下代碼:
function identify() {
return this.name.toUpperCase();
}
function speak() {
var greeting = "Hello, I'm " + identify.call(this);
console.log(greeting);
}
var me = {
name: "Kyle"
};
var you = {
name: "Reader"
};
identify.call(me); // KYLE
identify.call(you); // READER
speak.call(me); // Hello, 我是 KYLE
speak.call( you ); // Hello, 我是 READER
這段代碼可以在不同的上下文對象(me 和 you)中重復(fù)使用函數(shù) identify() 和 speak(), 不用針對每個(gè)對象編寫不同版本的函數(shù)。
如果不使用 this,那就需要給 identify() 和 speak() 顯式傳入一個(gè)上下文對象。如下所示:
function identify(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greeting = "Hello, I'm " + identify(context);
console.log(greeting);
}
identify(you); // READER
speak(me); //hello, 我是 KYLE
然而,this 提供了一種更優(yōu)雅的方式來隱式“傳遞”一個(gè)對象引用,因此可以將 API 設(shè)計(jì)得更加簡潔并且易于復(fù)用。隨著使用模式越來越復(fù)雜,顯式傳遞上下文對象會讓代碼變得越來越混亂,使用 this 則不會這樣。后面介紹對象和原型時(shí),你就會明白函數(shù)可以自動(dòng)引用合適的上下文對象有多重要。
3. this的兩種錯(cuò)誤解讀
this常見的錯(cuò)誤解讀有兩種,下面我們來仔細(xì)分析一下。
3.1 this指向自身
this,字面上的理解就是“這”,大家很容易將其解讀為指向這個(gè)函數(shù)自身。看一下如下代碼:
function foo(num) {
console.log( "foo: " + num );
// 記錄 foo 被調(diào)用的次數(shù)
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo被調(diào)用了多少次?
console.log( foo.count ); // 0
console.log 語句產(chǎn)生了 4 條輸出,證明 foo(..) 確實(shí)被調(diào)用了 4 次,但是 foo.count 仍然是 0。顯然從字面意思來理解 this 是錯(cuò)誤的。其實(shí),此處this.count++創(chuàng)建了一個(gè)全局變量count。執(zhí)行count++以后count變量成了NaN。如果不相信,大家可以在最后一行嘗試輸出window.count。
這個(gè)例子說明this并不能單純理解為指向這個(gè)函數(shù)本身。不過,既然this不是指向函數(shù)本身,我們在函數(shù)內(nèi)部如何引用函數(shù)本身呢?主要有以下三個(gè)方法:
- 具名引用
例如:
function foo() {
foo.count = 4; // foo指向它自身
}
該方法只能用于具名函數(shù)中。
- arguments. callee
例如:
setTimeout( function(){
arguments.callee.count = 4; // 匿名(沒有名字的)函數(shù)無法指向自身
}, 10 );
但是這種寫法已被廢棄,不建議使用。
- this
剛才不是說this不是指向函數(shù)本身么?可是現(xiàn)在為什又說可以呢?別急,等看完這篇文章,你自然會有答案。
3.2 this指向其作用域
這是this最使人混淆的地方。需要明確的是,this 在任何情況下都不指向函數(shù)的詞法作用域。在 JavaScript 內(nèi)部,作用域確實(shí)和對象類似,可見的標(biāo)識符都是它的屬性。但是作用域“對象”無法通過 JavaScript 代碼訪問,它存在于 JavaScript 引擎內(nèi)部。同時(shí),this與作用域鏈也不相關(guān)。
看下面的代碼:
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log( this.a );
}
foo(); // undefined
這段代碼本意是想,foo在全局定義,那么this就指向全局,this.bar就可以調(diào)用全局中定義的bar,而bar執(zhí)行的時(shí)候呢正好是在foo的執(zhí)行上下文,所以this指向foo。其實(shí)這里對this的兩處解讀都是錯(cuò)誤的。首先,this.bar()能夠運(yùn)行完全是一種偶然,怎樣的偶然呢?你使用的是非嚴(yán)格模式,你是在瀏覽器環(huán)境運(yùn)行而不是在node運(yùn)行,你是獨(dú)立調(diào)用的foo而正好bar在全局聲明。是不是很巧合呢?是不是有點(diǎn)迷糊,不要緊,繼續(xù)往下看。第二點(diǎn),代碼視圖在bar里面打印foo的變量,這里是完全錯(cuò)誤的,因?yàn)閎ar在運(yùn)行時(shí),this也是指向了全局(非嚴(yán)格模式,下面我們的討論都是基于運(yùn)行于瀏覽器的非嚴(yán)格模式)。
3.3 this的真正解讀
this 是在運(yùn)行時(shí)進(jìn)行綁定的,并不是在編寫時(shí)綁定,它的上下文取決于函數(shù)調(diào) 用時(shí)的各種條件。this 的綁定和函數(shù)聲明的位置沒有任何關(guān)系,只取決于函數(shù)的調(diào)用方式。
4. 綁定規(guī)則與函數(shù)調(diào)用方式
this在運(yùn)行時(shí)進(jìn)行綁定,綁定主要有四種規(guī)則,取決于綁定時(shí)候函數(shù)的調(diào)用方式。
4.1 默認(rèn)綁定與獨(dú)立調(diào)用(函數(shù)調(diào)用模式)
獨(dú)立調(diào)用是指函數(shù)作為一個(gè)普通函數(shù)來調(diào)用。當(dāng)一個(gè)函數(shù)并非一個(gè)對象的屬性時(shí),那么它就是被當(dāng)做一個(gè)函數(shù)來調(diào)用的。對于普通的函數(shù)調(diào)用來說,函數(shù)的返回值就是調(diào)用表達(dá)式的值。
使用函數(shù)調(diào)用模式調(diào)用函數(shù)時(shí),非嚴(yán)格模式下,this被綁定到全局對象;在嚴(yán)格模式下,this是undefined。
以下是四種常見的獨(dú)立調(diào)用場景。
- 普通獨(dú)立調(diào)用
function foo(){
console.log(this === window);
}
foo(); //true
- 被嵌套的函數(shù)獨(dú)立調(diào)用
//雖然test()函數(shù)被嵌套在obj.foo()函數(shù)中,但test()函數(shù)是獨(dú)立調(diào)用,而不是方法調(diào)用。所以this默認(rèn)綁定到window
var a = 0;
var obj = {
a : 2,
foo:function(){
function test(){
console.log(this.a);
}
test();
}
}
obj.foo();//0
3.IIFE(立即執(zhí)行函數(shù))
var a = 0;
function foo(){
(function test(){
console.log(this.a);
})()
};
var obj = {
a : 2,
foo:foo
}
obj.foo();//0
其實(shí)立即執(zhí)行函數(shù)可以理解為立即賦值并獨(dú)立調(diào)用。上面代碼其實(shí)和下面效果一樣:
var a = 0;
function foo(){
var temp = (function test(){
console.log(this.a);
});
temp(); // 獨(dú)立調(diào)用
};
var obj = {
a : 2,
foo:foo
}
obj.foo();//0
- 閉包
var a = 0;
function foo(){
function test(){
console.log(this.a);
}
return test;
};
var obj = {
a : 2,
foo:foo
}
obj.foo()();//0
4.2. 隱式綁定和方法調(diào)用(方法調(diào)用模式)
當(dāng)一個(gè)函數(shù)被保存為對象的一個(gè)屬性時(shí),我們稱它為一個(gè)方法。當(dāng)一個(gè)方法被直接對象所調(diào)用時(shí),this會被隱式綁定到該對象。如果調(diào)用表達(dá)式包含一個(gè)提取屬性的動(dòng)作,那么它就是被當(dāng)做一個(gè)方法來調(diào)用。要記住,對象屬性引用鏈中只有最頂層或者說最后一層會影響調(diào)用位置。
function foo(){
console.log(this.a);
};
var obj1 = {
a:1,
foo:foo,
obj2:{
a:2,
foo:foo
}
}
//foo()函數(shù)的直接對象是obj1,this隱式綁定到obj1
obj1.foo();//1
//foo()函數(shù)的直接對象是obj2,this隱式綁定到obj2
obj1.obj2.foo();//2
對于隱式綁定,是最容易出現(xiàn)錯(cuò)誤的地方,也是面試出陷阱題最多的地方。因?yàn)楹苋菀壮霈F(xiàn)所謂的隱式丟失。隱式丟失是指被隱式綁定的函數(shù)丟失綁定對象,從而默認(rèn)綁定到window。我們來看一下哪些情況會出現(xiàn)隱式丟失。
- 函數(shù)別名
var a = 0;
function foo(){
console.log(this.a);
};
var obj = {
a : 2,
foo:foo
}
//把obj.foo賦予別名bar,造成了隱式丟失,因?yàn)橹皇前裦oo()函數(shù)賦給了bar,而bar與obj對象則毫無關(guān)系
var bar = obj.foo;
bar();//0
等價(jià)于
var a = 0;
var bar = function foo(){
console.log(this.a);
}
bar();//0
其實(shí),要理解一點(diǎn)。就是函數(shù)在進(jìn)入函數(shù)執(zhí)行上下文時(shí)才會進(jìn)行this綁定,也就是這個(gè)函數(shù)調(diào)用以后才會進(jìn)行this綁定。而此處僅僅是做了引用賦值,然后進(jìn)行了bar的獨(dú)立調(diào)用。后面出現(xiàn)的所謂隱式丟失,其實(shí)都可以用這個(gè)道理去解釋。
- 參數(shù)傳遞
var a = 0;
function foo(){
console.log(this.a);
};
function bar(fn){
fn();
}
var obj = {
a : 2,
foo:foo
}
//把obj.foo當(dāng)作參數(shù)傳遞給bar函數(shù)時(shí),有隱式的函數(shù)賦值fn=obj.foo。與上例類似,只是把foo函數(shù)賦給了fn,而fn與obj對象則毫無關(guān)系
bar(obj.foo);//0
等價(jià)于
//等價(jià)于
var a = 0;
function bar(fn){
fn();
}
bar(function foo(){
console.log(this.a);
});
- 內(nèi)置函數(shù)
var a = 0;
function foo(){
console.log(this.a);
};
var obj = {
a : 2,
foo:foo
}
setTimeout(obj.foo,100);//0
等價(jià)于
var a = 0;
setTimeout(function foo(){
console.log(this.a);
},100);//0
- 間接引用
function foo() {
console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
//將o.foo函數(shù)賦值給p.foo函數(shù),然后立即執(zhí)行。相當(dāng)于僅僅是foo()函數(shù)的立即執(zhí)行
(p.foo = o.foo)(); // 2
不等價(jià)于
function foo() {
console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
//將o.foo函數(shù)賦值給p.foo函數(shù),之后p.foo函數(shù)再執(zhí)行,是屬于p對象的foo函數(shù)的執(zhí)行
p.foo = o.foo;
p.foo();//4
- 其他情況
在javascript引擎內(nèi)部,obj和obj.foo儲存在兩個(gè)內(nèi)存地址,簡稱為M1和M2。只有obj.foo()這樣調(diào)用時(shí),是從M1調(diào)用M2,因此this指向obj。但是,下面三種情況,都是直接取出M2進(jìn)行運(yùn)算,然后就在全局環(huán)境執(zhí)行運(yùn)算結(jié)果(還是M2),因此this指向全局環(huán)境。
var a = 0;
var obj = {
a : 2,
foo:foo
};
function foo() {
console.log( this.a );
};
(obj.foo = obj.foo)(); // 0
(false || obj.foo)(); // 0
(1, obj.foo)(); // 0
// 直接加括號并不會有造成隱式丟失
(obj.foo)(); // 2
總結(jié):其實(shí),隱式綁定只有在直接進(jìn)行對象方法調(diào)用時(shí)才會出現(xiàn),就是讀取到屬性方法以后直接在后面加括號調(diào)用,如下:
obj.foo();
如果在調(diào)用前經(jīng)過了任何運(yùn)算,比如“=”,“,”“||”等運(yùn)算(注意"()"并不是運(yùn)算符),其實(shí)都是執(zhí)行了一個(gè)隱式的賦值引用,然后對被隱式賦值的函數(shù)進(jìn)行了直接調(diào)用,如下:
(obj2.foo = obj.foo)();
(obj.foo = obj.foo)();
(false || obj.foo)();
(1, obj.foo)();
...
等價(jià)于
var temp = (obj2.foo = obj.foo);temp();
var temp = (obj.foo = obj.foo);temp();
var temp = (false || obj.foo);temp();
var temp = (1, obj.foo);temp();
...
4.3 顯式綁定與間接調(diào)用(間接調(diào)用模式)
在分析隱式綁定時(shí),我們必須在一個(gè)對象內(nèi)部包含一個(gè)指向函數(shù)的屬性,并通過這個(gè)屬性間接引用函數(shù),從而把 this 間接(隱式)綁定到這個(gè)對象上。 那么如果我們不想在對象內(nèi)部包含函數(shù)引用,而想在某個(gè)對象上強(qiáng)制調(diào)用函數(shù),該怎么做呢?
可以通過call()、apply()、bind()方法把對象綁定到this上,這種做法叫做顯式綁定。對于被調(diào)用的函數(shù)來說,叫做間接調(diào)用。
var a = 0;
function foo(){
console.log(this.a);
}
var obj = {
a:2
};
foo(); // 0
foo.call(obj); // 2
obj并沒有指向函數(shù)foo的屬性,但卻可以間接調(diào)用foo。這時(shí)候我們可以回答文章開頭提出的問題了。另一種指向自身的方式,使用this進(jìn)行顯示綁定。
function foo(){
console.log(this);
}
foo(); // Window
foo.call(foo); // f foo
不過,這種普通的顯式綁定無法解決前面提到的隱式丟失問題。以前面所舉的函數(shù)別名導(dǎo)致隱式丟失的代碼為例。
var a = 0;
function foo(){
console.log(this.a);
};
var obj = {
a : 2,
foo:foo
}
//把obj.foo賦予別名bar,造成了隱式丟失,因?yàn)橹皇前裦oo()函數(shù)賦給了bar,而bar與obj對象則毫無關(guān)系
var bar = obj.foo;
bar();//0
改成如下顯示綁定:
var a = 0;
function foo(){
console.log(this.a);
};
var obj = {
a : 2,
foo:foo
}
var obj2 = {
a : 3,
}
var bar = obj.foo;
bar();//0
bar.call(obj);//2
bar.call(obj2);//3
說明,call傳入的對象改變時(shí),隱式綁定的對象也發(fā)生了改變,this不再綁定foo的直接擁有者obj,發(fā)生了隱式丟失。那么如何防止這種隱式丟失呢?只要想辦法讓this始終指向其屬性擁有者即可。當(dāng)然我們也可以讓this指向任何事先設(shè)定的對象,做到一種強(qiáng)制的綁定,也就是所謂的硬綁定。
var a = 0;
function foo(){
console.log(this.a);
};
var obj = {
a : 2,
foo:foo
}
var obj2 = {
a : 3,
}
var bar = function () {
foo.call(obj);
};
bar();//2
bar.call(obj);//2
bar.call(obj2);//2
不管給call傳入什么,最后this實(shí)際綁定的對象都是預(yù)先指定的obj。
JavaScript中新增了許多內(nèi)置函數(shù),具有顯式綁定的功能,如數(shù)組的5個(gè)迭代方法:map()、forEach()、filter()、some()、every()
var id = 'window';
function foo(el){
console.log(el,this.id);
}
var obj = {
id: 'fn'
};
[1,2,3].forEach(foo); // 1 "window" 2 "window" 3 "window"
[1,2,3].forEach(foo,obj); // 1 "fn" 2 "fn" 3 "fn"
4.4 new綁定和構(gòu)造函數(shù)調(diào)用(構(gòu)造函數(shù)調(diào)用模式)
如果函數(shù)或者方法調(diào)用之前帶有關(guān)鍵字new,它就構(gòu)成構(gòu)造函數(shù)調(diào)用。對于this綁定來說,稱為new綁定。要注意以下幾點(diǎn):
- 構(gòu)造函數(shù)通常不使用return關(guān)鍵字,它們通常初始化新對象,當(dāng)構(gòu)造函數(shù)的函數(shù)體執(zhí)行完畢時(shí),它會顯式返回。在這種情況下,構(gòu)造函數(shù)調(diào)用表達(dá)式的計(jì)算結(jié)果就是這個(gè)新對象的值。
function fn(){
this.a = 2;
}
var test = new fn();
console.log(test); // {a:2}
- 如果構(gòu)造函數(shù)使用return語句但沒有指定返回值,或者返回一個(gè)原始值,那么這時(shí)將忽略返回值,同時(shí)使用這個(gè)新對象作為調(diào)用結(jié)果。
function fn(){
this.a = 2;
return 1;
}
var test = new fn();
console.log(test); // {a:2}
- 如果構(gòu)造函數(shù)顯式地使用return語句返回一個(gè)對象,那么調(diào)用表達(dá)式的值就是這個(gè)對象。
var obj = {a:1};
function fn(){
this.a = 2;
return obj;
}
var test = new fn();
console.log(test); // {a:1}
- 盡管有時(shí)候構(gòu)造函數(shù)看起來像一個(gè)方法調(diào)用,它依然會使用這個(gè)新對象作為this。也就是說,在表達(dá)式new o.m()中,this并不是o。
var o = {
m: function(){
this.a = 1;
return this;
}
}
var obj = new o.m();
console.log(obj, obj === o); // {a:1} , false
console.log(obj.a); // 1
console.log(o.a); // undefined
console.log(o.m.a); // undefined
console.log(obj.constructor === o.m); // true
5. this綁定優(yōu)先級
現(xiàn)在我們已經(jīng)了解了函數(shù)調(diào)用中 this 綁定的四條規(guī)則,你需要做的就是找到函數(shù)的調(diào)用位置并判斷應(yīng)當(dāng)應(yīng)用哪條規(guī)則。但是,如果某個(gè)調(diào)用位置可以應(yīng)用多條規(guī)則該怎么辦?為了 解決這個(gè)問題就必須給這些規(guī)則設(shè)定優(yōu)先級,這就是我們接下來要介紹的內(nèi)容。
毫無疑問,默認(rèn)綁定的優(yōu)先級是四條規(guī)則中最低的,所以我們可以先不考慮它。現(xiàn)在我們將其與另外三種規(guī)則互相比較。
5.1 顯式綁定 vs 隱式綁定
function foo() {
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
//在該語句中,顯式綁定call(obj2)和隱式綁定obj1.foo同時(shí)出現(xiàn),最終結(jié)果為3,說明被綁定到了obj2中
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2
可以看到,顯式綁定優(yōu)于隱式綁定。
5.2 new綁定 vs 隱式綁定
function foo(something) {
this.a = something;
}
var obj1 = {foo: foo};
var obj2 = {};
obj1.foo( 2 );
console.log( obj1.a ); // 2
obj1.foo.call(obj2,3);
console.log( obj2.a ); // 3
//在下列代碼中,隱式綁定obj1.foo和new綁定同時(shí)出現(xiàn)。最終obj1.a結(jié)果是2,而bar.a結(jié)果是4,說明this被綁定到new創(chuàng)建的新對象上
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4
可以看到,new綁定優(yōu)先于隱式綁定
5.3 new綁定 vs 顯式綁定
function foo(something) {
this.a = something;
}
var obj1 = {};
// 先將obj1綁定到foo函數(shù)中,此時(shí)this值為obj1
var bar = foo.bind( obj1 );
bar( 2 );
console.log(obj1.a); // 2
// 通過new綁定,此時(shí)this值為baz
var baz = new bar( 3 );
console.log( obj1.a ); // 2
// 說明使用new綁定時(shí),在bar函數(shù)內(nèi),無論this指向obj1有沒有生效,最終this都指向?qū)嵗齜az
console.log( baz.a ); // 3
6. 總結(jié)
關(guān)于this的綁定,可以按照如下順序判定:
- 是否是new綁定?如果是,this綁定的是新創(chuàng)建的實(shí)例對象
var bar = new foo(); // 綁定bar
- 是否是顯式綁定?如果是,this綁定的是指定的對象
var bar = foo.call(obj2); // 綁定obj2
- 是否是隱式綁定?如果是,this綁定的是調(diào)用的對象
var bar = obj1.foo(); // 綁定obj1
- 如果都不是,則使用默認(rèn)綁定
var bar = foo(); // 綁定到全局對象(非嚴(yán)格模式)或者undefined(嚴(yán)格模式)
參考
深入理解this機(jī)制系列第一篇——this的4種綁定規(guī)則
JavaScript深入之從ECMAScript規(guī)范解讀this
深入理解javascript函數(shù)系列第一篇——函數(shù)概述
深入理解this機(jī)制系列第二篇——this綁定優(yōu)先級
BOOK-《你不知道的JavaScript》 第2部分