前言
this 之謎揭底:從淺入深理解 JavaScript 中的 this 關(guān)鍵字(二)
調(diào)用位置
- 在理解 this 的綁定過程之前,首先要理解
調(diào)用位置:調(diào)用位置就是函數(shù)在代碼中被調(diào)用的位置(而不是聲明的位置)。
- 通常來說,尋找調(diào)用位置就是尋找"函數(shù)被調(diào)用的位置", 最重要的要分析調(diào)用棧(就是為了到達(dá)當(dāng)前執(zhí)行位置所調(diào)用的所有函數(shù))。運(yùn)行代碼時(shí),調(diào)用器會(huì)在那個(gè)位置暫停,同時(shí)會(huì)在展示當(dāng)前位置的函數(shù)調(diào)用列表,這就是調(diào)用棧。
綁定規(guī)則
- 函數(shù)的調(diào)用位置決定了 this 的綁定對(duì)象,通常情況下分為以下幾種規(guī)則:
默認(rèn)綁定
- 最常用的函數(shù)調(diào)用類型:
獨(dú)立函數(shù)調(diào)用??砂堰@條規(guī)則看到是無法應(yīng)用其他規(guī)則時(shí)的默認(rèn)規(guī)則。
function foo(){
console.log(this.a);
}
var a = 2;
foo(); // 2
- 當(dāng)調(diào)用 foo() 時(shí),this.a 被解析成了全局變量 a。為什么?
- 因?yàn)樵谏鲜龃a中,函數(shù)調(diào)用時(shí)應(yīng)用了this 的默認(rèn)綁定,因此 this 指向全局對(duì)象。(要理解 this,就要先理解調(diào)用位置)
如果使用嚴(yán)格模式(strict mode),那全局對(duì)象將無法使用默認(rèn)綁定,因此 this 會(huì)綁定到 undefined。
function foo(){
"use strict";
console.log(this.a);
}
var a = 2;
foo(); // Type: this is undefined
- 雖然 this 的綁定規(guī)則完全取決于調(diào)用位置,但是
只有 foo() 運(yùn)行在非 strict mode下時(shí),默認(rèn)綁定才能綁定到全局對(duì)象; 嚴(yán)格模式下與 foo() 的調(diào)用位置無關(guān)。
function foo(){
console.log(this.a);
}
var a = 2;
(function (){
"use strict";
foo(); // 2
})
- 通常情況下,盡量減少在代碼中混合使用
strict mode 與 non-strict mode,盡量減少在代碼中混合使用 strict mode 和 non-strict mode。
隱式綁定
另一條規(guī)則是調(diào)用位置是否有上下文對(duì)象,或者說是否被某個(gè)對(duì)象擁有或包裹。
- 考慮以下代碼:
function foo() {
console.log(this.a); // 2
}
var obj = {
a: 2,
foo: foo
}
obj.foo();
- 上述代碼中,調(diào)用位置使用 obj 的上下文來引用函數(shù),可以說函數(shù)被調(diào)用時(shí) obj 對(duì)象擁有或包含它。
-
當(dāng)函數(shù)引用有上下文對(duì)象時(shí),隱式綁定規(guī)則會(huì)把函數(shù)調(diào)用中的 this 綁定到這個(gè)上下文對(duì)象上,因此在調(diào)用 foo() 時(shí) this 被綁定到了 obj 上,所以 this.a 與 obj.a 是一樣的。
- 注意:
對(duì)象屬性引用鏈中只有最頂層或最后一層會(huì)影響調(diào)用位置。
- 如下代碼:
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
- 隱式丟失:在被隱式綁定的函數(shù)會(huì)丟失綁定對(duì)象,也就是說它會(huì)默認(rèn)綁定,從而把 this 綁定到全局對(duì)象或 undefined 上,這取決于是否是嚴(yán)格模式。
- 如下代碼:
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函數(shù)別名!
var a = "oops, global"; // a 是全局對(duì)象的屬性
bar(); // "oops, global"
- 還有一種奇怪的方式,就是在傳入回調(diào)函數(shù)時(shí)隱式丟失
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// fn其實(shí)引用的是 foo
fn(); // <-- 調(diào)用位置!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局對(duì)象的屬性
doFoo( obj.foo ); // "oops, global"
- 在我們傳入函數(shù)時(shí)也會(huì)被隱式賦值。
- 那如果傳入的函數(shù)不是自定義的函數(shù),而是語言內(nèi)置的函數(shù)呢?結(jié)果還是一樣的,沒有區(qū)別
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局對(duì)象的屬性
setTimeout( obj.foo, 100 ); // "oops, global"
顯示綁定
- 那我們不想在對(duì)象內(nèi)部包含函數(shù)引用,而是想在某個(gè)對(duì)象上強(qiáng)制調(diào)用函數(shù),該如何操作?
- 那就必須要使用
call() 和 apply()。第一個(gè)參數(shù)是一個(gè)對(duì)象,也就是需要綁定的對(duì)象,第二個(gè)參數(shù)傳入的參數(shù),而兩者之間的區(qū)別就在于第二個(gè)參數(shù),call 的第二個(gè)參數(shù)是一個(gè)個(gè)參數(shù),而 apply 則是一個(gè)參數(shù)數(shù)組。
// call()
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2
// apply()
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a:2
};
var bar = function() {
return foo.apply( obj, arguments );
};
var b = bar( 3 ); // 2 3
console.log( b ); // 5
new綁定
- 在傳統(tǒng)的語言中,構(gòu)造函數(shù)時(shí)一個(gè)特殊方法,使用 new 初始化需要調(diào)用的類,通常形式下是
let something = new MyClass();。
- 在使用 new 來調(diào)用函數(shù),會(huì)自動(dòng)執(zhí)行以下操作:
- 創(chuàng)建一個(gè)新對(duì)象
- 讓新對(duì)象的
__proto__(隱式原型) 等于函數(shù)的 prototype(顯式原型)
- 綁定 this, 讓新象綁定于函數(shù)的 this 指向
- 判斷返回值,如果返回值不是一個(gè)對(duì)象,則返回剛新建的新對(duì)象。
優(yōu)先級(jí)
- 如果在某個(gè)調(diào)用位置應(yīng)用多條規(guī)則該如何?那為了解決此問題,那就引申出了優(yōu)先級(jí)問題。
- 毫無疑問,默認(rèn)綁定的優(yōu)先級(jí)是四條規(guī)則中最低的,可以先不考慮它。
- 先來看看隱式綁定和顯式綁定那個(gè)優(yōu)先級(jí)更高?
function foo() {
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
// 隱式綁定
obj1.foo(); // 2
obj2.foo(); // 3
// 顯式綁定
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2
- 可以看出,
顯式綁定的優(yōu)先級(jí)更高,也就是說在判斷時(shí)應(yīng)當(dāng)考慮是否可以應(yīng)用顯式綁定。
- 再來看看new綁定和隱式綁定的優(yōu)先級(jí)?
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
// new綁定
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4
- 可以看出,
new 綁定比隱式綁定的優(yōu)先級(jí)更高,但 new 綁定和顯式綁定誰的優(yōu)先級(jí)更高呢?
- new 與 call/apply 無法一起使用,因此無法通過 new foo.call(obj1) 來進(jìn)行測(cè)試,但可以通過硬綁定來測(cè)試他兩的優(yōu)先級(jí)。
- 硬綁定:Function.prototype.bind(...) 會(huì)創(chuàng)建一個(gè)新的包裝函數(shù),這個(gè)函數(shù)會(huì)忽略當(dāng)前的this綁定(無論綁定的對(duì)象是什么),并把我們提供的對(duì)象綁定到this上。
- 這樣看起來硬綁定(也是顯式綁定的一種)似乎比 new 綁定的優(yōu)先級(jí)更高,無法使用 new 來控制 this 綁定。
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar(3);
console.log( obj1.a ); // 2
console.log( baz.a ); // 3
- 出乎意料! bar 被硬綁定到 obj1 上,但是 new bar(3) 并沒有像我們預(yù)計(jì)的那樣把 obj1.a 修改為 3。相反, new 修改了硬綁定(到 obj1 的)調(diào)用 bar(..) 中的 this。因?yàn)槭褂昧?new 綁定,我們得到了一個(gè)名字為 baz 的新對(duì)象,并且 baz.a 的值是 3。
- 硬綁定中的bind(...) 的功能之一就是可以把除了第一個(gè)參數(shù)(第一個(gè)參數(shù)用于綁定this)之外的其他參數(shù)傳遞給下層的函數(shù)(這種技術(shù)稱為"部分應(yīng)用",是"柯里化"的一種)。
function foo(p1,p2) {
this.val = p1 + p2;
}
// 之所以使用 null 是因?yàn)樵诒纠形覀儾⒉魂P(guān)心硬綁定的 this 是什么
// 反正使用 new 時(shí) this 會(huì)被修改
var bar = foo.bind( null, "p1" );
var baz = new bar( "p2" );
baz.val; // p1p2
-
判斷this
- 是否在 new 中調(diào)用(new 綁定), this 指向新創(chuàng)建的對(duì)象
- 是否通過 call、apply(顯示綁定),this 指向綁定的對(duì)象
- 是否在某個(gè)對(duì)象中調(diào)用(隱式綁定),this 指向綁定的上下文對(duì)象
- 如果都不是,則是默認(rèn)綁定,在嚴(yán)格模式下,this 指向 undefined, 非嚴(yán)格模式下,this 指向全局對(duì)象。
-
優(yōu)先級(jí)問題
- 顯式綁定:call()、apply()。(硬綁定也是顯式綁定的其中一種: bind())
- new 綁定: new Foo()
- 隱式綁定: obj.foo();
- 默認(rèn)綁定: foo();
排序:顯式綁定 > new 綁定 > 隱式綁 定 > 默認(rèn)綁定
綁定例子
被忽略的this
如果你把 null 或者 undefined 作為 this 的綁定對(duì)象傳入 call、apply 或者 bind,這些值在調(diào)用時(shí)會(huì)被忽略,實(shí)際應(yīng)用的是默認(rèn)綁定規(guī)則:
function foo() {
console.log( this.a );
}
var a = 2;
foo.call( null ); // 2
- 那在什么情況下會(huì)傳入 null 呢?
一種非常常見的做法是使用 apply(..) 來“展開”一個(gè)數(shù)組,并當(dāng)作參數(shù)傳入一個(gè)函數(shù)。
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 把數(shù)組“展開”成參數(shù)
foo.apply( null, [2, 3] ); // a:2, b:3
// 使用 bind(..) 進(jìn)行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3
- 但總是用 null 來忽略 this 綁定可能會(huì)產(chǎn)生一些副作用。
-
更安全的this
- DMZ(demilitarized zone)空委托對(duì)象
在 JavaScript 中創(chuàng)建一個(gè)空對(duì)象最簡(jiǎn)單的方法都是 Object.create(null)。Object.create(null) 和 {} 很 像, 但 是 并 不 會(huì) 創(chuàng) 建 Object.prototype 這個(gè)委托,所以它比 {}“更空”:
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 我們的 DMZ 空對(duì)象
var ? = Object.create( null );
// 把數(shù)組展開成參數(shù)
foo.apply( ?, [2, 3] ); // a:2, b:3
// 使用 bind(..) 進(jìn)行柯里化
var bar = foo.bind( ?, 2 );
bar( 3 ); // a:2, b:3
間接引用
function foo() {
console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2
- 賦值表達(dá)式 p.foo = o.foo 的返回值是目標(biāo)函數(shù)的引用,因此調(diào)用位置是 foo() 而不是 p.foo() 或者 o.foo()。根據(jù)我們之前說過的,這里會(huì)應(yīng)用默認(rèn)綁定。
注意:對(duì)于默認(rèn)綁定來說,決定 this 綁定對(duì)象的并不是調(diào)用位置是否處于嚴(yán)格模式,而是函數(shù)體是否處于嚴(yán)格模式。如果函數(shù)體處于嚴(yán)格模式,this 會(huì)被綁定到 undefined,否則this 會(huì)被綁定到全局對(duì)象。
軟綁定
- 硬綁定這種方式可以把 this 強(qiáng)制綁定到指定的對(duì)象(除了使用 new 時(shí)),防止函數(shù)調(diào)用應(yīng)用默認(rèn)綁定規(guī)則。使用硬綁定會(huì)大大降低函數(shù)的靈活性,使用硬綁定之后就無法使用隱式綁定或顯示綁定來修改 this。
- 可通過一種軟綁定的方法來實(shí)現(xiàn):
if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this;
// 捕獲所有 curried 參數(shù)
var curried = [].slice.call( arguments, 1 );
var bound = function() {
return fn.apply(
(!this || this === (window || global)) ?
obj : this
curried.concat.apply( curried, arguments )
);
};
bound.prototype = Object.create( fn.prototype );
return bound;
};
}
function foo() {
console.log("name: " + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看?。。?
fooOBJ.call( obj3 ); // name: obj3 <---- 看!
setTimeout( obj2.foo, 10 );
// name: obj <---- 應(yīng)用了軟綁定
- 可以看到,軟綁定的 foo() 可手動(dòng)將 this 綁定到 obj2 或 obj3 上,但如果應(yīng)用默認(rèn)綁定,則會(huì)將 this 綁定到 obj。
this 詞法
- 在 ES6 中出現(xiàn)了一種無法使用這些規(guī)則的特殊函數(shù)類型:
箭頭函數(shù)
箭頭函數(shù)不適用 this 的四種標(biāo)準(zhǔn)規(guī)則,而是根據(jù)外層(函數(shù)或全局)的作用域來決定 this
function foo() {
// 返回一個(gè)箭頭函數(shù)
return (a) => {
//this 繼承自 foo()
console.log( this.a );
};
}
var obj1 = {
a:2
};
var obj2 = {
a:3
};
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是 3 !
- foo() 內(nèi)部創(chuàng)建的箭頭函數(shù)會(huì)捕獲調(diào)用時(shí) foo() 的 this。由于 foo() 的 this 綁定到 obj1, bar(引用箭頭函數(shù))的 this 也會(huì)綁定到 obj1,箭頭函數(shù)的綁定無法被修改。(new 也不行?。?/li>
- 在 ES6 之前,我們也有使用和箭頭函數(shù)一樣的模式,如下代碼:
function foo() {
var self = this; // this 快照
setTimeout( function(){
console.log( self.a );
}, 100 );
}
var obj = {
a: 2
};
foo.call( obj ); // 2
- 雖然 self = this 和箭頭函數(shù)看起來都可以取代 bind(..),但是從本質(zhì)上來說,它們想替代的是 this 機(jī)制。
小結(jié)
-
判斷 this 指向
- 是否在 new 中調(diào)用(new 綁定), this 指向新創(chuàng)建的對(duì)象
- 是否通過 call、apply(顯示綁定),this 指向綁定的對(duì)象
- 是否在某個(gè)對(duì)象中調(diào)用(隱式綁定),this 指向綁定對(duì)象的上下文
- 如果都不是,則是默認(rèn)綁定,在嚴(yán)格模式下,this 指向 undefined, 非嚴(yán)格模式下,this 指向全局對(duì)象。
- 箭頭函數(shù)不會(huì)使用上述的四條規(guī)則,而是根據(jù)當(dāng)前的詞法作用域來決定 this 的。箭頭函數(shù)會(huì)繼承外層函數(shù)調(diào)用的 this 綁定(無論 this 綁定到什么)。與 ES6 之前的 self = this 的機(jī)制一樣。
- 注意:對(duì)于默認(rèn)綁定來說,決定 this 綁定對(duì)象的并不是調(diào)用位置是否處于嚴(yán)格模式,而是函數(shù)體是否處于嚴(yán)格模式。如果函數(shù)體處于嚴(yán)格模式,this 會(huì)被綁定到 undefined,否則this 會(huì)被綁定到全局對(duì)象。
特殊字符描述:
- 問題標(biāo)注
Q:(question)
- 答案標(biāo)注
R:(result)
- 注意事項(xiàng)標(biāo)準(zhǔn):
A:(attention matters)
- 詳情描述標(biāo)注:
D:(detail info)
- 總結(jié)標(biāo)注:
S:(summary)
- 分析標(biāo)注:
Ana:(analysis)
- 提示標(biāo)注:
T:(tips)