??我們都知道Javascript中的「this」真的是個(gè)頭痛的東西,今兒我們就來(lái)好好總結(jié)下這個(gè)「this」。
??我記得之前這塊內(nèi)容,我直接先背了個(gè)「口訣」。
function fn() {
console.log(this);
}
// 1. fn(); // 指向window
// 2. fn(); // undefined(嚴(yán)格模式下)
// 3. a.b.c.fn() // 指向a.b.c
// 4. new fn() // 指向new的實(shí)例對(duì)象
// 5. () => { fn(); } // 箭頭函數(shù)調(diào)用fn,this指向「外層代碼庫(kù)的this」
之前了解的不多,今天來(lái)詳細(xì)解析一波。
首先我們來(lái)說(shuō)說(shuō)this的綁定規(guī)則。
(1)默認(rèn)綁定
(2)隱式綁定
(3)顯式綁定
(4)new綁定
以下全部在「瀏覽器環(huán)境」中。
(1)默認(rèn)綁定
??在不能應(yīng)用「其他綁定規(guī)則」的時(shí)候,使用「默認(rèn)綁定」,通常是作為「獨(dú)立函數(shù)」進(jìn)行調(diào)用。
function foo() {
console.log(this.name); // 'Jason'
}
var name = 'Jason';
foo();
??在調(diào)用「foo()」的時(shí)候,應(yīng)用了「默認(rèn)綁定」,this指向了全局對(duì)象window(非嚴(yán)格模式下),嚴(yán)格模式下,指向「undefined」。
(2)隱式綁定
??函數(shù)的調(diào)用是在「某個(gè)對(duì)象」上觸發(fā)的,即調(diào)用位置上存在「執(zhí)行上下文」。典型的形式如「xxx.fn()」。
var name = 'Jack';
function fn() {
console.log(this.name);
}
var obj = {
name: 'Jason',
foo: fn
};
obj.foo(); // 'Jason';
??函數(shù)fn的聲明在對(duì)象obj的外部,看起來(lái)是不屬于obj對(duì)象的,但是在調(diào)用foo的時(shí)候,隱式綁定會(huì)把函數(shù)調(diào)用中的「this」(foo函數(shù)中的this)綁定到對(duì)應(yīng)的「執(zhí)行上下文」(此例中的obj)。注意:對(duì)象屬性鏈只有最后一層會(huì)影響調(diào)用位置。
function fn() {
console.log(this.name);
}
let b = {
name: 'Jack',
foo: fn
}
let a = {
name: 'Jason',
friend: b
}
a.friend.foo();
??上面代碼中,foo函數(shù)的執(zhí)行環(huán)境不是a,而是「a.friend」,即是「b」。
??但是隱式綁定存在一個(gè)問(wèn)題:綁定丟失。我們來(lái)看下邊一段代碼:
function fn() {
console.log(this.name);
}
var name = 'Jack';
let obj = {
name: 'Jason',
sayName: fn,
}
let say = obj.sayName;
say(); // 'Jack'
??如果我們單看「obj.sayName」,執(zhí)行上下文是對(duì)象obj,但是我們把「obj.sayName」賦值給了變量say后,調(diào)用say()后,函數(shù)fn的執(zhí)行上下文就變?yōu)榱巳肿兞浚╳indow)中。
??針對(duì)這類(lèi)問(wèn)題,我們只要記住,形式為「xxx.fn()」才是隱式綁定,如果格式為「fn()」,前面什么都沒(méi)有,那肯定不是隱式綁定,但是也不一定是「默認(rèn)綁定」,下文中會(huì)解釋。
??除了上述的「綁定丟失」,還有一種綁定丟失的情況,就是發(fā)生在「回調(diào)函數(shù)」中,我們?cè)倏匆粋€(gè)例子。
function fn() {
console.log(this.name);
}
var person1 = {
name: 'Jason',
sayName: function() {
setTimeout(function() {
console.log('Hello!', this.name);
})
}
};
var person2 = {
name: 'Jack',
sayName: fn
};
var name = 'Tom';
person1.sayName(); // 'Hello! Tom'
setTimeout(person2.sayName, 100); // 'Tom'
setTimeout(function() {
person2.sayName(); // 'Jack'
}, 200);
我們依次說(shuō)說(shuō)每次輸出的原因。
- (1)setTimeout的回調(diào)函數(shù)中,this是「默認(rèn)綁定」,在非嚴(yán)格模式下,指向全局變量「window」,所以輸出「'Hello! Tom'」
- (2)第二條可能有些迷惑,不是說(shuō)格式為「xxx.fn()」就是隱式綁定嗎?然后執(zhí)行上下文就是對(duì)象xxx?其實(shí)是這樣的,對(duì)于setTimeout(fn, delay),第一個(gè)參數(shù)「fn」是「person2.sayName」,也就是說(shuō)我們把「person2.sayName」賦值給了fn,然后執(zhí)行了fn(),這樣就跟person2無(wú)關(guān)系。
- (3)第三條雖然也是在setTimeout函數(shù)中,但是我們可以看到執(zhí)行的是「 person2.sayName()」,所以是一個(gè)隱式綁定,因此函數(shù)的執(zhí)行上下文是person2,跟當(dāng)前的作用域無(wú)關(guān)系。
(3)顯式綁定
??顯式綁定主要是通過(guò)call,apply和bind來(lái)顯式的綁定this,call,apply和bind的第一個(gè)參數(shù)就是對(duì)應(yīng)的this對(duì)象,call和apply的作用一樣,只是call從第二個(gè)參數(shù)開(kāi)始依次傳入?yún)?shù),而apply是直接把所有參數(shù)集成為一個(gè)數(shù)組放到第二個(gè)參數(shù),bind會(huì)返回一個(gè)函數(shù),在正式執(zhí)行函數(shù)的時(shí)候,優(yōu)先調(diào)bind第二個(gè)后的參數(shù)。來(lái)看看代碼:
function fn() {
console.log(this.name);
}
var person = {
name: 'Jason',
sayName: fn
};
var name = 'Jack';
var sayName = person.sayName;
sayName.call(person); // 'Jason'
??上述代碼中,如果先不看最后一行,看倒數(shù)第二行「var sayName = person.sayName」,通過(guò)上述的講述,可以認(rèn)定到這一行,如果直接調(diào)用,所處的執(zhí)行上下文是全局變量window,但是最后一行的call函數(shù),指定了this的對(duì)象為person,則輸出‘Jason’。
??那么顯式綁定是不是會(huì)出現(xiàn)綁定丟失呢?看下面的代碼。
function fn() {
console.log(this.name);
}
var person = {
name: 'Jason',
sayName: fn
}
var name = 'Jack';
var Hi = function(fn) {
fn(); // 'Jack'
};
Hi.call(person, person.sayName);
??乍一看最后一行的顯式綁定,確實(shí)Hi函數(shù)的this綁定到了對(duì)象person上,Hi函數(shù)會(huì)接受一個(gè)參數(shù)fn,然后執(zhí)行fn();此刻這個(gè)參數(shù)fn是call的第二個(gè)參數(shù)「person.sayName」,但是在執(zhí)行fn的時(shí)候,相當(dāng)于直接調(diào)用了sayName函數(shù)(person.sayName賦值給了參數(shù)fn,隱式綁定也丟了,),對(duì)應(yīng)的是默認(rèn)綁定。
??那我們能不能繼續(xù)還是想要綁定到person上?
function fn() {
console.log(this.name);
}
var person = {
name: 'Jason',
sayName: fn
}
var name = 'Jack';
var Hi = function(fn) {
fn.call(this); // 'Jason'
};
Hi.call(person, person.sayName);
??其實(shí)只用在Hi函數(shù)中對(duì)fn使用call調(diào)用,因?yàn)閯倓偽覀冋f(shuō)了最后一句話,HI的this對(duì)象綁定到了person對(duì)象上,那么我們?cè)谡{(diào)用fn函數(shù)的時(shí)候再次顯示綁定一次this,此時(shí)「fn.call(this)」中的this就是Hi函數(shù)的this對(duì)象(person)。
(4)new綁定
??關(guān)于new會(huì)發(fā)生什么,可以具體看我之前的一篇文章(http://www.itdecent.cn/p/6ea91eb41283
)。
??簡(jiǎn)單來(lái)說(shuō):
(1)創(chuàng)建一個(gè)空對(duì)象,作為將要返回的對(duì)象實(shí)例。
(2)將這個(gè)空對(duì)象的原型,指向構(gòu)造函數(shù)的「prototype」屬性。
(3)將這個(gè)空對(duì)象賦值給構(gòu)造函數(shù)內(nèi)的「this」關(guān)鍵字。
(4)開(kāi)始執(zhí)行構(gòu)造函數(shù)內(nèi)的代碼。
function Person(name) {
this.name = name;
}
var x = new Person('Jason');
console.log(x.name) // 'Jason'
(5)綁定例外
??上述(1)~(4)已經(jīng)基本參數(shù)了this綁定的規(guī)則,但是我們還是要補(bǔ)充點(diǎn)可能存在的問(wèn)題。比如「綁定例外」。
??如果我們?cè)谑褂谩革@示綁定」的時(shí)候,第一個(gè)參數(shù)傳了「null」或者「undefined」,這些值是會(huì)被忽略的,實(shí)際上應(yīng)用的是「默認(rèn)綁定」。
(6)綁定優(yōu)先級(jí)
new綁定 > 顯式綁定 > 隱式綁定 > 默認(rèn)綁定
(7)箭頭函數(shù)
??箭頭函數(shù)是ES6中的語(yǔ)法,帶來(lái)了很多的便利,但是我們也有幾個(gè)重要的注意點(diǎn):
(1)函數(shù)體內(nèi)的this對(duì)象,就是定義時(shí)所在的對(duì)象,而不是使用時(shí)所在的對(duì)象。
(2)不可以當(dāng)作構(gòu)造函數(shù),也就是說(shuō),不可以使用new命令,否則會(huì)拋出一個(gè)錯(cuò)誤。
(3)不可以使用arguments對(duì)象,該對(duì)象在函數(shù)體內(nèi)不存在。如果要用,可以用 rest 參數(shù)代替。
(4)不可以使用yield命令,因此箭頭函數(shù)不能用作 Generator 函數(shù)。
??其中第一點(diǎn)就跟本文的主題this密切相關(guān),我們來(lái)看兩個(gè)例子。
var name = 'Jason';
var obj = {
name: 'Jack',
sayName: function() {
console.log(this.name); // 'Jack'
}
};
obj.sayName();
var name = 'Jason';
var obj = {
name: 'Jack',
sayName: () => {
console.log(this.name); // 'Jason'
}
};
obj.sayName();
??上述兩個(gè)例子非常相似,唯一不同點(diǎn)在于obj的sayName函數(shù)一個(gè)使用了普通函數(shù)形式(第一個(gè)例子),另一個(gè)使用了箭頭函數(shù)(第二個(gè)例子),然后輸出結(jié)果就不同了。
??總的來(lái)說(shuō):普通函數(shù)的this是函數(shù)「運(yùn)行時(shí)」綁定的,而箭頭函數(shù)的this是函數(shù)「定義時(shí)」綁定的。
??這里我們?cè)趺蠢斫?strong>「定義時(shí)」?我們可以說(shuō)箭頭函數(shù)的this和其外層代碼庫(kù)的this一樣,或者說(shuō)指向其父級(jí)執(zhí)行上下文中的this。
上述第二個(gè)例子中,箭頭函數(shù)本身跟sayName平級(jí)以key:value的形式,也就是說(shuō)箭頭函數(shù)本身存在于obj對(duì)象上,而obj對(duì)象所處的環(huán)境在全局變量window上,所以此例中的this.name其實(shí)是window.name。我們?cè)倏磧蓚€(gè)例子。
var name = 'Jason';
function Test() {
this.name = 'Jack';
let fn = function() {
console.log(this.name); // 'Jason'
};
fn();
}
var x = new Test();
var name = 'Jason';
function Test() {
this.name = 'Jack';
let fn = () => {
console.log(this.name); // 'Jack'
};
fn();
}
var x = new Test();
??第一個(gè)例子中,一個(gè)普通函數(shù)賦值給了變量fn,然后直接調(diào)用fn(),是默認(rèn)綁定,此時(shí)的this指向全局變量window,所以this.name就是window.name,輸出‘Jason’。
??第二個(gè)例子中,我們把箭頭函數(shù)賦值為了變量fn,箭頭函數(shù)中的this指向了父級(jí)執(zhí)行上下文中的this,父級(jí)執(zhí)行上下文中的this是通過(guò)new指向了其構(gòu)造函數(shù)的實(shí)例(本例中的x),然后Test函數(shù)中第一句的「this.name = 'Jack'」,使得x.name為Jack。
??我們?cè)賮?lái)一個(gè)例子練練手。
var obj = {
hi: function(){
console.log('1', this); // obj
return ()=>{
console.log('2',this); // obj
}
},
sayHi: function(){
return function() {
console.log('3', this); // window
return ()=>{
console.log('4', this); // window
}
}
},
say: ()=>{
console.log('5', this); // window
}
}
let hi = obj.hi();
hi();
let sayHi = obj.sayHi();
let fun1 = sayHi();
fun1();
obj.say();
??我們來(lái)依次解釋一下:
(1)第一條(1處)是由于「obj.hi()」調(diào)用函數(shù),此時(shí)是隱式綁定,固然1處的this指向了obj。
(2)第2處,由于調(diào)用「hi()」,obj.hi()返回的是一個(gè)箭頭函數(shù),這個(gè)箭頭函數(shù)中的this與外層代碼庫(kù)的this一樣,外層代碼庫(kù)就是obj對(duì)象中的fn屬性(這是一個(gè)函數(shù)),于是乎,跟1處的this是一樣的,都指向obj。
(3)由于「obj.sayHi()」賦值給了變量sayHi,「sayHi」執(zhí)行的時(shí)候,相當(dāng)于原本是隱式綁定然后變?yōu)槟J(rèn)綁定,固然3處輸出的是window。
(4)由于sayHi()賦值給了fun1,在「fun1()」執(zhí)行的時(shí)候,執(zhí)行的是一個(gè)箭頭函數(shù),然后我們就找這個(gè)箭頭函數(shù)的外層代碼庫(kù)的this,就與3處的this相同,即輸出window。
(5)由于「obj.say()」的執(zhí)行,乍一看是一個(gè)隱式綁定,但是看到函數(shù)是箭頭函數(shù),obj的外層代碼庫(kù)所在的環(huán)境就是全局變量window,輸出window。
那么箭頭函數(shù)一定是靜態(tài)的嗎?
var obj = {
hi: function(){
console.log('1', this);
return ()=>{
console.log('2', this);
}
},
sayHi: function(){
return function() {
console.log('3', this);
return ()=>{
console.log('4', this);
}
}
},
say: ()=>{
console.log('5', this);
}
}
let sayHi = obj.sayHi();
let fun1 = sayHi(); // window
fun1(); // window
let fun2 = sayHi.bind(obj)(); // obj
fun2(); // obj
??還是用上面的例子,我們看看fun1和fun2,第一次執(zhí)行「sayHi()」賦值給fun1的時(shí)候,是由隱式綁定轉(zhuǎn)換為了默認(rèn)綁定,this為window,執(zhí)行「fun1」的時(shí)候,是執(zhí)行箭頭函數(shù),所以箭頭函數(shù)里的this也是window。
??但是對(duì)于fun2, 「sayHi.bind(obj)()」中使用bind顯示綁定了sayHi的this對(duì)象為obj,本來(lái)沒(méi)有bind綁定的時(shí)候,是由隱式綁定轉(zhuǎn)變?yōu)槟J(rèn)綁定,然后此處我們又強(qiáng)行使用bind綁定回來(lái)了。所以「sayHi.bind(obj)()」執(zhí)行的時(shí)候,3處的this為obj,然后「fun2()」執(zhí)行的時(shí)候,4處的this和3處的this一樣都是obj。
我們來(lái)一個(gè)終極題目來(lái)綜合一下。
var number = 5;
var obj = {
number: 3,
fn: (function () {
var number;
this.number *= 2;
number = number * 2;
number = 3;
return function () {
var num = this.number;
this.number *= 2;
console.log(num);
number *= 3;
console.log(number);
}
})()
}
var myFun = obj.fn;
myFun.call(null);
obj.fn();
console.log(window.number);
??我們來(lái)分析下這段代碼,在obj對(duì)象的fn屬性定義的時(shí)候,就是一個(gè)「立即執(zhí)行函數(shù)」,并且?guī)в小搁]包」,我們看看這個(gè)「立即執(zhí)行函數(shù)」中的this指向了誰(shuí)?沒(méi)有new綁定,沒(méi)有顯式綁定,沒(méi)有隱式綁定,自然就是默認(rèn)綁定了,this指向了全局變量window。所以「立即執(zhí)行函數(shù)」中的代碼可以這樣理解。
var number; // number是undefined
window.number *= 2; // 此時(shí)全局變量中的number變?yōu)?10
number = number * 2; // 由于number是undefined, Number(undefined)為NaN,此處number變?yōu)镹aN。
number = 3; // 然后又使變量number變?yōu)?
執(zhí)行完fn的立即執(zhí)行函數(shù)后。obj的fn屬性是下列這樣的:
fn: function() {
var num = this.number;
this.number *= 2;
console.log(num);
number *= 3;
console.log(number);
}
然后我們執(zhí)行「myFun.call(null)」,這種顯式綁定我們?cè)谏鲜鑫恼轮姓f(shuō)過(guò),如果第一個(gè)參數(shù)為null,就是轉(zhuǎn)為默認(rèn)綁定。本例中就是執(zhí)行obj的fn函數(shù),然后我們?cè)賮?lái)每一行分析一波:我們首先想想obj的fn函數(shù)里面的this是誰(shuí)?由于是默認(rèn)綁定所以是window,于是乎。
var num = this.number; // 由于this指向了window,此時(shí)window.number為10,則num被賦值為10
this.number *= 2; // 使得全局變量的「number」,變?yōu)?0
console.log(num); // 輸出10
number *= 3; 這個(gè)number是obj.fn中的閉包函數(shù)中的「number」
console.log(number) ; // 同時(shí)輸出這個(gè)number

接著我們執(zhí)行「obj.fn()」,還是優(yōu)先執(zhí)行了obj.fn的立即執(zhí)行函數(shù)。立即執(zhí)行函數(shù)的this還是指向全局變量window,然后依次分析每行代碼。立即執(zhí)行函數(shù)中:
var number;
this.number *= 2; // 等價(jià)于 window.number *= 2; 使得全局變量的number變?yōu)?0
number = number * 2; // NaN
number = 3;
然后我們?cè)趫?zhí)行「立即執(zhí)行函數(shù)」返回的函數(shù)。此時(shí)由于是「obj.fn()」這樣調(diào)用,所以this指向了obj對(duì)象。
var number = this.number; // 3,即obj.number
this.number *= 2; // 使得obj.number變?yōu)?
console.log(number); // 輸出3
number *= 3; // 此時(shí)number還是指閉包中的那個(gè)「number」,剛剛number是9,現(xiàn)在就是27了
console.log(number); // 就是輸出閉包那個(gè)「number」,是27
最后執(zhí)行「console.log(window.number)」,此時(shí)全局變量的number是20,輸出20.
參考: