this
什么是this,其實(shí)你可以理解為它類似一個(gè)指針
在瀏覽器環(huán)境中,全局作用域下,this指的是windows,在node環(huán)境中,全局下this打印的是一個(gè)空對象{};
function foo(){
console.log(this);
}
foo();
上面這段代碼運(yùn)行之后,在瀏覽器環(huán)境中,會打印window對象。再看下面這段代碼。
function foo(){
console.log(this);
}
var fn1 = foo;
fn1();
上面的代碼執(zhí)行之后,打印的還是window,我們在看。
function foo(){
console.log(this)
}
var fn1 = {
a: 1,
foo: foo
}
fn1.foo();
這段代碼執(zhí)行之后返回的是fn1對象。接下來再看
function foo(){
console.log(this)
}
var arr = [foo,2,3]
arr[0]()
上述代碼執(zhí)行完成后,打印的是arr數(shù)組對象。
通過之前的代碼我們可以很明顯的看到,this 是在運(yùn)行時(shí)進(jìn)行綁定的,并不是在編寫時(shí)綁定,當(dāng)我們把函數(shù)賦值到引用對象里調(diào)用的時(shí)候,那this就指向當(dāng)前的調(diào)用環(huán)境,就是調(diào)用的對象本身
什么是執(zhí)行上下文
javascript是一個(gè)單線程語言,這意味著在瀏覽器中同時(shí)只能做一件事情。當(dāng)javascript解釋器初始執(zhí)行代碼,它首先默認(rèn)進(jìn)入全局上下文。每次調(diào)用一個(gè)函數(shù)將會創(chuàng)建一個(gè)新的執(zhí)行上下文。
每次新創(chuàng)建的一個(gè)執(zhí)行上下文會被添加到作用域鏈的頂部,有時(shí)也稱為執(zhí)行或調(diào)用棧。瀏覽器總是運(yùn)行位于作用域鏈頂部的當(dāng)前執(zhí)行上下文。一旦完成,當(dāng)前執(zhí)行上下文將從棧頂被移除并且將控制權(quán)歸還給之前的執(zhí)行上下文。比如我調(diào)用了這個(gè)函數(shù),那么這個(gè)函數(shù)的執(zhí)行上下文 就會被添加到作用域鏈的頂部,然后這個(gè)函數(shù)執(zhí)行完成這個(gè)上下文就會被移除,然后控制權(quán)交給之前的上下文。
執(zhí)行上下文的建立過程
我們現(xiàn)在已經(jīng)知道,每當(dāng)調(diào)用一個(gè)函數(shù)時(shí),一個(gè)新的執(zhí)行上下文就會被創(chuàng)建出來。然而,在javascript引擎內(nèi)部,這個(gè)上下文的創(chuàng)建過程具體分為兩個(gè)階段:
-
建立階段(發(fā)生在當(dāng)調(diào)用一個(gè)函數(shù)時(shí),但是在執(zhí)行函數(shù)體內(nèi)的具體代碼以前)
- 建立變量,函數(shù),arguments對象,參數(shù)
- 建立作用域鏈
- 確定this的值
-
代碼執(zhí)行階段:
- 變量賦值,函數(shù)引用,執(zhí)行其它代碼
我們可以看到,當(dāng)一個(gè)函數(shù)調(diào)用的時(shí)候,會建立執(zhí)行上下文,建立的時(shí)候會確定this的值,所以我們現(xiàn)在,就是要探討他的this的值是怎么確定的。
調(diào)用位置
function baz() {
// 當(dāng)前調(diào)用棧是:baz
// 因此,當(dāng)前調(diào)用位置是全局作用域
console.log("baz");
bar(); // <-- bar 的調(diào)用位置
}
function bar() {
// 當(dāng)前調(diào)用棧是 baz -> bar
// 因此,當(dāng)前調(diào)用位置在 baz 中
console.log("bar");
foo(); // <-- foo 的調(diào)用位置
}
function foo() {
// 當(dāng)前調(diào)用棧是 baz -> bar -> foo
// 因此,當(dāng)前調(diào)用位置在 bar 中
console.log("foo");
}
baz(); // <-- baz 的調(diào)用位置
注意我們是如何(從調(diào)用棧中)分析出真正的調(diào)用位置的,因?yàn)樗鼪Q定了 this 的綁定
綁定規(guī)則
我們來看看在函數(shù)的執(zhí)行過程中調(diào)用位置如何決定 this 的綁定對象。
你必須找到調(diào)用位置,然后判斷需要應(yīng)用下面四條規(guī)則中的哪一條。我們首先會分別解釋這四條規(guī)則,然后解釋多條規(guī)則都可用時(shí)它們的優(yōu)先級如何排列。
規(guī)則一 默認(rèn)綁定
獨(dú)立調(diào)用。可以把這條規(guī)則看作是無法應(yīng)用其他規(guī)則時(shí)的默認(rèn)規(guī)則。
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2
在代碼中, foo() 是直接使用不帶任何修飾的函數(shù)引用進(jìn)行調(diào)用的,因此只能使用默認(rèn)綁定,無法應(yīng)用其他規(guī)則。
如果使用嚴(yán)格模式( strict mode ),那么全局對象將無法使用默認(rèn)綁定,因此 this 會綁定到 undefined
規(guī)則二 隱式綁定
另一條需要考慮的規(guī)則是調(diào)用位置是否有上下文對象,或者說是否被某個(gè)對象擁有或者包含,不過這種說法可能會造成一些誤導(dǎo)。
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
調(diào)用位置會使用 obj 上下文來引用函數(shù),因此你可以說函數(shù)被調(diào)用時(shí) obj 對象“擁有”或者“包含”它。
當(dāng) foo() 被調(diào)用時(shí),它的落腳點(diǎn)確實(shí)指向 obj 對象。當(dāng)函數(shù)引用有上下文對象時(shí),隱式綁定規(guī)則會把函數(shù)調(diào)用中的 this 綁定到這個(gè)上下文對象。因?yàn)檎{(diào)用 foo() 時(shí) this 被綁定到 obj ,因此 this.a 和 obj.a 是一樣的。
對象屬性引用鏈中只有最頂層或者說最后一層會影響調(diào)用位置。舉例來說:
function foo() {
console.log(this.a);
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
隱式丟失
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函數(shù)別名!
var a = "oops, global"; // a 是全局對象的屬性
bar(); // "oops, global"
雖然 bar 是 obj.foo 的一個(gè)引用,但是實(shí)際上,它引用的是 foo 函數(shù)本身,因此此時(shí)的bar() 其實(shí)是一個(gè)不帶任何修飾的函數(shù)調(diào)用,因此應(yīng)用了默認(rèn)綁定。
一種更微妙、更常見并且更出乎意料的情況發(fā)生在傳入回調(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 是全局對象的屬性
doFoo(obj.foo); // "oops, global"
參數(shù)傳遞其實(shí)就是一種隱式賦值,因此我們傳入函數(shù)時(shí)也會被隱式賦值,所以結(jié)果和上一個(gè)例子一樣
規(guī)則三 顯式綁定
使用 call(..) 和 apply(..) 方法可以強(qiáng)制綁定this的指向。
function foo(){
console.log(this.a)
}
var obj = {
a: 2,
}
foo.call(obj)
如果你傳入了一個(gè)原始值(字符串類型、布爾類型或者數(shù)字類型)來當(dāng)作 this 的綁定對象,這個(gè)原始值會被轉(zhuǎn)換成它的對象形式(也就是 new String(..) 、 new Boolean(..) 或者
new Number(..) )。這通常被稱為“裝箱”。
規(guī)則四 new 綁定
在javascript中,使用new 操作符的時(shí)候,其實(shí)和其他大多數(shù)語言中使用new操作符的機(jī)制不太一樣,當(dāng)我們使用了new 操作符調(diào)用函數(shù),這個(gè)函數(shù)就會被當(dāng)做構(gòu)造函數(shù)來調(diào)用。
調(diào)用構(gòu)造函數(shù)的時(shí)候,會發(fā)生四件事情:
- 創(chuàng)建一個(gè)空對象。
- 將這個(gè)空對象的proto成員指向了構(gòu)造函數(shù)的prototype成員對象
- 這個(gè)新對象會綁定到函數(shù)調(diào)用的 this
- 如果函數(shù)沒有返回其他對象,那么 new 表達(dá)式中的函數(shù)調(diào)用會自動(dòng)返回這個(gè)新對象
關(guān)于構(gòu)造函數(shù)可查看 http://www.itdecent.cn/p/794672ea66c5
綁定優(yōu)先級
默認(rèn)綁定的優(yōu)先級是最低的。
先看隱式綁定和顯式綁定哪個(gè)優(yōu)先級更高:
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)先級更高,也就是說在判斷時(shí)應(yīng)當(dāng)先考慮是否可以應(yīng)用顯式綁定。
現(xiàn)在我們需要搞清楚 new 綁定和隱式綁定的優(yōu)先級誰高誰低:
function foo(val){
this.a = val;
}
var obj = {
foo: foo,
}
obj.foo(3);
console.log(obj.a); // 3
var bar = new obj.foo(4);
console.log(bar.a);
console.log(obj.a)
可以看到 new 綁定比隱式綁定優(yōu)先級高。但是 new 綁定和顯式綁定誰的優(yōu)先級更高呢
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
判斷this
現(xiàn)在我們可以根據(jù)優(yōu)先級來判斷函數(shù)在某個(gè)調(diào)用位置應(yīng)用的是哪條規(guī)則
- 函數(shù)是否在 new 中調(diào)用( new 綁定)?如果是的話 this 綁定的是新創(chuàng)建的對象。
var bar = new foo()
- 函數(shù)是否通過 call 、 apply (顯式綁定)或者硬綁定調(diào)用?如果是的話, this 綁定的是指定的對象
var bar = foo.call(obj2)
- 函數(shù)是否在某個(gè)上下文對象中調(diào)用(隱式綁定)?如果是的話, this 綁定的是那個(gè)上下文對象。
var bar = obj1.foo()
- 如果都不是的話,使用默認(rèn)綁定。如果在嚴(yán)格模式下,就綁定到 undefined ,否則綁定到全局對象。
var bar = foo()
被忽略的this
如果你把 null 或者 undefined 作為 this 的綁定對象傳入 call 、 apply 或者 bind ,這些值在調(diào)用時(shí)會被忽略,實(shí)際應(yīng)用的是默認(rèn)綁定規(guī)則
function foo() {
console.log(this.a);
}
var a = 2;
foo.call(null); // 2
一般我們要展開數(shù)組,或者是對參數(shù)進(jìn)行柯里化(預(yù)先設(shè)置一些參數(shù))的時(shí)候,會經(jīng)常傳入一些null值進(jìn)行占位u,es6中展開數(shù)組可以用拓展運(yùn)算符...,但是還沒有參數(shù)柯里化的相關(guān)語法。
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
這兩種方法都需要傳入一個(gè)參數(shù)當(dāng)作 this 的綁定對象。如果函數(shù)并不關(guān)心 this 的話,你仍然需要傳入一個(gè)占位值,這時(shí) null 可能是一個(gè)不錯(cuò)的選擇,
更安全的this
function foo(a, b) {
console.log("a:" + a + ", b:" + b);
}
// 我們的 DMZ 空對象
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
使用變量名 ? 不僅讓函數(shù)變得更加“安全”,而且可以提高代碼的可讀性,因?yàn)?? 表示“我希望 this 是空”,這比 null 的含義更清楚。
間接引用
另一個(gè)需要注意的是,你有可能(有意或者無意地)創(chuàng)建一個(gè)函數(shù)的“間接引用”,在這種情況下,調(diào)用這個(gè)函數(shù)會應(yīng)用默認(rèn)綁定規(guī)則。
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ù)我們之前說過的,這里會應(yīng)用默認(rèn)綁定。
箭頭函數(shù)
箭頭函數(shù)不適用this的四種標(biāo)準(zhǔn)規(guī)則。而是根據(jù)外層作用域來決定this。
function foo() {
return () => {
console.log(this.a);
}
}
var obj = {
a: 2,
foo: foo
}
var obj2 = {
a: 4
}
var bar = foo.call(obj2)
bar.call(obj); // 4
foo() 內(nèi)部創(chuàng)建的箭頭函數(shù)會捕獲調(diào)用時(shí) foo() 的 this,由于 foo() 的 this 綁定到 obj2,bar (引用箭頭函數(shù))的 this 也會綁定到 obj2,頭函數(shù)的綁定無法被修改。( new 也不行?。?/p>
總結(jié)
如果要判斷一個(gè)運(yùn)行中函數(shù)的 this 綁定,就需要找到這個(gè)函數(shù)的直接調(diào)用位置。找到之后就可以順序應(yīng)用下面這四條規(guī)則來判斷 this 的綁定對象。
- 由 new 調(diào)用?綁定到新創(chuàng)建的對象。
- 由 call 或者 apply (或者 bind )調(diào)用?綁定到指定的對象。
- 由上下文對象調(diào)用?綁定到那個(gè)上下文對象。
- 默認(rèn):在嚴(yán)格模式下綁定到 undefined ,否則綁定到全局對象。
一定要注意,有些調(diào)用可能在無意中使用默認(rèn)綁定規(guī)則。如果想“更安全”地忽略 this 綁定,你可以使用一個(gè) DMZ 對象,比如 ? = Object.create(null) ,以保護(hù)全局對象。
ES6 中的箭頭函數(shù)并不會使用四條標(biāo)準(zhǔn)的綁定規(guī)則,而是根據(jù)當(dāng)前的詞法作用域來決定this ,具體來說,箭頭函數(shù)會繼承外層函數(shù)調(diào)用的 this 綁定(無論 this 綁定到什么)。這其實(shí)和 ES6 之前代碼中的 self = this 機(jī)制一樣。