寫在最前:文章轉(zhuǎn)自掘金
this
在JavaScript
中是非常重要的概念粉洼,因?yàn)槲覀冇玫剿念l率非常之高凌唬,在享受到它的便利性的同時(shí),與之對應(yīng)的是它的綁定規(guī)則比較難理解独悴。今天我們就來好好總結(jié)一下this
的相關(guān)知識點(diǎn)蹬昌,這樣在使用它的過程中就能更有自信和把握啦混驰!
this
是什么
我們先來看一個(gè)例子:
function foo() {
console.log(this); //Window
}
foo();
我們定義了一個(gè)函數(shù)foo
并執(zhí)行,在函數(shù)體中我們直接打印輸出this
皂贩,發(fā)現(xiàn)結(jié)果并不是undefined
,而是Window
對象栖榨。這是為什么呢?這就引出了相關(guān)的概念:
this是一個(gè)很特別的關(guān)鍵字先紫,被自動(dòng)定義在所有函數(shù)的作用域中治泥。
1. this
是一個(gè)關(guān)鍵字
這句話說出了this
的本質(zhì)其實(shí)是一個(gè)關(guān)鍵字,只不過有些特別遮精,具體特別在哪里我們下面再介紹。既然this
是一個(gè)關(guān)鍵詞败潦,我們就得注意它應(yīng)該具備的一個(gè)特點(diǎn)了:this
無法被重寫本冲。我們修改一下剛剛的例子試試:
function foo() {
this = "null"; //Uncaught SyntaxError
console.log(this);
}
foo();
可以看到,this
的值無法被修改劫扒,否則會(huì)報(bào)錯(cuò)檬洞。
2. this
被自動(dòng)定義在所有函數(shù)的作用域中
這句話里面包含很多信息點(diǎn),首先就是自動(dòng)定義這個(gè)說法沟饥,指出了this
這個(gè)關(guān)鍵詞在函數(shù)作用域中會(huì)被自動(dòng)定義添怔,這也就是為什么我們剛剛在foo
函數(shù)中可以直接輸出this
得到Window
對象湾戳。
其次是所有函數(shù)的作用域中,這句話說明了广料,在所有的的函數(shù)作用域中都存在this
關(guān)鍵字砾脑。但這不是說只有在函數(shù)中才有this
,假如我們試著在全局直接輸出this
會(huì)是什么結(jié)果呢艾杏?
console.log(this); //Window
可以看到韧衣,同樣輸出了Window
對象,這說明在全局作用域下同樣存在this
關(guān)鍵詞购桑。
看到這里你可能有點(diǎn)疑問了畅铭,為什么舉得例子中,this
的值都是Window
對象勃蜘,是不是this
的值就是等于Window
對象呢硕噩?我們再看個(gè)例子:
var obj = {
say: function () {
console.log(this);
},
};
obj.say(); //{say: ?}
我們將例子修改了一下,發(fā)現(xiàn)此時(shí)輸出的this
值變成了obj
對象缭贡,這說明this
的值并不是之前猜測的固定等于Window
對象榴徐,那么this
值到底是怎么來的呢?
this
的指向值
事實(shí)上匀归,我們之前在介紹執(zhí)行上下文的時(shí)候有介紹到坑资,在動(dòng)態(tài)創(chuàng)建執(zhí)行上下文時(shí),會(huì)確認(rèn)This Binding
也就是this
的值穆端,它和函數(shù)定義的位置無關(guān)袱贮,而是由函數(shù)調(diào)用時(shí)的綁定規(guī)則決定的。
這也就是為什么体啰,在全局的情況下this
值也是存在的攒巍,正是因?yàn)榇嬖谌稚舷挛?而且this
的值就存儲在這個(gè)上下文中。
那么this
值具體指向誰荒勇,函數(shù)調(diào)用時(shí)的綁定規(guī)則是什么柒莉,這就是我們接下來要講的重頭戲了。
this
的綁定規(guī)則
我們剛剛講了沽翔,this
的指向值由函數(shù)調(diào)用時(shí)的綁定規(guī)則決定兢孝,我們現(xiàn)在就來講講這些綁定規(guī)則分別有哪些。
默認(rèn)綁定
默認(rèn)綁定是最基本的綁定規(guī)則仅偎,它被應(yīng)用在其他規(guī)則均不適用的情況下跨蟹,因此也是最常見的綁定規(guī)則。默認(rèn)綁定比較典型的一種判斷就是:當(dāng)使用不帶任何修飾的函數(shù)引用進(jìn)行調(diào)用時(shí)橘沥,只能使用默認(rèn)綁定窗轩,而不能使用其他綁定規(guī)則。
舉個(gè)例子:
function foo(){
console.log(this); //Window
}
foo();
可以看到座咆,這里的foo()
就是不帶任何修飾的函數(shù)調(diào)用痢艺,foo
前面光禿禿的啥也沒有仓洼。另外你會(huì)發(fā)現(xiàn),這里輸出的this值為Window
對象堤舒,那大家就已經(jīng)知道了這可能就是默認(rèn)規(guī)則下this的指向值色建,不過不準(zhǔn)確,事實(shí)上默認(rèn)規(guī)則下this
的指向值可以分幾種情況:
· 嚴(yán)格模式
在嚴(yán)格模式下植酥,使用默認(rèn)規(guī)則得到的this
值會(huì)指向undefined
镀岛,可以看代碼:
'use strict'
function foo(){
console.log(this); //undefined
}
foo();
· 非嚴(yán)格模式
非嚴(yán)格模式下,如果應(yīng)用了默認(rèn)規(guī)則友驮,那么this
的值會(huì)指向Window
對象漂羊,這也就是為什么我們剛剛舉得好幾個(gè)例子this
值都是Window
。
· 嚴(yán)格模式和非嚴(yán)格模式混用
這種模式比較特殊卸留,如果我們在非嚴(yán)格模式下定義了函數(shù)走越,又在嚴(yán)格模式下調(diào)用了函數(shù),最終this
的值還是會(huì)指向Window
對象耻瑟,看下代碼:
function foo() {
console.log(this); //Window
}
{
("use strict");
foo();
}
隱式綁定
講完了默認(rèn)的綁定規(guī)則旨指,那么肯定要看看一些特殊的綁定規(guī)則了。當(dāng)函數(shù)作為某個(gè)對象的方法調(diào)用時(shí)喳整,此時(shí)這個(gè)對象就是函數(shù)的上下文對象谆构,這時(shí)候this
會(huì)指向這個(gè)對象,我們來看個(gè)例子:
var obj = {
foo: function () {
console.log(this); //{foo: ?}
},
};
obj.foo();
可以看到框都,我們給對象obj
定義了一個(gè)方法foo
搬素,并通過obj.foo()
的方式進(jìn)行調(diào)用,此時(shí)的obj
對象就是函數(shù)foo
調(diào)用時(shí)的上下文對象魏保,因此this
會(huì)指向obj
對象熬尺,我們把這種情況稱為隱式綁定。隱式綁定有個(gè)重要的點(diǎn)需要著重說明一下:
隱式丟失
隱式綁定的函數(shù)谓罗,在有些情況下會(huì)丟失綁定的上下文對象粱哼,這時(shí)候就會(huì)應(yīng)用我們的默認(rèn)綁定規(guī)則,把this
指向Window
對象(非嚴(yán)格模式)或者undefined
(嚴(yán)格模式)檩咱。我們先來看一個(gè)例子:
var name = "我是全局的name";
var obj = {
name: "我是obj的name",
foo: function () {
console.log(this.name); //我是全局的name
},
};
let fn = obj.foo; //這里用變量fn保存foo函數(shù)
fn();
這里我們用一個(gè)變量fn
保存了obj
下的foo
函數(shù)的引用揭措,此時(shí)調(diào)用以后輸出的是全局對象下定義的name
變量值,可見此時(shí)的this
對象指向了Window
税手。
當(dāng)我們執(zhí)行let fn = obj.foo
時(shí)蜂筹,實(shí)際上是將fn
指向了函數(shù)foo
的地址,因此當(dāng)我們執(zhí)行fn()
的時(shí)候?qū)嶋H上就是在執(zhí)行foo()
芦倒,上面的代碼也就是這樣:
var name = "我是全局的name";
function foo() {
console.log(this.name); //我是全局的name
}
foo();
這時(shí)候?qū)嶋H上就是不帶任何修飾的函數(shù)調(diào)用,因此會(huì)應(yīng)用默認(rèn)綁定規(guī)則不翩。
隱式丟失還有一種情況兵扬,就是對象的方法作為參數(shù)傳遞到另外的函數(shù)中去麻裳,來看個(gè)例子:
var name = "我是全局的name";
var obj = {
name: "我是obj的name",
foo: function () {
console.log(this.name); //我是全局的name
},
};
function otherFn(fn) {
fn();
}
otherFn(obj.foo);
這個(gè)例子和上個(gè)例子唯一的不同就是在后面,我們不是通過變量保存foo
函數(shù)后再調(diào)用器钟,而是把foo
函數(shù)作為參數(shù)傳遞給了另一個(gè)函數(shù)津坑,然后在另一個(gè)函數(shù)中調(diào)用,這里為什么也發(fā)生了隱式丟失呢傲霸?
答案是疆瑰,參數(shù)在傳遞給函數(shù)的時(shí)候,會(huì)發(fā)生一個(gè)賦值操作昙啄,也就是將實(shí)參賦值給形參穆役,函數(shù)部分的代碼可以看成這樣:
function otherFn() {
var fn = obj.foo; //這里實(shí)際上就是將實(shí)參賦值給形參
fn();
}
otherFn(obj.foo);
這下我們就發(fā)現(xiàn)了,這種情況和上面那種隱式丟失的情況一樣梳凛,也是不帶任何修飾的函數(shù)調(diào)用耿币,因此也應(yīng)用了默認(rèn)規(guī)則,發(fā)生了隱式丟失韧拒。
對象屬性引用鏈
我們剛剛說到淹接,當(dāng)函數(shù)作為對象的方法調(diào)用時(shí),會(huì)以這個(gè)對象作為上下文對象叛溢,所以this
會(huì)指向這個(gè)對象塑悼,那么當(dāng)多個(gè)對象嵌套在一起后調(diào)用方法以后this
會(huì)指向哪個(gè)對象呢?我們來看個(gè)例子:
var obj1 = {
name: "obj1",
obj2: {
name: "obj2",
obj3: {
name: "obj3",
foo: function () {
console.log(this.name); //obj3
},
},
},
};
obj1.obj2.obj3.foo();
是不是看蒙了楷掉?不要怕厢蒜,我們看到結(jié)果輸出了obj3
,事實(shí)上當(dāng)我們通過對象屬性調(diào)用鏈來調(diào)用方法時(shí)靖诗,最終起作用的只有函數(shù)前面的那個(gè)調(diào)用對象郭怪。比如剛剛的obj1.obj2.obj3.foo()
,實(shí)際上可以看成obj3.foo()
刊橘。同理鄙才,哪怕是...a.b.c.d.e.foo()
,我們只要看最后的e.foo()
即可促绵。
顯示綁定
我們剛剛介紹了隱式綁定攒庵,this的指向值完全由調(diào)用對象決定,十分難把控败晴,有沒有一個(gè)辦法浓冒,不管我是通過哪個(gè)對象調(diào)用的,我都可以指定我想要的this指向值呢尖坤?有的稳懒,在函數(shù)的原型上有兩個(gè)方法call和apply,可以傳入指定的對象作為this的指向值慢味,不過兩者有一些區(qū)別场梆,我們來分別看下用法墅冷。
call的用法
call()
方法使用一個(gè)指定的this
值和單獨(dú)給出的一個(gè)或多個(gè)參數(shù)來調(diào)用一個(gè)函數(shù)。
注意:該方法的語法和作用與apply()
方法類似或油,只有一個(gè)區(qū)別寞忿,就是call()
方法接受的是一個(gè)參數(shù)列表,而apply()
方法接受的是一個(gè)包含多個(gè)參數(shù)的數(shù)組顶岸。
我們看看call
如何顯式的改變this
的指向值:
var obj = {
name: "obj",
};
function foo() {
console.log(this.name); //obj
}
foo.call(obj);
foo
函數(shù)調(diào)用call
方法腔彰,并傳入obj
作為第一個(gè)參數(shù),obj
會(huì)作為foo
函數(shù)的this
指向值辖佣,并執(zhí)行函數(shù)霹抛。那么call
方法是如何實(shí)現(xiàn)這樣的功能的呢?我們下面再講凌简,我們先來看下apply
上炎。
apply的用法
apply
的用法和效果和call
幾乎一樣,看例子就可以看出來:
var obj = {
name: "obj",
};
function foo() {
console.log(this.name); //obj
}
foo.apply(obj);
從這兩個(gè)例子來看的話雏搂,兩者幾乎完全相同藕施,但它們還是有區(qū)別的,區(qū)別就在于它們接收參數(shù)的方式不同凸郑,我們看個(gè)例子:
var obj = {
name: "obj",
};
function foo(a,b,c) {
console.log(this.name,a,b,c); //obj 1 2 3
}
foo.call(obj,1,2,3);
foo.apply(obj,[1,2,3]);
可以看到裳食,call
函數(shù)接收多個(gè)參數(shù)并傳入foo
函數(shù)中,而apply
函數(shù)接收的參數(shù)放在了一個(gè)數(shù)組里芙沥,然后拆分開來傳入了foo
函數(shù)中诲祸,這就是兩者的區(qū)別。
我們通過調(diào)用call
或者apply
的方式顯式的綁定了this
的指向值而昨,但是你會(huì)發(fā)現(xiàn)通過call
或者apply
的方式會(huì)直接調(diào)用函數(shù)救氯,就沒辦法把函數(shù)連帶著this
一起傳到別的函數(shù)中存儲或者執(zhí)行。能不能給函數(shù)先綁定好了this
值歌憨,再套個(gè)殼子包裝好着憨,這樣就可以傳來傳去的還保留著綁定好的this
值了呢?于是我們的bind
函數(shù)就應(yīng)運(yùn)而生了务嫡。
bind的用法
bind
函數(shù)可以傳入指定對象作為this
的指向?qū)ο蠹锥叮酥猓?code>bind函數(shù)還可以接受參數(shù)并存儲起來,下次調(diào)用的時(shí)候可以只傳入剩余的參數(shù)心铃,我們來看個(gè)例子:
//還是剛剛隱式丟失的例子准谚,我們使用bind函數(shù)綁定上obj對象看看
var name = "我是全局的name";
var obj = {
name: "我是obj的name",
foo: function () {
console.log(this.name);
},
};
function otherFn(fn) {
//會(huì)發(fā)現(xiàn)函數(shù)被傳入進(jìn)來以后,this依然指向obj而沒有發(fā)生隱式丟失
fn(); //我是obj的name
}
otherFn(obj.foo.bind(obj)); //我們這里不直接傳入去扣,而是把obj.foo包裝一下再傳入
從這個(gè)例子我們可以看到柱衔,通過bind
綁定了this
指向值的函數(shù),即使傳入了其他函數(shù)中執(zhí)行也不會(huì)丟失this
對象。我們再舉個(gè)例子看看秀存,如何在綁定this
的同時(shí)存儲參數(shù):
function foo(a, b, c) {
console.log(a, b, c);
}
//第一個(gè)參數(shù)為null時(shí)捶码,非嚴(yán)格模式下會(huì)以Window對象作為this指向值羽氮,后面會(huì)介紹
//給foo函數(shù)套了層殼子或链,并存儲了兩個(gè)參數(shù)1,2
var bindFoo = foo.bind(null, 1, 2);
//當(dāng)調(diào)用這個(gè)包裝函數(shù)的時(shí)候,傳入的參數(shù)會(huì)連同之前存儲的參數(shù)一起傳給foo函數(shù)
bindFoo(3); //1 2 3
這樣我們就實(shí)現(xiàn)了預(yù)傳參數(shù)和this
值档押,可以方便傳值的函數(shù)啦~
還有一種情況澳盐,我覺得這種顯示綁定的方式太僵硬了,其實(shí)我想要一種更靈活的綁定方式令宿,我想預(yù)設(shè)一個(gè)this
指向?qū)ο蟮鸢遥?dāng)我不小心應(yīng)用了默認(rèn)綁定規(guī)則,this
指向了Window
或者undefined
時(shí)候把this重新指向我預(yù)設(shè)的對象粒没,否則的話就指向他本來的this
對象筛婉,這樣可以嗎?好家伙癞松,要求還不少爽撒,但是我滿足你了,那就是我們的softBind
啦~
softBind的用法
softBind
函數(shù)在函數(shù)原型上并不存在响蓉,是后來創(chuàng)造的硕勿,顧名思義就是軟綁定,為了實(shí)現(xiàn)我們剛剛說的需求而出現(xiàn)的枫甲。 既然原型上沒有源武,自然要介紹一下怎么定義實(shí)現(xiàn)的啦:
Function.prototype.softBind = function (obj) {
//先拿到調(diào)用softBind的函數(shù)本身
var fn = this;
//這里是為了拿到傳入的其他參數(shù),并存儲起來
var curried = [].slice.call(arguments, 1); //arguments是類數(shù)組所以沒有slice方法
//這里是返回的包裝好的函數(shù)
var bound = function () {
//判斷this的情況想幻,這里的this是返回的封裝函數(shù)執(zhí)行時(shí)的this粱栖,和調(diào)用softBind函數(shù)時(shí)的this不同
var that = (!this || this === (window || global)) ? obj : this; //判斷this是否空,同時(shí)考慮node環(huán)境
//這里的目的是為了把包裝時(shí)傳入的參數(shù)脏毯,和執(zhí)行包裝函數(shù)時(shí)傳入的參數(shù)進(jìn)行合并闹究,arguments和之前的arguments不同
var newArguments = [].concat.apply(curried, arguments);
//這里其實(shí)就是調(diào)用函數(shù)
return fn.apply(that, newArguments);
};
//有一個(gè)細(xì)節(jié)是調(diào)整包裝好的函數(shù)的原型鏈,使得instanceof能夠用于包裝好的函數(shù)的判斷
bound.prototype = Object.create(fn.prototype);
return bound;
};
看不懂的話慢慢琢磨一下抄沮,然后我們來舉個(gè)例子看看它的用法:
var name = "我是全局的name";
var obj1 = {
name: "我是obj1的name"
};
var obj2 = {
name: "我是obj2的name"
};
function foo(){
console.log(this.name)
}
//包裝一個(gè)默認(rèn)this為obj1的函數(shù)
var fn=foo.softBind(obj);
//當(dāng)通過obj2調(diào)用時(shí)跋核,會(huì)使用obj2作為this值
fn.call(obj2); //我是obj2的name
//當(dāng)不加修飾符調(diào)用時(shí),會(huì)應(yīng)用綁定的this值
fn(); //我是obj1的name
new綁定
new
操作符也可以實(shí)現(xiàn)改變this
指向,關(guān)于new操作符的知識點(diǎn):
- 創(chuàng)建一個(gè)新對象叛买,將this綁定到新創(chuàng)建的對象
- 使用傳入的參數(shù)調(diào)用構(gòu)造函數(shù)
- 將創(chuàng)建的對象的proto_指向構(gòu)造函數(shù)的prototype
- 如果構(gòu)造函數(shù)沒有顯式返回一個(gè)對象砂代,則返回創(chuàng)建的新對象,否則返回顯式返回的對象(即手動(dòng)返回的對象)
然后我們來看個(gè)例子:
function Foo(name){
this.name=name;
}
let person=new Foo('xiaowang');
console.log(person); //xiaowang
可以看到率挣,當(dāng)函數(shù)作為構(gòu)造函數(shù)執(zhí)行new
的過程中刻伊,this
指向了最終創(chuàng)建的實(shí)例person
,說明new
操作符確實(shí)能夠改變this
的指向。
箭頭函數(shù)綁定
除了之前介紹的那么多種捶箱,還存在著一種ES6中的特殊函數(shù)類型:箭頭函數(shù)智什。箭頭函數(shù)中的this
比較特殊,它的指向值不是動(dòng)態(tài)決定的丁屎,而是由函數(shù)定義時(shí)作用域中包含的this
值確定的荠锭,我們舉個(gè)例子:
//定義一個(gè)箭頭函數(shù)
var foo = () => {
console.log(this.name);
};
var name = "我是全局的name";
var obj1 = {
name: "我是obj1的name",
};
foo.call(obj1); // "我是全局的name"
可以看到,雖然我們調(diào)用了call
傳入了obj1
晨川,但最終輸出的值還是全局的name
证九,這是因?yàn)楹瘮?shù)foo
定義在全局中,因此this
會(huì)指向window
對象共虑。
不管在什么情況下,箭頭函數(shù)的this
跟外層function
的this
一致愧怜,外層function
的this
指向誰,箭頭函數(shù)的this
就指向誰妈拌,如果外層不是function
則指向window
拥坛。
綁定規(guī)則的優(yōu)先級
當(dāng)多個(gè)綁定規(guī)則同時(shí)運(yùn)用的時(shí)候,會(huì)使用優(yōu)先級更高的綁定規(guī)則尘分。我們按照由低到高進(jìn)行排序的話就是:
- 默認(rèn)綁定
2.隱式綁定
3.顯示綁定
4.new操作符綁定猜惋、箭頭函數(shù)
接下來我們分別看幾個(gè)例子來驗(yàn)證它們的優(yōu)先級:
- 默認(rèn)綁定和隱式綁定
var obj = {
foo: function () {
console.log(this); //{foo: ?}
},
};
obj.foo();
- 隱式綁定和顯示綁定
var obj1 = {
name: "obj1",
foo: function () {
console.log(this.name);
},
};
var obj2 = {
name: "obj2",
};
obj1.foo.apply(obj2); //obj2
obj1.foo.call(obj2); //obj2
obj1.foo.bind(obj2)(); //obj2
- 顯示綁定和new操作符綁定
由于call
和apply
只能執(zhí)行函數(shù),沒法和new
操作符一起使用音诫,因此我們只對比bind
函數(shù)和new
操作符的優(yōu)先級惨奕。
var obj = {};
function foo() {
this.name = "obj";
}
//將foo綁定到obj上
var fn = foo.bind(obj);
//執(zhí)行new操作
var f1 = new foo();
console.log(obj.name); //undefined
console.log(f1.name); //obj
可以看到,foo
已經(jīng)顯式綁定obj
對象了竭钝,最終name
值還是賦值到了實(shí)例f1
上梨撞,因此new
操作符綁定的優(yōu)先級是大于顯式綁定(bind
)的。
你可能會(huì)疑惑香罐,bind
明明為foo
函數(shù)套了層殼卧波,按照new操作符的邏輯怎么都不能把里面的this
指向改了才對,事實(shí)上bind
函數(shù)內(nèi)部做了判斷庇茫,如果和new
操作符一起使用的話港粱,要把this
讓給new
操作符的對象,這也坐實(shí)了它們之間優(yōu)先級的關(guān)系了旦签。
- 箭頭函數(shù)綁定和new操作符綁定
由于箭頭函數(shù)是不可構(gòu)造的查坪,所以無法和new
操作符組合剑肯,因此我把他們放在了同級媒役。