難道我們就不能徹底搞清楚“this”嗎?在某種程度上不瓶,幾乎所有的 JavaScript 開發(fā)人員都曾經(jīng)思考過“this”這個事情十酣。對我來說,每當“this”出來搗亂的時候匈棘,我就會想方設(shè)法地去解決掉它丧慈,但過后就把它忘了,我想你應該也曾遇到過類似的場景主卫。但是今天伊滋,讓我們弄明白它,讓我們一次性地徹底解決“this”的問題队秩,一勞永逸笑旺。
前幾天,我在圖書館遇到了一個意想不到的事情馍资。
這本書的整個第二章都是關(guān)于“this”的筒主,我很有自信地通讀了一遍,但是發(fā)現(xiàn)其中有些地方講到的“this”鸟蟹,我居然搞不懂它們是什么乌妙,需要去猜測。真的是時候反省一下我過度自信的愚蠢行為了建钥。我再次把這一章重讀了好幾遍藤韵,發(fā)覺這里面的內(nèi)容是每個 Javascript 開發(fā)人員都應該了解的。
因此熊经,我嘗試著用一種更徹底的方式和更多的示例代碼來展示 凱爾·辛普森 在他的這本書 你不知道的 Javascript 中描述的那些規(guī)范泽艘。
在這里我不會通篇只講理論欲险,我會直接以曾經(jīng)困擾過我的困難問題為例開始講起,我希望它們也是你感到困難的問題匹涮。但不管這些問題是否會困撓你天试,我都會給出解釋說明,我會一個接一個地向你介紹所有的規(guī)則然低,當然還會有一些追加內(nèi)容喜每。
在開始之前,我假設(shè)你已經(jīng)了解了一些 JavaScript 的背景知識雳攘,當我講到 global带兜、window、this吨灭、prototype 等等的時候刚照,你知道它們是什么意思。這篇文章中沃于,我會同時使用 global 和 window涩咖,在這里它們就是一回事海诲,是可以互換的繁莹。
在下面給出的所有代碼示例中,你的任務(wù)就是猜一下控制臺輸出的結(jié)果是什么特幔。如果你猜對了咨演,就給你自己加一分。準備好了嗎蚯斯?讓我們開始吧薄风。
Example #1
function foo() {?
console.log(this);?
bar();?
}
function bar() {?
console.log(this);?
baz();?
}
function baz() {?
console.log(this);?
}
foo();
復制代碼你被難住了嗎?為了測試拍嵌,你當然可以把這段代碼復制下來遭赂,然后在瀏覽器或者 Node 的運行環(huán)境中去運行看看結(jié)果。再來一次,你被難住了嗎横辆?好吧撇他,我就不再問了。但說真的狈蚤,如果你沒被難住困肩,那就給你自己加一分。
如果你運行上面的代碼脆侮,就會在控制臺中看到 global 對象被打印出三次锌畸。為了解釋這一點,讓我來介紹 第一個規(guī)則靖避,默認綁定潭枣。規(guī)則規(guī)定比默,當一個函數(shù)執(zhí)行獨立調(diào)用時,例如只是 funcName();卸耘,這時函數(shù)的“this”被指向 global 對象退敦。
需要理解的是,在調(diào)用函數(shù)之前蚣抗,“this”并沒有綁定到這個函數(shù)侈百,因此,要找到“this”翰铡,你應該密切注意該函數(shù)是如何調(diào)用钝域,而不是在哪里調(diào)用。所有三個函數(shù) foo();bar(); 和 baz();_ 都是獨立的調(diào)用锭魔,因此這三個函數(shù)的“this”都指向全局對象例证。
Example #2
‘use strict’;
function foo() {
console.log(this);
bar();
}
function bar() {
console.log(this);
baz();
}
function baz() {
console.log(this);
}
foo();
復制代碼注意下最開始的“use strict”。在這種情況下迷捧,你覺得控制臺會打印什么织咧?當然,如果你了解 strict mode漠秋,你就會知道在嚴格模式下 global 對象不會被默認綁定笙蒙。所以,你得到的打印是三次 undefined 的輸出庆锦,而不再是 global捅位。
回顧一下,在一個簡單調(diào)用函數(shù)中搂抒,比如獨立調(diào)用中艇搀,“this”在非嚴格模式下指向 global 對象,但在嚴格模式下不允許 global 對象默認綁定求晶,因此這些函數(shù)中的“this”是 undefined焰雕。
為了使我們對默認綁定概念理解得更加具體,這里有一些示例芳杏。
Example #3
function foo() {
function bar() {
? console.log(this);
}
bar();
}
foo();
復制代碼foo 先被調(diào)用矩屁,然后又調(diào)用 bar,bar 將“this”打印到控制臺中蚜锨。這里的技巧是看看函數(shù)是如何被調(diào)用的档插。foo 和 bar 都被單獨調(diào)用,因此亚再,他們內(nèi)部的“this”都是指向 global 對象郭膛。但是由于 bar 是唯一執(zhí)行打印的函數(shù),所以我們看到 global 對象在控制臺中輸出了一次氛悬。
我希望你沒有回答 foo 或 bar则剃。有沒有耘柱?
我們已經(jīng)了解了默認綁定。讓我們再做一個簡單的測試棍现。在下面的示例中调煎,控制臺輸出什么?
Example #4
var a = 1;
function foo() {?
console.log(this.a);?
}
foo();
復制代碼輸出結(jié)果是 undefined己肮?是 1士袄?還是什么?
如果你已經(jīng)很好地理解了之前講解的內(nèi)容谎僻,那么你應該知道控制臺輸出的是“1”娄柳。為什么?首先艘绍,默認綁定作用于函數(shù) foo赤拒。因此 foo 中的“this”指向 global 對象,并且 a 被聲明為 global 變量诱鞠,這就意味著 a 是 global 對象的屬性(也稱之為全局對象污染)挎挖,因此 this.a 和 var a 就是同一個東西。
隨著本文的深入航夺,我們將會繼續(xù)研究默認綁定蕉朵,但是現(xiàn)在是時候向你介紹下一個規(guī)則了。
Example #5
var obj = {?
a: 1,?
foo: function() {?
? console.log(this);?
}?
};
obj.foo();
復制代碼這里應該沒有什么疑問敷存,對象“obj”會被輸出在控制臺中墓造。你在這里看到的是 隱式綁定堪伍。規(guī)則規(guī)定锚烦,當一個函數(shù)被作為一個對象方法被調(diào)用時,那么它內(nèi)部的“this”應該指向這個對象帝雇。如果函數(shù)調(diào)用前面有多個對象( obj1.obj2.func() )涮俄,那么函數(shù)之前的最后一個對象(obj3)會被綁定。
需要注意的一點是函數(shù)調(diào)用必須有效尸闸,那也就是說當你調(diào)用 obj.func() 時彻亲,必須確保 func 是對象 obj 的屬性。
因此吮廉,在上面的例子中調(diào)用 obj.foo() 時苞尝,“this”就指向 obj,因此 obj 被打印輸出在控制臺中宦芦。
Example #6
function logThis() {?
console.log(this);?
}
var myObject = {?
a: 1,?
logThis: logThis?
};
logThis();?
myObject.logThis();
復制代碼你被難住了宙址?我希望沒有。
跟在 myObject 后面的這個全局調(diào)用 logThis() 通過 console.log(this) 打印的是 global 對象调卑;而 myObject.logThis() 打印的是 myObject 對象抡砂。
這里需要注意一件有趣的事情:
console.log(logThis === myObject.logThis); // true
復制代碼為什么不呢大咱?它們當然是相同的函數(shù),但是你可以看到 如何調(diào)用_logThis_ 會讓其中的“this”發(fā)生改變注益。當 logThis 被單獨調(diào)用時碴巾,使用默認綁定規(guī)則,但是當 logThis 作為前面的對象屬性被調(diào)用時丑搔,使用隱式綁定規(guī)則厦瓢。
不管采用哪條規(guī)則,讓我們看看是怎么處理的(雙關(guān)語)啤月。
Example #8
function foo() {?
var a = 2;?
this.bar();?
}
function bar() {?
console.log(this.a);?
}
foo();
復制代碼控制臺輸出什么旷痕?首先,你可能會問我們可以調(diào)用“_this.bar()”嗎顽冶?當然可以欺抗,它不會導致錯誤。
就像示例 #4 中的 var a 一樣强重,bar 也是全局對象的屬性绞呈。因為 foo 被單獨調(diào)用了,它內(nèi)部的“this”就是全局對象(默認綁定規(guī)則)间景。因此 foo 內(nèi)部的 this.bar 就是 bar佃声。但實際的問題是,控制臺中輸出什么倘要?
如果你猜的沒錯圾亏,“undefined”會被打印出來。
注意 bar 是如何被調(diào)用的封拧?看起來志鹃,隱式綁定在這里發(fā)揮作用。隱式綁定意味著 bar 中的“this”是其前面的對象引用泽西。bar 前面的對象引用是全局對象曹铃,在 foo 里面是全局對象,對不對捧杉?因此在 bar 中嘗試訪問 this.a 等同于訪問 [global object].a陕见。沒有什么意外,因此控制臺會輸出 undefined味抖。
太棒了评甜!繼續(xù)向下講解。
Example #7
var obj = {?
a: 1,?
foo: function(fn) {?
? console.log(this);?
? fn();?
}?
};
obj.foo(function() {?
console.log(this);?
});
復制代碼請不要讓我失望仔涩。
函數(shù) foo 接受一個回調(diào)函數(shù)作為參數(shù)忍坷。我們所做的就是在調(diào)用 foo 的時候在參數(shù)里面放了一個函數(shù)。
obj.foo( function() { console.log(this); } );
復制代碼但是請注意 foo 是 如何 被調(diào)用的。它是一個單獨調(diào)用嗎承匣?當然不是蓖乘,因此第一個輸出到控制臺的是對象 obj 。我們傳入的回調(diào)函數(shù)是什么韧骗?在 foo 內(nèi)部嘉抒,回調(diào)函數(shù)變?yōu)?fn ,注意 fn 是 如何 被調(diào)用的袍暴。對些侍,因此 fn 中的“this”是全局對象,因此第二個被輸出到控制臺的是全局對象政模。
希望你不會覺得無聊岗宣。順便問一下,你的分數(shù)怎么樣淋样?還可以嗎耗式?好吧,這次我準備難倒你了趁猴。
Example #8
var arr = [1, 2, 3, 4];
Array.prototype.myCustomFunc = function() {
console.log(this);
};
arr.myCustomFunc();
復制代碼如果你還不知道 Javascript 里面的 .prototype 是什么刊咳,那你就權(quán)且把它和其他對象等同看待,但如果你是 JavaScript 開發(fā)者儡司,你應該知道娱挨。你知道嗎?努努力捕犬,再去多讀一些關(guān)于原型鏈相關(guān)的書籍吧跷坝。我在這里等著你。
那么打印輸出的是什么碉碉?是 Array.prototype 對象柴钻?錯了!
這是和之前相同的技巧誉裆,請檢查 custommyfunc 是 如何 被調(diào)用的顿颅。沒錯缸濒,隱式綁定把 arr 綁定到 myCustomFunc足丢,因此輸出到控制臺的是 arr[1,2,3,4]。
我說的庇配,你理解了嗎斩跌?
Example #9
var arr = [1, 2, 3, 4];
arr.forEach(function() {?
console.log(this);?
});
復制代碼執(zhí)行上述代碼的結(jié)果是,在控制臺中輸出了 4 次全局對象捞慌。如果你錯了耀鸦,也沒關(guān)系。請再看示例#7。還沒理解袖订?下一個示例會有所幫助氮帐。
Example #10
var arr = [1, 2, 3, 4];
Array.prototype.myCustomFunc = function(fn) {?
console.log(this);?
fn();?
};
arr.myCustomFunc(function() {?
console.log(this);?
});
復制代碼就像示例 #7 一樣,我們將回調(diào)函數(shù) fn 作為參數(shù)傳遞給函數(shù) myCustomFunc洛姑。結(jié)果是傳入的函數(shù)會被獨立調(diào)用上沐。這就是為什么在前面的示例(#9)中輸出全局對象,因為在 forEach 中傳入的回調(diào)函數(shù)被獨立調(diào)用楞艾。
類似地参咙,在本例中,首先輸出到控制臺的是 arr硫眯,然后是輸出的是全局對象蕴侧。我知道這看上去有點復雜,但我相信如果你能再多用點心两入,你會弄明白的净宵。
讓我們繼續(xù)使用這個數(shù)組的示例來介紹更多的概念。我想我會在這里使用一個簡稱裹纳,WGL 怎么樣塘娶?作為 WHAT.GETS.LOGGED 的簡稱?好吧痊夭,在我開始老生常談之前刁岸,下面是另外一個例子。
Example #11
var arr = [1, 2, 3, 4];
Array.prototype.myCustomFunc = function() {?
console.log(this);
(function() {?
console.log(this);?
})();
};
arr.myCustomFunc();
復制代碼那么她我,輸出是虹曙?
答案和示例 #10 完全一樣。輪到你了番舆,說一說為什么首先輸出的是 arr酝碳?你看到第一個 console.log(this) 的下面有一段復雜的代碼,它被稱為 IIFE(立即調(diào)用的函數(shù)表達式)恨狈。這個名字不用再過多解釋了疏哗,對吧?被 (…)(); 這樣形式封裝的函數(shù)會立即被調(diào)用禾怠,也就是說等同于被獨立調(diào)用返奉,因此它內(nèi)部的“this”是全局變量,所以輸出的是全局變量吗氏。
要來新概念了芽偏!讓我們看看你對 ES2015 的熟悉程度。
Example #12
var arr = [1, 2, 3, 4];
Array.prototype.myCustomFunc = function() {?
console.log(this);
(function() {?
? console.log(‘Normal this : ‘, this);?
})();
(() =\> {?
? console.log(‘Arrow function this : ‘, this);?
})();
};
arr.myCustomFunc();
復制代碼除了 IIFE 后面的增加了 3 行代碼之外弦讽,其他代碼與示例 #11 完全相同污尉。它實際上也是一種 IIFE,只是語法稍有不同。嗨被碗,這是箭頭函數(shù)某宪。
箭頭函數(shù)的意思是,這些函數(shù)中的“this”是一個詞法變量锐朴。也就是說缩抡,當將“this”與這種箭頭函數(shù)綁定時,函數(shù)會從包裹它的函數(shù)或作用域中獲取“this”的值包颁。包裹我們這個箭頭函數(shù)的函數(shù)里面的“this”是 arr瞻想。因此?
// This is WGL
arr [1, 2, 3, 4]
Normal this : global
Arrow function this : arr [1, 2, 3, 4]
復制代碼如果我用箭頭函數(shù)重寫示例 #9 會怎么樣娩嚼?控制臺輸出什么呢蘑险?
var arr = [1, 2, 3, 4];
arr.forEach(() => {
console.log(this);
});
復制代碼上面的這個例子是額外追加的,所以即使你猜對了也不用增加分數(shù)岳悟。你還在算分嗎佃迄?書呆子。
現(xiàn)在請仔細關(guān)注以下示例贵少。我會不惜一切代價讓你弄懂他們 :-)呵俏。
Example #13
var yearlyExpense = {
year: 2016,
expenses: [
? {‘month’: ‘January’, amount: 1000},
? {‘month’: ‘February’, amount: 2000},
? {‘month’: ‘March’, amount: 3000}
? ],
printExpenses: function() {
? this.expenses.forEach(function(expense) {
? console.log(expense.amount + ‘ spent in ‘ + expense.month + ‘, ‘ +? ? this.year);
? });
? }
};
yearlyExpense.printExpenses();
復制代碼那么,輸出是?多點時間想一想滔灶。
這是答案普碎,但我希望你在閱讀解釋之前先自己想想。
1000 spent in January, undefined?
2000 spent in February, undefined?
3000 spent in March, undefined
復制代碼這都是關(guān)于 printExpenses 函數(shù)的录平。首先注意下它是如何被調(diào)用的麻车。隱式綁定?是的斗这。所以 printExpenses 中的“this”指向的是對象 yearlycost动猬。這意味著 this.expenses 是 yearlyExpense 對象中的 expenses 數(shù)組,所以這里沒有問題”砑現(xiàn)在赁咙,當它在傳遞給 forEach 的回調(diào)函數(shù)中出現(xiàn)“this”時,它當然是全局對象免钻,請參考例 #9彼水。
注意,下面的“修正”版本是如何使用箭頭函數(shù)進行改進的伯襟。
var expense = {
year: 2016,
expenses: [
? {‘month’: ‘January’, amount: 1000},
? {‘month’: ‘February’, amount: 2000},
? {‘month’: ‘March’, amount: 3000}
? ],
printExpenses: function() {
? this.expenses.forEach((expense) => {
? ? console.log(expense.amount + ‘ spent in ‘ + expense.month + ‘, ‘ +? this.year);
? });
? }
};
expense.printExpenses();
復制代碼這樣我們就得到了想要的輸出結(jié)果:
1000 spent in January, 2016?
2000 spent in February, 2016?
3000 spent in March, 2016
復制代碼到目前為止猿涨,我們已經(jīng)熟悉了隱式綁定和默認綁定。我們現(xiàn)在知道函數(shù)被調(diào)用的方式?jīng)Q定了它里面的“this”姆怪。我們還簡要地講了箭頭函數(shù)以及它們內(nèi)部的“this”是怎樣定義的。
在我們討論其他規(guī)則之前,你應該知道稽揭,有些情況下俺附,我們的“this”可能會丟失隱式綁定。讓我們快速地看一下這些例子溪掀。
Example #14
var obj = {?
a: 2,?
foo: function() {?
? console.log(this);?
}?
};
obj.foo();
var bar = obj.foo;?
bar();
復制代碼不要被這里面的花哨代碼所分心事镣,只需注意函數(shù)是如何被調(diào)用的,就可以弄明白“this”的含義揪胃。你現(xiàn)在一定已經(jīng)掌握這個技巧了吧璃哟。首先 obj.foo() 被調(diào)用,因為 foo 前面有一個對象引用喊递,所以首先輸出的是對象 obj随闪。bar 當然是被獨立調(diào)用的,因此下一個輸出是全局變量骚勘。提醒你一下铐伴,記住在嚴格模式下,全局對象是不會默認綁定的俏讹,因此如果你在開啟了嚴格模式当宴,那么控制臺輸出的就是 undefined,而不再是全局變量泽疆。
bar 和 foo 是對同一個函數(shù)的引用户矢,唯一區(qū)別是它們被調(diào)用的方式不同。
Example #15
var obj = {?
a: 2,?
foo: function() {?
? console.log(this.a);?
}?
};
function doFoo(fn) {?
fn();?
}
doFoo(obj.foo);
復制代碼這里也沒什么特別的殉疼。我們是通過把 obj.foo 作為 doFoo 函數(shù)的參數(shù)(doFoo 這個名字聽起來很有趣)逗嫡。同樣, fn 和 foo 是對同一個函數(shù)的引用≈暌溃現(xiàn)在我要重復同樣的分析過程驱证, fn 被獨立調(diào)用,因此 fn 中的“this”是全局對象恋腕。而全局對象沒有屬性 a抹锄,因此我們在控制臺中得到了 undifined 的輸出結(jié)果。
到這里荠藤,我們這部分就講完了伙单。在這一部分中,我們討論了將“this”綁定到函數(shù)的兩個規(guī)則哈肖。默認綁定和隱式綁定吻育。我們研究了如何使用“use strict”來影響全局對象的綁定,以及如何會讓隱式綁定的“this”失效淤井。我希望在接下來的第二部分中布疼,你會發(fā)現(xiàn)本文對你有所幫助摊趾,在那里我們將介紹一些新規(guī)則,包括 new 和顯式綁定游两。那里再見吧砾层!
在我們結(jié)束之前,我想用一個“簡單”的例子來作為這一部分的收尾贱案,當我開始使用 Javascript 時肛炮,這個例子曾經(jīng)讓我感到非常震驚。Javascript 里面也并不是所有的東西都是美的宝踪,也有看起來很糟糕的東西侨糟。讓我們看看其中的一個。
var obj = {?
a: 2,?
b: this.a * 2?
};
console.log( obj.b ); // NaN
復制代碼它讀起來感覺很好瘩燥,在 obj 里面秕重,“this”應該是 obj,因此是 this.a 應該是 2颤芬。嗯,錯了悲幅。因為在這個對象里面的“this”是全局對象,所以如果你像這么寫…
var myObj = {?
a: 2,?
b: this?
};
console.log(myObj.b); // global
復制代碼控制臺輸出的就是全局對象站蝠。你可能會說“但是汰具,myObj 是全局對象的屬性(示例 #4 和示例 #8),不對嗎菱魔?”是的留荔,絕對正確。
console.log( this === myObj.b ); // true?
console.log( this.hasOwnProperty(‘myObj’) ); //true
復制代碼“也就是說澜倦,如果我像這樣寫的話聚蝶,它就可以!”
var myObj = {?
a: 2,?
b: this.myObj.a * 2?
};
復制代碼遺憾的是藻治,不是這樣的碘勉,這會導致邏輯錯誤。上面的代碼是不正確的桩卵,編譯器會抱怨它找不到未定義的屬性 a验靡。為什么會這樣?我也不太清楚雏节。
幸運的是胜嗓,getters(隱式綁定)可以給我們提供幫助。
var myObj = {?
a: 2,?
get b() {?
? return this.a * 2?
}?
};
console.log( myObj.b ); // 4
復制代碼你堅持到最后了钩乍!做得好辞州。第二部分,我們再見寥粹。