你可能不知道的JavaScript之this指向詳解
前言
JavaScript 對 this 指向 話題 的理解是永不過時的,鑒于 JavaScript 中 this 風騷的運作方式,本文將試圖將其拆解分析,烹飪再食用~。
this is all about context.,大概意思就是:this 與當前執(zhí)行上下文相關。
this 說白了就是找離自己最近的對象,即擁有當前上下文(context)的對象(context object)。
換句話說,this 與函數被調用時,調用函數的對象有關。
默認綁定,全局對象
正所謂近水樓臺先得月,全局對象作為對遙遠的對象是作為備胎的存在,為語言邊界護城河做兜底。
一般情況下,this 指向全局對象則屬于默認綁定。那么什么是默認綁定呢?
this 默認綁定,通俗地可理解為函數被調用時無任何調用前綴對象的情景,由于函數調用時無調用前綴對象或函數無特定綁定,所以非嚴格模式下此時 this 會指向全局對象。
在非嚴格模式下,不同終端的全局變量對象有所區(qū)別:
• 在瀏覽器端,this 指向 Window 對象;
• 在 Nodejs 環(huán)境,this 指向 global 對象;
• 在函數環(huán)境,this 指向 綁定當前函數的作用域;
在嚴格模式下:
• 在 use strict 環(huán)境, this 指向 undefined;
?? 在非嚴格模式下
{
/* 在非嚴格模式下,this默認綁定 */
console.log('window global this: ', this); // window
function fnOuter() {
console.log('fnOuter: ', this); // window
}
function windowThis() {
console.log('windowThis: ', this); // window
function fnInner() {
console.log('fnInner: ', this); // window
fnOuter();
}
fnInner();
}
windowThis();
}
上述栗子中,無論函數聲明在哪,在哪調用,由于函數調用時前面并未指定任何對象,這種情況下 this 均指向全局對象 window。
但須注意的是,在嚴格模式下,默認綁定下的 this 會指向 undefined。
?? 在嚴格模式下,再來看幾個栗子,然后在心中記下答案
{
/* 在非嚴格模式下,this默認綁定 */
var mode = '在非嚴格模式下,this默認綁定';
function windowThis() {
console.log('windowThis: ', this);
console.log('windowThis: ', this.mode);
function fnInner() {
console.log('fnInner: ', this);
console.log('fnInner: ', this.mode);
}
fnInner();
}
function windowStrictThis() {
'use strict';
windowThis();
function fnInner() {
console.log('windowStrictThis: ', this);
console.log('windowStrictThis: ', this.mode);
}
fnInner();
}
windowStrictThis();
}建議得出答案再看下文,
一起來倒數吧,“花栗鼠。。。。。。”。
??????? ??????? ??????? ??????? ??????? ??????? ??????? ??????? ???????
好啦,來看正確輸出吧,都答對了吧~
// windowThis: Window{}
// windowThis: 在非嚴格模式下,this默認綁定
// fnInner: Window{}
// fnInner: 在非嚴格模式下,this默認綁定
// windowStrictThis: undefined
// windowStrictThis: TypeError: Cannot read property 'mode' of undefined
可見在函數內部使用嚴格模式聲明后,this 指向變?yōu)?undefined 了,同時在函數內聲明嚴格模式只對函數內定義的變量與函數有關,跟其調用的外部函數無關。
點石成金,隱式綁定
什么是隱式綁定呢?
this 隱式綁定:如果在函數調用的時,函數前面存在調用他的對象,那么 this 就會隱式綁定到這個調用對象上。
所以隱式綁定的關鍵點在于函數被調用的對象是誰,說白了就是找調用這個函數前面的點.是誰,誰就是 this 所綁定的對象了。
?? 舉個栗子:
{
? var mode = 'window';
? var boss1 = {
? ? mode: 'boss1',
? ? fn() {
? ? ? console.log(this.mode);
? ? },
? };
? var boss2 = {
? ? mode: 'boss2',
? ? call: boss1.fn,
? ? o: boss1,
? };
? boss2.o.fn(); // boss1
? boss2.call(); // boss2
? var boss1Copy = boss1.fn;
? boss1Copy(); // window
}函數隱式綁定時,如果函數調用時存在多個對象,this 指向距離自己最近的對象,也就是 . 前面的對象是誰,this 就指向誰。
那么問題來了,如果刪除 boss2 上的 mode,會有什么不一樣呢?
?? 舉個栗子:
{
? var mode = 'window';
? var boss1 = {
? ? mode: 'boss1',
? ? fn() {
? ? ? console.log(this.mode);
? ? },
? };
? var boss2 = {
? ? call: boss1.fn,
? ? o: boss1,
? };
? boss2.call(); // undefined
}答案是輸出 undefined,因為此時由于 boss1 只是 boss2 的屬性,boss1 與 boss2 的原型鏈各不相同相同,不屬于父子關系,因此符合作用域鏈查找規(guī)則,所以 this 須從 boss2 上找 mode 屬性,當 boss2 上不存在 mode 屬性時則返回 undefined。注意不要與作用域鏈混淆了。
?? 下面這個例子就要小心點咯,能想出答案么?
{
? var mode = 'window';
? var boss1 = {
? ? mode: 'boss1 mode',
? ? fn() {
? ? ? console.log(this.mode);
? ? },
? };
? function Fn() {}
? Fn.prototype.mode = 'Fn mode';
? Fn.prototype.fnProto = function() {
? ? console.log(this.mode);
? };
? var boss2 = {
? ? mode: 'boss2 mode',
? ? fn: function() {
? ? ? return boss1.fn();
? ? },
? ? proto: new Fn(),
? };
? boss2.fn(); // boss1 mode
? boss2.proto.fnProto(); // Fn mode
}答案是 boss1 mode 和 Fn mode 哦,猜對了嗎。
涉及到原型鏈與作用域鏈的以一些區(qū)別,基本這里就不做解析了,請各自查漏補缺~。
隱式綁定丟失
相信細心的同學已經發(fā)現,上述例子有一個函數賦值給變量再調用的情景。當函數賦值再調用后,原本 this 指向會發(fā)生改變,函數的 this 不會指向其原對象,從而引起隱形綁定丟失問題。
常見引起隱形丟失的方式:
1. 函數賦值變量再調用
?? 舉個栗子:
{
? var mode = 'window';
? var boss1 = {
? ? mode: 'boss1',
? ? fn() {
? ? ? console.log(this.mode);
? ? },
? };
? var boss2 = {
? ? mode: 'boss2',
? ? call: boss1.fn,
? ? o: boss1,
? };
? boss2.o.fn(); // boss1
? boss2.call(); // boss2
? var boss1Copy = boss1.fn;
? boss1Copy(); // window
}上述案例 boss1Copy 和 boss2.call 就是函數賦值變量再調用的情況
1. 函數以形參傳遞
?? 舉個栗子:
{
var mode = 'window';
var boss1 = {
mode: 'boss1',
fn() {
console.log(this.mode);
},
};
function exce(params) {
params && params();
}
exce(boss1.fn); // window
}
上述例子中我們將 boss1.fn 也就是一個函數傳遞進 exce 中執(zhí)行,這里只是單純傳遞了一個函數而已,this 并沒有跟函數綁在一起,此時 this 指向原對象發(fā)送 丟失從而指向了 window。
可見,隱式丟失本質上是因為函數賦值引起的,在函數賦值給變量或另一個函數形參 Fn 后,在調用 Fn 時 this 會指向離其最近的對象。
指腹為婚,顯式綁定
this 顯式綁定:指通過 Object.prototype.call、Object.prototype.apply、Object.prototype.bind 方法改變 this 指向。
這里我將顯式綁定細分為:
• 顯式綁定:在運行時改變 this 指向
• call
• apply
• 顯式硬綁定:一次綁定后,永久不能改變 this 指向
• bind
接下來看個例子,分別通過 call、apply、bind 改變了函數 log 的 this 指向。
?? 舉個栗子:
{
? function log() {
? ? console.log(this.name);
? }
? var boss1 = { name: 'boss1' };
? var boss2 = { name: 'boss2' };
? var boss3 = { name: 'boss3' };
? log.call(boss1); // boss1
? log.apply(boss2); // boss2
? log.bind(boss3)(); // boss3
? var logBind = log.bind(boss3);
? logBind.apply(boss1); // boss3
? logBind.bind(boss2); // boss3
}在 JavaScript 中,當調用一個函數時,我們習慣稱之為函數調用,此時函數處于一個被動的狀態(tài);而 bind、 call 與 apply 讓函數從被動變主動,函數能主動選擇自己的上下文,所以這種寫法我們又稱之為函數應用。
注意,如果在使用 bind、 call 與 apply 之類的方法改變 this 指向時,指向參數提供的是 null 或者 undefined 時, this 將指向全局對象。
?? 舉個栗子:
{
? var name = 'window';
? function log() {
? ? console.log(this.name);
? }
? var boss1 = { name: 'boss1' };
? var boss2 = { name: 'boss2' };
? var boss3 = { name: 'boss3' };
? log.call(null); // window
? log.apply(undefined); // window
? log.bind(undefined)(); // window
}同樣值得注意的是,bind 在顯式改變 this 指向之后會返回一個新的綁定函數(bound function,BF)。綁定函數是一個 exotic function object(怪異函數對象,ECMAScript 2015 中的術語),它包裝了原函數對象。調用綁定函數通常會導致執(zhí)行包裝函數。
另外 簡明 補充一下 call、apply、bind 的區(qū)別:
• bind:函數硬綁定 this 指向并返回一個全新函數 BF,且返回的 BF 無法再次被 call、apply、bind 改變 this 指向,且需要執(zhí)行 BF 才會運行函數。
• function.bind(thisArg[, arg1[, arg2[, ...]]])()
• call:改變 this 指向的同時還會執(zhí)行函數,一個以散列形式的形參。
• function.bind(thisArg[, arg1[, arg2[, ...]]])
• apply:改變 this 指向的同時還會執(zhí)行函數,可接受一個數組形式的形參。
• function.apply(thisArg,[param1,param2...])
call & apply 主要區(qū)別在于傳參形式不同,在傳參的情況下,call 的性能要高于 apply,因為 apply 在執(zhí)行時還要多一步解析數組。
內有乾坤,new 綁定
嚴格來說,JavaScript 中的構造函數只是使用 關鍵字 new 調用的普通函數,它并不是一個類,最終返回的對象也不是一個實例,只是為了便于理解習慣這么說罷了。
一個比較容易忽略的會綁定 this 指向 的方法就是使用 new。當我們 new 一個函數時,就會自動把 this 綁定在新對象上,然后再調用這個函數。new 會覆蓋 bind 的綁定讓其無法生效。
那么 new 對函數到底起到什么作用呢,大致分為三步:
1. 創(chuàng)建一個空的簡單 JavaScript 對象(即{});
2. 為步驟 1 新創(chuàng)建的對象添加屬性__proto__,將該屬性鏈接至構造函數的原型對象 ;
3. 將步驟 1 新創(chuàng)建的對象作為 this 的上下文 ;
4. 如果該函數沒有返回對象,則返回 this。
這個過程我們稱之為構造調用。
?? 那么 new 在構造調用時對 this 產生什么影響呢?請看栗子:
{
? function log() {
? ? console.log(this);
? }
? log(); // window{}
? new log(); // log{}
? var boss1 = { name: 'boss1' };
? log.call(boss1); // boss1{}
? new log.call(boss1); // Uncaught TypeError: log.call is not a constructor
? new log.bind(boss1); // Uncaught TypeError: log.call is not a constructor
? var logBind = log.bind(boss1);
? logBind(); // boss1{}
? new logBind(); // log{}
}
當 new 一個函數時,this 會被綁定為函數本身,即使函數在 bind 改變 this 指向的情況下,關鍵字 new 依舊會將 this 指向為函數本身。且 new 綁定與顯式綁定互不兼容。
軍令如山,箭頭函數
ES6 的箭頭函數是另類的存在,為什么要單獨說呢,這是因為箭頭函數中的 this 不適用上面介紹的幾種綁定規(guī)則。
準確來說,箭頭函數中沒有 this,箭頭函數的 this 指向取決于外層作用域中的 this,外層作用域或函數的 this 指向誰,箭頭函數中的 this 便指向誰。
因為箭頭函數里的 this 是永遠指向到當前詞法作用域(Lexical this)之中 ,在代碼編碼時就可以確定。沒有其它 this 綁定方式可以覆蓋。
這樣的好處就是方便讓回調函數的 this 使用當前的作用域,不怕引起混淆。
所以對于箭頭函數,只要看它在哪里創(chuàng)建的就行。
?? 有點吃軟飯的嫌疑,一點都不硬朗,我們來看看栗子:
{
? function fn() {
? ? return () => {
? ? ? console.log('efnArrow: ', this);
? ? };
? }
? function callback(cb) {
? ? cb();
? }
? var boss1 = {
? ? name: 'boss1',
? ? fn() {
? ? ? console.log('fn: ', this);
? ? },
? ? fnArrow: () => {
? ? ? console.log('fnArrow: ', this);
? ? },
? ? ret() {
? ? ? return function() {
? ? ? ? console.log('ret: ', this);
? ? ? };
? ? },
? ? retArrow() {
? ? ? return () => {
? ? ? ? console.log('retArrow: ', this);
? ? ? };
? ? },
? ? cb() {
? ? ? callback(function() {
? ? ? ? console.log('cb: ', this);
? ? ? });
? ? },
? ? cbArrow() {
? ? ? callback(() => {
? ? ? ? console.log('cbArrow: ', this);
? ? ? });
? ? },
? };
? var boss2 = {
? ? name: 'boss2',
? ? fn: boss1.retArrow,
? };
? boss1.fn(); // fn: boss1{}
? boss1.fnArrow(); // fnArrow: window{}
? boss1.ret()(); // ret: window{}
? boss1.retArrow()(); // retArrow: boss1{}
? boss1.cb(); // cb: window{}
? boss1.cbArrow(); // cbArrow: boss1{}
? boss1.fn.call(boss2); // fn: boss2{}
? boss1.fnArrow.call(boss2); // fnArrow: window{}
? boss1.ret.call(boss2)(); // ret: window{}
? boss1.retArrow.call(boss2)(); // retArrow: boss2{}
? boss1.ret().call(boss2); // ret: boss2{}
? boss1.retArrow().call(boss2); // retArrow: boss1{}
? boss1.cb.call(boss2); // cb: window{}
? boss1.cbArrow.call(boss2); // cbArrow: boss2{}
? var bar = boss1.retArrow.call(boss2);
? bar(); // returnArrowLog: boss2{}
? bar.call(boss1); // returnArrowLog: boss2{}
? bar.bind(boss1)(); // returnArrowLog: boss2{}
}對 boss1.retArrow 為啥我們第一次綁定 this 并返回箭頭函數后,再次改變 this 指向沒生效呢?
前面說了,箭頭函數的 this 取決于外層作用域的 this,boss1.retArrow 函數執(zhí)行時 this 指向了 boss1,所以箭頭函數的 this 也指向 boss1。除此之外,箭頭函數 this 還有一個特性,那就是一旦箭頭函數的 this 綁定成功,也無法被再次修改,有點硬綁定的意思。
當然,箭頭函數的 this 也不是真的無法修改,我們知道箭頭函數的 this 就像作用域繼承一樣從上層作用域找,因此我們可以修改外層函數 this 指向達到間接修改箭頭函數 this 的目的,如 boss1.retArrow.call(boss2)()成功將 this 指向 boss2。
this 綁定優(yōu)先級
前面已經介紹了幾種 this 綁定規(guī)則,那么問題來了,如果一個函數調用存在多種綁定方法,this 最終指向誰呢?這里直接先上答案。
this 綁定優(yōu)先級:
• 顯式綁定 > 隱式綁定 > 默認綁定
• new 綁定 > 隱式綁定 > 默認綁定
為什么顯式綁定不和 new 綁定比較呢?因為不存在這種綁定同時生效的情景,如果同時寫這兩種代碼會直接拋錯,所以大家只用記住上面的規(guī)律即可。
總結
文章到這里,對于 this 的幾種綁定場景就全部總結完畢了,如果你有跟著例子一起練習下來,相信聰明的你現在對于 this 的理解一定更上一層樓了。
那么通過本文,我們學習了默認綁定在嚴格模式與非嚴格模式下 this 指向會有所不同。
也知道了隱式綁定的觸發(fā)條件與隱式丟失的幾種情況。
相對于隱式綁定的不可見,我們還學習到顯式綁定以及硬綁定,并提到非嚴格模式下,當綁定指向為 null 或 undefined 時 this 會指向全局。
接著重新認識了 new 綁定。
最后我們了解了不太合群的箭頭函數中的 this 綁定,了解到箭頭函數的 this 由外層作用域的 this 指向決定,并有一旦綁定成功也無法再修改的特性。
好了,內容如有不妥之處請指正,希望你能受益良多~
相關文獻
• MDN-this[1]
• MDN-Function.prototype.bind()[2]
• MDN-Function.prototype.call()[3]
• MDN-Function.prototype.apply()[4]
• MDN-new 運算符[5]
• MDN-箭頭函數[6]
引用鏈接
[1] MDN-this: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this
[2] MDN-Function.prototype.bind(): https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
[3] MDN-Function.prototype.call(): https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/call
[4] MDN-Function.prototype.apply(): https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/apply
[5] MDN-new 運算符: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/new
[6] MDN-箭頭函數: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions
到此這篇關于你可能不知道的JavaScript之this指向詳解的文章就介紹到這了,更多相關JS this指向內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
在Javascript中為String對象添加trim,ltrim,rtrim方法
利用Javascript中每個對象(Object)的prototype屬性我們可以為Javascript中的內置對象添加我們自己的方法和屬性。2006-09-09

