導(dǎo)語(yǔ)
不得不說(shuō)燎潮,作為一名初級(jí)的前端開(kāi)發(fā)者蔑滓,this關(guān)鍵字這個(gè)問(wèn)題對(duì)于我來(lái)說(shuō)一直是一個(gè)痛點(diǎn),什么是this拆宛?什么是函數(shù)的執(zhí)行環(huán)境?函數(shù)的執(zhí)行環(huán)境和this之間的關(guān)聯(lián)是什么讼撒?以及在不同的函數(shù)調(diào)用方式(function invocation,method invocation浑厚,constructor invocation,indirect invocation)里this的具體值是什么根盒? 于是我?guī)е@些問(wèn)題對(duì)this關(guān)鍵字進(jìn)行了深入的學(xué)習(xí)钳幅,并寫了一個(gè)相關(guān)的demo。
一. 認(rèn)識(shí)JavaScript函數(shù)的執(zhí)行環(huán)境
以前學(xué)C語(yǔ)言的時(shí)候炎滞,學(xué)過(guò)函數(shù)的執(zhí)行過(guò)程敢艰,在C語(yǔ)言中,函數(shù)調(diào)用的時(shí)候册赛,會(huì)在內(nèi)存里的程序執(zhí)行棧上push即將被調(diào)用的函數(shù)的地址入棧钠导,然后再根據(jù)被調(diào)用的函數(shù)的地址震嫉,執(zhí)行函數(shù)里相應(yīng)的代碼。我發(fā)現(xiàn)這種調(diào)用方式對(duì)應(yīng)于JavaScript的函數(shù)調(diào)用方式也是有類似的地方牡属, 每一個(gè)函數(shù)都有自己的執(zhí)行環(huán)境票堵,當(dāng)執(zhí)行流進(jìn)入到這個(gè)函數(shù)時(shí),該執(zhí)行環(huán)境會(huì)被推入到環(huán)境棧中逮栅,函數(shù)執(zhí)行完畢之后悴势,推出該執(zhí)行環(huán)境,把執(zhí)行流控制權(quán)還給之前的執(zhí)行環(huán)境措伐。
每個(gè)執(zhí)行環(huán)境都有一個(gè)與之關(guān)聯(lián)的變量對(duì)象特纤,環(huán)境中所定義的所以變量以及方法都保存在這個(gè)變量對(duì)象中。全局執(zhí)行環(huán)境是一個(gè)最外圍的執(zhí)行環(huán)境侥加,根據(jù)js所處在不同的宿主環(huán)境來(lái)決定全局執(zhí)行環(huán)境捧存,在web瀏覽器中模暗,全局執(zhí)行環(huán)境是window對(duì)象水援,全局里所定義的所以變量和方法都在這個(gè)對(duì)象里。
二. 認(rèn)識(shí)JavaScript中的函數(shù)的調(diào)用方式
由于 this 關(guān)鍵字和JavaScript中函數(shù)的調(diào)用方式有著很密切的關(guān)系杠河,所以我們先談?wù)凧avaScript中的函數(shù)的調(diào)用方式氢架。
在JavaScript中傻咖,函數(shù)的調(diào)用一共有四種方式:
- 函數(shù)調(diào)用(function invocation)
function add(a,b) {
return a + b;
}
//函數(shù)調(diào)用(function invocation)
var result = add(1, 2);
值得注意的是,立即調(diào)用函數(shù)(IIFE)也是屬于函數(shù)調(diào)用這種方式岖研。
var calculate = (function (num) {
return 10 + num;
})(2);
- 方法調(diào)用(method invocation)
var addNum = {
sum: function (a,b) {
return a + b;
}
}
//方法調(diào)用(method invocation)
addNum.sum(1, 2);
- 構(gòu)造函數(shù)調(diào)用(constructor invocation)
function Car(name,price) {
this.name = name;
this.price = price;
}
//構(gòu)造函數(shù)調(diào)用(constructor invocation)
var Bus = new Car('bus', 100000);
- 間接調(diào)用(indirect invocation)
function increment(num) {
return ++num;
}
//間接調(diào)用(indirect invocation)
increment.call(undefined,1);
三. 在四種JavaScript函數(shù)調(diào)用方式中的this
1. 在函數(shù)調(diào)用中的this
一般來(lái)說(shuō)卿操,函數(shù)調(diào)用的時(shí)候this的值就是全局環(huán)境執(zhí)行對(duì)象,也就是如果JavaScript當(dāng)前的執(zhí)行環(huán)境是瀏覽器的話孙援,這個(gè)時(shí)候的函數(shù)調(diào)用中的this就是指的window對(duì)象害淤。
function add(a,b) {
console.log(this === window)//true
return a + b;
}
//函數(shù)調(diào)用(function invocation)
var result = add(1, 2);
但是需要注意的一點(diǎn)是,在嚴(yán)格模式下拓售,函數(shù)調(diào)用中的this的值就不再是全局環(huán)境執(zhí)行對(duì)象了窥摄,而是undefined。
在函數(shù)調(diào)用中有一個(gè)很需要注意的地方础淤,內(nèi)層定義的函數(shù)的this并不一定等同于外層定義的函數(shù)的this崭放,也就是說(shuō)如果外層定義的函數(shù)是以函數(shù)聲明的方式表達(dá)的,那么內(nèi)層定義的函數(shù)的this還是和外層函數(shù)的this一樣鸽凶,都是全局環(huán)境執(zhí)行對(duì)象币砂;
function add(a,b) {
alert(this === window);//true
function innerf() {
alert(this === window);//true
}
innerf();
return a + b;
}
//函數(shù)調(diào)用(function invocation)
var result = add(1, 2);
但是,當(dāng)外層函數(shù)是以對(duì)象的屬性定義在對(duì)象里時(shí)玻侥,外層函數(shù)的this為當(dāng)前對(duì)象决摧,而內(nèi)層函數(shù)中的 this 是window(嚴(yán)格模式下為 undefined)。
var addNum = {
sum: function (a,b) {
alert(this === addNum)//true
function innerf() {
alert(this === window)//true
}
innerf();
return a + b;
}
}
//方法調(diào)用(method invocation)
addNum.sum(1, 2);
所以,在這種情況下掌桩,內(nèi)層函數(shù)是無(wú)法通過(guò)this對(duì)象來(lái)讀取到addNum對(duì)象里的屬性的边锁。不過(guò),為了能解決這個(gè)問(wèn)題拘鞋,我們可以使用在內(nèi)層函數(shù)調(diào)用的時(shí)候調(diào)用內(nèi)層函數(shù)的call函數(shù)來(lái)將外層函數(shù)的this值傳入到內(nèi)層函數(shù)中砚蓬,也就是調(diào)用innerf.call(this)。
2. 在方法調(diào)用中的this
當(dāng)我們以方法調(diào)用的模式調(diào)用函數(shù)的時(shí)候盆色,函數(shù)內(nèi)部的this的值是調(diào)用這個(gè)函數(shù)的對(duì)象灰蛙,在執(zhí)行方法調(diào)用的時(shí)候,被調(diào)用的函數(shù)的執(zhí)行環(huán)境也就是調(diào)用這個(gè)函數(shù)的對(duì)象隔躲,需要注意的一點(diǎn)是摩梧,當(dāng)一個(gè)JavaScript對(duì)象從它的原型那里繼承了方法時(shí),這個(gè)對(duì)象調(diào)用從其原型繼承的函數(shù)的時(shí)候宣旱,這個(gè)從原型那里繼承來(lái)的函數(shù)執(zhí)行環(huán)境仍然是這個(gè)對(duì)象(而不是其原型對(duì)象)仅父,即這個(gè)函數(shù)的this值為當(dāng)前調(diào)用此函數(shù)的對(duì)象。
function Test1() {
}
Test1.prototype.addNew = function () {
alert(this == temp)//true
}
var temp = new Test1();
temp.addNew();
在ES6的class語(yǔ)法中浑吟,被創(chuàng)建的對(duì)象也是其內(nèi)部方法中的this對(duì)象的值笙纤。
這一切看起來(lái)似乎順理成章,但是我們可以想象一個(gè)情況组力,就是當(dāng)我們將對(duì)象內(nèi)部的函數(shù)抽取出來(lái)并將之賦值到一個(gè)新的變量上省容,那么當(dāng)我們?cè)偻ㄟ^(guò)調(diào)用這個(gè)變量所指向的函數(shù)時(shí),此時(shí)函數(shù)內(nèi)部的this還會(huì)是之前的那個(gè)對(duì)象嗎燎字?
var addNum = {
sum: function (a,b) {
alert(this === addNum)// ????
return a + b;
}
}
var newFunc = addNum.sum;
newFunc(1, 2);
實(shí)際上腥椒,結(jié)合我之前所說(shuō)的,函數(shù)調(diào)用和方法調(diào)用的形式是不同的候衍,根據(jù)這點(diǎn)我們就可以知道這個(gè)問(wèn)題的答案了笼蛛,雖然我們將對(duì)象內(nèi)部的函數(shù)賦值給了一個(gè)新的變量,但是當(dāng)我們調(diào)用這個(gè)變量來(lái)執(zhí)行函數(shù)的時(shí)候蛉鹿,是以函數(shù)調(diào)用的方式來(lái)執(zhí)行的滨砍,所以此時(shí)函數(shù)內(nèi)部的this值應(yīng)該是全局環(huán)境執(zhí)行對(duì)象(嚴(yán)格模式下為undefined)。現(xiàn)在讓我們?cè)侔褑?wèn)題更深入一步妖异,該怎樣實(shí)現(xiàn)把一個(gè)對(duì)象內(nèi)的函數(shù)抽取出來(lái)賦值給了一個(gè)新的變量的時(shí)候惨好,并在調(diào)用這個(gè)變量去執(zhí)行函數(shù)時(shí),這個(gè)函數(shù)內(nèi)部的this值仍然是這個(gè)對(duì)象随闺?答案很簡(jiǎn)單,通過(guò)函數(shù)自身的綁定方法蔓腐,也就是bind方法矩乐,將對(duì)象的值賦給函數(shù)的this。
var addNum = {
sum: function (a,b) {
alert(this === addNum)// true
return a + b;
}
}
var newFunc = addNum.sum.bind(addNum);
newFunc(1, 2);
3. 在構(gòu)造函數(shù)調(diào)用中的this
說(shuō)到在構(gòu)造函數(shù)調(diào)用中的this,我們需要先清楚構(gòu)造函數(shù)的概念散罕,在JavaScript中函數(shù)雖然是對(duì)象分歇,但是函數(shù)也能夠去創(chuàng)建新的對(duì)象,而通常用函數(shù)去創(chuàng)建對(duì)象的形式是在類似于函數(shù)調(diào)用的方式前加上new關(guān)鍵字欧漱。而用new關(guān)鍵字來(lái)創(chuàng)建一個(gè)對(duì)象一共會(huì)經(jīng)歷四步
創(chuàng)建一個(gè)Object對(duì)象實(shí)例
將構(gòu)造函數(shù)的執(zhí)行環(huán)境設(shè)置為新創(chuàng)建的這個(gè)實(shí)例
執(zhí)行構(gòu)造函數(shù)中的代碼
返回新生成的對(duì)象實(shí)例
所以职抡,在構(gòu)造函數(shù)調(diào)用的時(shí)候,this關(guān)鍵字就是當(dāng)前被構(gòu)造出來(lái)的新對(duì)象误甚。也就是說(shuō)構(gòu)造函數(shù)調(diào)用時(shí)的執(zhí)行環(huán)境也就是當(dāng)前被構(gòu)造出來(lái)的新對(duì)象缚甩。
function City() {
alert(this instanceof city);//true
this.name = 'beijing';
}
var bj = new City();
alert(bj.name); //beijing
在ES6的class語(yǔ)法中,constructor方法的作用是負(fù)責(zé)對(duì)象的初始化窑邦,在constructor方法里this關(guān)鍵字的值就是新創(chuàng)建出來(lái)的那個(gè)對(duì)象擅威。
class City {
constructor (){
alert(this instanceof City);//true
this.name = 'beijing';
}
}
var bj = new City();
在JavaScript中我們也可以不使用new關(guān)鍵字來(lái)創(chuàng)建對(duì)象,我們可以使用函數(shù)調(diào)用的方式來(lái)創(chuàng)建一個(gè)對(duì)象冈钦,只要在函數(shù)的最后加上return 創(chuàng)建的對(duì)象
即可郊丛。但是這種創(chuàng)建對(duì)象的方式可能會(huì)帶來(lái)一個(gè)問(wèn)題,例如下面的代碼示例:
function City() {
alert(this instanceof City);//false
alert(this === window);//true
this.name = 'beijing';
return this;
}
var bj = City();
理由正是之前提到的函數(shù)調(diào)用的執(zhí)行環(huán)境是全局環(huán)境執(zhí)行對(duì)象瞧筛,所以在瀏覽器上厉熟,由于函數(shù)調(diào)用的執(zhí)行環(huán)境是window,函數(shù)內(nèi)部的this值即為window對(duì)象较幌,所以這樣的調(diào)用方式并不會(huì)產(chǎn)生一個(gè)新的對(duì)象揍瑟,而是給全局執(zhí)行對(duì)象增加了新的屬性。
4. 在間接調(diào)用中的this
我們知道在JavaScript中函數(shù)也是對(duì)象绅络,所以函數(shù)也擁有對(duì)象的特性月培,即函數(shù)也可以有自己的屬性,因此函數(shù)也擁有一些內(nèi)部方法恩急,比如.call()
,.apply()
杉畜。這兩個(gè)函數(shù)的內(nèi)部方法的共同特點(diǎn)是它們接受的第一個(gè)參數(shù)即被當(dāng)做函數(shù)的執(zhí)行環(huán)境對(duì)象,不同點(diǎn)是.call()
接受的第一個(gè)參數(shù)之后的參數(shù)形式為list衷恭,而.apply()
接受的第一個(gè)參數(shù)之后的參數(shù)形式為array此叠。
function increment(num) {
return ++ num;
}
increment.call(window,1); // 2
increment.apply(window, [1]);// 2
正如代碼示例,當(dāng)以increment.call()
,increment.apply()
的形式調(diào)用函數(shù)的時(shí)候随珠,其實(shí)是向increment函數(shù)的執(zhí)行環(huán)境對(duì)象賦值并執(zhí)行函數(shù)的代碼灭袁。
因此我們可以知道出在像.call()
,.apply()
這樣的間接調(diào)用中,函數(shù)中的this關(guān)鍵字的值即為這兩個(gè)間接調(diào)用函數(shù)的第一個(gè)參數(shù)值窗看。
function increment(num) {
alert(this === newOb)//true
return ++ num;
}
var newOb = {name:'addFunc'};
increment.call(newOb,1); // 2
increment.apply(newOb, [1]);// 2
由于有了間接調(diào)用的方式茸歧,我們可以利用間接調(diào)用來(lái)使得函數(shù)的執(zhí)行環(huán)境對(duì)象為我們所指定的值。
5. Bound function
現(xiàn)在來(lái)講一講在JavaScript函數(shù)中的另一種調(diào)用————Bound function显沈,熟悉JavaScript的人都知道软瞎,JavaScript的函數(shù)對(duì)象還擁有另一個(gè)方法————.bind()
逢唤,這個(gè)方法能夠產(chǎn)生一個(gè)和調(diào)用這個(gè)方法的原函數(shù)一樣代碼的新函數(shù),并把在調(diào)用時(shí)傳入的第一個(gè)參數(shù)作為這個(gè)新函數(shù)的執(zhí)行環(huán)境對(duì)象涤浇。也就是說(shuō)這個(gè)函數(shù)和.call()
,.apply()
這兩個(gè)方法不一樣的是執(zhí)行.call()
,.apply()
這兩個(gè)間接調(diào)用時(shí)鳖藕,原函數(shù)會(huì)被立刻執(zhí)行,而.bind()
是產(chǎn)生并且返回一個(gè)和原函數(shù)一樣代碼的函數(shù)對(duì)象只锭,且這個(gè)新函數(shù)的執(zhí)行環(huán)境對(duì)象是.bind()
方法的第一個(gè)參數(shù)著恩,但是這個(gè)新的函數(shù)并不會(huì)被立即執(zhí)行,也就是說(shuō)這個(gè)新的函數(shù)對(duì)象只是被預(yù)定義了執(zhí)行環(huán)境對(duì)象而已蜻展。
var caculateObj = {
numbers:[1,2,3],
getNumbers: function () {
return this.numbers;
}
}
var bindFunc = caculateObj.getNumbers.bind(caculateObj);
bindFunc();// [1,2,3]
var simpleFunc = caculateObj.getNumbers;
simpleFunc();// window or undefined
在調(diào)用.bind()
的時(shí)候需要注意的是喉誊,這種方式的綁定this對(duì)象是“永久”的,也就是說(shuō)通過(guò)調(diào)用.bind()
方法定義了新函數(shù)內(nèi)部的this值是會(huì)一直存在并且不可被再次修改的铺呵,即便是在對(duì)新函數(shù)使用.call()
,.apply()
也是不能夠重新定義this值的裹驰。不過(guò)我們可以對(duì)調(diào)用.bind()
創(chuàng)造出來(lái)的新函數(shù)進(jìn)行構(gòu)造函數(shù)調(diào)用來(lái)改變this值,不過(guò)這種方式并不推薦~
6. 在箭頭函數(shù)中的this關(guān)鍵字
想必接觸過(guò)ES6的人對(duì)箭頭函數(shù)一定不陌生片挂,那么在箭頭函數(shù)里的this值是什么呢幻林?
對(duì)于箭頭函數(shù)來(lái)說(shuō),它本身是不能夠像普通函數(shù)一樣創(chuàng)建自己的執(zhí)行環(huán)境對(duì)象的音念,但是它可以繼承它的外部函數(shù)中的執(zhí)行環(huán)境對(duì)象的沪饺,也就說(shuō)其實(shí)對(duì)于箭頭函數(shù)來(lái)說(shuō)它的this值是來(lái)自于它外部函數(shù)的this值。仔細(xì)一想闷愤,這種方式是不是又提供了之前提到的在對(duì)象內(nèi)部的函數(shù)屬性里定義新的函數(shù)整葡,新的函數(shù)的this為window對(duì)象或者undefined的問(wèn)題的一種解決辦法呢?之前如果我們想讓對(duì)象內(nèi)部的函數(shù)屬性里定義的新的函數(shù)擁有指向該對(duì)象的this關(guān)鍵值是需要通過(guò).bind()
方法來(lái)實(shí)現(xiàn)的讥脐,而現(xiàn)在我們可以通過(guò)箭頭函數(shù)來(lái)做到這些遭居。
var caculateObj = {
numbers:[1,2,3],
getNumbers: function () {
alert(this === caculateObj);//true
var insideFunc = () =>{
alert(this === caculateObj);//true
}
insideFunc();
return this.numbers;
}
}
caculateObj.getNumbers();
而且箭頭函數(shù)內(nèi)部的this值一旦被定義后是不能被修改的,即便調(diào)用.call()
,.apply()
也是不能夠修改的旬渠,而且箭頭函數(shù)也不能夠通過(guò)構(gòu)造函數(shù)調(diào)用來(lái)改變this值俱萍,因?yàn)榧^函數(shù)并不能夠作為構(gòu)造函數(shù)。
在使用箭頭函數(shù)的時(shí)候需要注意一個(gè)小細(xì)節(jié)告丢,那就是箭頭函數(shù)內(nèi)部的this值一定是其外部函數(shù)的this值枪蘑,所以箭頭函數(shù)被定義的地方是很重要的,比如下面這個(gè)例子
function Car(name,price) {
this.name = name;
this.price = price;
}
Car.prototype.showInfo = () =>{
alert(this === window);//true
}
var bus = new Car('Bus', 1000);
bus.showInfo();
雖然.showInfo()
這個(gè)函數(shù)是Car內(nèi)部的方法岖免,但是由于這個(gè)箭頭函數(shù)所被定義的地方并不是在函數(shù)內(nèi)部岳颇,而是在被定義在全局環(huán)境中,所以它內(nèi)部的this值其實(shí)是window對(duì)象颅湘。但是這種情況對(duì)于普通函數(shù)來(lái)說(shuō)并不會(huì)出現(xiàn)這個(gè)問(wèn)題话侧,這正是因?yàn)閯倓偽覀冋f(shuō)過(guò)的箭頭函數(shù)的this值不可改變性,對(duì)于一個(gè)普通函數(shù)來(lái)說(shuō)闯参,它內(nèi)部的this值是取決于調(diào)用方式的瞻鹏,當(dāng)以方法調(diào)用的形式來(lái)調(diào)用普通函數(shù)的時(shí)候术羔,普通函數(shù)的this也就是調(diào)用它的對(duì)象,所以箭頭函數(shù)所遇到的這種情況相對(duì)于普通函數(shù)來(lái)說(shuō)就不會(huì)發(fā)生乙漓。
總結(jié)
在JavaScript中this關(guān)鍵字一直是一個(gè)很重要的問(wèn)題,這次總結(jié)了這些也是之前的一些思考释移,如果大家有什么想法可以多多和我交流:)