javascript精雕細琢(四):認親大戲——通過console.log徹底搞清this
目錄
引言
JS中的this指向一直是個老生常談,但是新手又容易暈的地方。我在網上瀏覽了很多帖子,但是發現一個通病,也是部落格的侷限性—— 重理論說明,實踐性低 。最多就是貼個程式碼或者圖,增加可理解性。
所以,我就想通過程式碼學習黃金法則—— 敲就完了 。以console.log,循序漸進,一步步的實踐,來說明this的指向。而從我自身的理解角度來講,這個方法效果還不錯~
那麼,接下來我將從兩個方面—— 普通函式 及 箭頭函式 兩個方面來說明this指向。建議 將所有程式碼copy下來,一步步列印, 最終肯定能夠理解this的指向。如果沒理解,那麼,再列印一遍~
程式碼在前
// function下的this基於執行環境的指定。簡單理解就是函式前邊掛誰,this就是誰,沒有就是window。那麼函式直接呼叫和匿名函式自執行由於前邊什麼都沒有,就指向window。 // () => {}箭頭函式下的this基於作用域鏈查詢確定,向上查詢this,那個作用域裡有this,就呼叫這個this。函式直接呼叫和箭頭函式自執行仍舊會遵循查詢原則。 //--------function下的this-------- //----首先是普通的函式宣告 // function test() { //console.log(this); // } // test(); // 列印window,因為沒有指明執行環境,那麼執行環境就是window // //----如果是閉包呢? // function test() { //console.log(this); //const log = "Lyu"; //const fn = function() { //console.log(log); //列印Lyu //console.log(this); //列印window // } // // fn(); // 列印Lyu,window // } // test(); //window,Lyu,window。 首先呼叫test(),由於test()前什麼都沒掛,this就指向window。然後test函式內部,由於fn()前也什麼都沒有掛,this同樣指向window。舉這個例子是想證明,this並不會受函式額作用域及執行上下文影響,必須明確指定。 // //----然後是匿名函式自執行? // function test() { //(function(){ //console.log(this); //})() // } // test(); //window,因為匿名函式自執行前邊就不能掛其他玩意兒,所以它始終指向window // //----接下來看明確指定執行環境的例子 // const obj = { //fn: function() { //console.log(this); //} // } // obj.fn(); //列印obj,因為指定執行環境為obj的{}塊級作用域內 //--如果改變一下呼叫方式呢? // const fn = obj.fn; // 此時fn = function() { console.log(this) },相當於建立了一個全域性函式 // fn(); //列印window,因為沒有指定執行環境 // //----事件呼叫下的this // document.onclick = function() { //console.log(this); //列印document,因為指定執行環境為document,即document在click時觸發 // } //這裡相信大家都是明白的,就不再多贅述了 //----最後是通過call、apply、bind繫結下的this //一句話說明,不再舉例。前邊說了,function下的this,通過指定執行環境來確定的,而call、apply、bind就是用來指定執行環境的,所以指誰,this就是誰。 //--------箭頭函式下的this-------- //既然箭頭函式下的this通過作用域鏈查詢,那麼作用域中如果沒有宣告this值,那麼就向上查詢 //----先說明this的建立與查詢 // function Test() { //console.log(this); // } // Test(); // window,未指定作用環境,所以this指向window // new Test(); //Test{},此時,建構函式內Test()內的this被new宣告,this指向建構函式建立的物件Test{},所以列印Test{}物件 // //----接下來是箭頭函式 // function Arrow() { //window.onmousewheel = () => { //console.log(this); //} // } // Arrow(); // window,直接呼叫時,Arrow()函式內並沒有宣告this,所以滾動滑鼠,this會隨作用域鏈查詢。先在Arrow函式內,沒找到this。然後一直向上,最終找到window。 //--然後我們new它一下子 // new Arrow(); //此時通過new,建構函式Arrow()內的this被宣告,且指向物件Arrow{},所以箭頭函式在作用域鏈中查詢時,在Arrow函式內就找到this為Arrow{} //----接下來複雜一點,來個事件,順便再加點匿名函式自執行 // function Go() { ////new一個this媽媽 //window.onmousewheel = () => { //console.log(this); // 媽媽不見了! //(() => { //console.log(this); // 媽媽去哪了? //(() => { //console.log(this); // 嚶嚶嚶,媽媽沒了! //(() => { //console.log(this); // 走啊哥幾個,找媽媽去 //})(); //})(); //})(); //} // } // Go(); //window,因為onmousewheel事件中及Go()函式中沒有宣告this,所以按照作用域鏈查詢,找到window //--然後我們再new它一下子 // new Go(); // 全部列印Go{},因為new操作符,Go()函式中聲明瞭this,且指向Go{}物件。而onmousewheel事件也用箭頭函式指定,仍舊遵循查詢原則。就這麼一層一層的找,最後都找到Go函式作用域內的this,最後全部列印Go{}。這不就是小蝌蚪找媽媽嘛! //----接下來換個搭配方式再看一下,普通function搭配箭頭函式 // function GoOn() { //document.onclick = function() { //console.log(this); //(() => { //console.log(this); //})(); //} // } // GoOn(); // document、document,此時由於function中的this已經繫結到document,所以第一個列印document; // 而由於箭頭函式自執行仍舊遵循作用域鏈查詢原則,不會指向window。所以箭頭函式自執行後,根據作用域鏈向上查詢this,找到document; //--啥也別說了,就是new它丫的 // new GoOn() // document、document,此時就算new操作聲明瞭this,但是是在click事件外的作用域中,箭頭函式在click中已經找到了this,不會再向上查詢; //所以仍舊列印document,document; // //----再看個混搭,然後我們結束搭配 // function Going() { //document.onclick = function() { //console.log(this); //(function() { //console.log(this); //})(); // //(() => { //onsole.log(this); //}) //} // } // Going(); // document、window、document,首先上來就指定了this為document,所以第一個列印document; // 而function的匿名函式自執行會指向window,所以第二個列印window; // 第三個箭頭函式自執行,遵循作用域鏈查詢原則,在onclick事件中找到this為document,所以列印document; // --new、new、new // new Going(); //規則不變,結果不變 // //----好,混搭看完,接下來說個更有意思的。關於作用域的形成 //----JS的函式作用域及作用域鏈,是在函式建立時就被固定的 //----這麼說確實不太直觀,那麼通過舉例來說明 // function Test() { //console.log(this) //innerTest(); // } // const innerTest = () => { //console.log(this); // } // Test(); //window、window,如果不明白,把上邊再看一遍 // new Test(); // Test{}、window,從這就可以看出端倪了。 // 為什麼Test內的this指向Test{}物件了,而箭頭函式中的this仍舊為window呢?innerTest函式在Test函式內執行,說好的按照作用域鏈查詢呢? // 請看文章中詳細的解釋 // //----最後,call、apply、bind下的箭頭函式 //一句話說明,箭頭函式的this改不了,幹啥都改不了,咋著都改不了,硬氣!
1、function下的this
我將從 普通的函式宣告 、 匿名函式自執行 、 物件宣告 、 事件繫結 、及 call等方法繫結 來分別說明function下的this指向。
function下的this理解起來也簡單。我們就以 親爹 和 乾爹 來比喻:
假設 window 是所有函式的 乾爹 。我們是公益組織,要給JS下的函式找到它們的 親爹 ,而function宣告的函式,都是渴望父愛的男孩;
確認親爹的方式就是 呼叫函式 的時候在它們前邊加個 .(點) 或者 ["name"] ,或者通過 call、apply、bind 其他手段確認;
而那些呼叫時候, 前邊嘛也沒有的 ,他們親爹沒找到,那他們的 乾爹就當親爹 來孝順;
這場公益認爹,就是 function的this 行為— 函式前邊有.(點)或者["name"],明確指定了親爹的,this就是親爹;直接呼叫的函式、匿名自執行的函式,這倆沒找到親爹的,this就是乾爹window;而通過call、apply、bind其他渠道找到的親爹,this同樣是親爹;
下面詳細說一下
!
1) 普通function宣告
最常用的函式宣告無非是兩種:
function test() {} 及 const test = function() {}
這兩種寫法的區別在於宣告方式的不同,進而影響 變數提升 ,並不會對this的指向產生影響。
這兩種宣告方式下的function函式,在呼叫時,通常就是 直接呼叫 。那麼通過 認爹 我們就能知道,這種情況下的 this就是window 。
function test() { console.log(this); } test(); // 列印window,因為沒有指明執行環境(沒找到親爹),那麼執行環境就是window(乾爹)
為什麼程式碼裡我加上了 閉包 的說明呢?主要是為了跟箭頭函式做一個區分,證明一下,function下的this跟 作用域以及作用域鏈 無關。同時跟它呼叫時的 執行上下文 也無關,就是看函式前邊有沒有 .(點)——必須明確它的親爹 。
2) 自執行匿名function宣告函式
與函式直接呼叫同理,不再贅述,匿名函式自執行就理解成 父母雙亡 ,這貨 再也沒有親爹 了,所以它的 this始終指向window 。
3) 物件下的function宣告
物件下宣告的函式,在呼叫時是要通過物件方法訪問的,所以~ 肯定有爹!
但是這裡邊分了兩種情況,一種情況是 正常的通過物件呼叫方法 ,另一種 跟直接呼叫函式無異 ~
const obj = { fn: function() { console.log(this); } } obj.fn(); //列印obj,因為指定執行環境為obj的{}塊級作用域內(親爹為obj) //如果改變一下呼叫方式呢? const fn = obj.fn; // 此時fn = function() { console.log(this) },相當於普通的function建立函式 fn(); //列印window,因為沒有指定執行環境(沒親爹)
4) 事件呼叫下的function宣告
事件的一般寫法上,它必須要有 .(點)或者[name] ,所以它 肯定有親爹 (最幸福的function函式),那麼.(點)前是誰,親爹就是誰~
document.onclick = function() { console.log(this); //列印document,因為指定執行環境為document,即document在click時觸發 }
5) call、apply、bind繫結
一句話總結: 給誰,誰就是親爹!
function test() { console.log(this.say); }; const obj = {say: "我是它爹"}; const father = {say: "我也是它爹"}; const result = {say: "我也是它爹,它到底幾個爹"}; test.apply(obj); // 我是他爹 test.call(father); // 我也是他爹 (test.bind(result))(); // 我也是它爹,他到底幾個爹
2、箭頭函式下的this
首先,不明白箭頭函式的,請先自行百度或者Google,不要還沒開車就出車禍了;
然後,接下來我會從 正常函式宣告 的箭頭函式、 匿名函式自執行 的箭頭函式、 物件下宣告 箭頭函式、 事件呼叫下 的箭頭函式、 function與箭頭函式混合雙打 、 作用域鏈查詢 、及 call等方法繫結 來說明箭頭函式下的this指向。
那麼,同上,箭頭函式也來個比喻,同樣用 親爹 和 乾爹 :
設定不變, window還是乾爹 。但是也有一點不同——那就是箭頭函式她是個拜金女, 就愛找有錢(this)的乾爹 ;
而且吧,在拜金女眼裡, window這個乾爹是最窮的,所以不到走投無路,不找window這個乾爹 。而 對它的親爹,有錢(this)才行 ;
而this當然就是錢啦,誰有錢這箭頭函式它就找誰!呼叫箭頭函式就是找錢!
所以啊,在這場發家致富之旅中,箭頭函式中this的指向也是很明確的—— 如果當前作用域中,沒有通過call、apply、bind、new等操作明確this的指向(沒錢),那麼箭頭函式將沿著作用域鏈(關係網)繼續想上查詢,直到找到明確的this(有錢的乾爹)為止
1) 正常宣告的箭頭函式
同function不同,對於箭頭函式,只有一種宣告方式:
const arrow = () => {}
可以變換的地方就是 引數和返回值部分的簡寫
同樣的,這種宣告方式下的函式,就是 直接呼叫 。那麼根據前邊的比喻,箭頭函式的認爹方式跟function是大不相同的。直接呼叫箭頭函式時,這個拜金女就開始見錢眼開了——它先在 當前作用域 中找,當前作用域下如果 沒有明確的this (錢),就繼續 沿著作用域鏈 往上找, 直到找到this 為止,因為有 window這個乾爹保底,所以一點好處沒撈到的時候,就找window 。
function Arrow() { window.onmousewheel = () => { console.log(this); } } Arrow(); // window,直接呼叫時,Arrow()函式內並沒有明確的this(沒錢),所以滾動滑鼠,this會隨作用域鏈查詢(這個乾爹不行,就再換個乾爹)。先在Arrow函式內,沒找到this。然後一直向上,最終找到window(只能保底)。 new Arrow(); //此時通過new,建構函式Arrow()內的this被宣告(有錢了),且指向物件Arrow{},所以箭頭函式在作用域鏈中查詢時,在Arrow函式內就找到this為Arrow{}
2) 自執行匿名箭頭函式
箭頭函式是個很有原則的拜金女,不管怎麼執行它,它 就認錢,就認錢,就認錢 (重要事情說3遍),有錢才是爹。所以就算是自執行的匿名箭頭函式,它仍舊遵循找爹原則, 沒錢免談,我接著向上找 。
所以,它仍舊先在 當前作用域 中找,當前作用域下如果 沒有明確的this (錢),就繼續 沿著作用域鏈 往上找, 直到找到this 為止。都沒有,就找window。
function Test() { console.log(this); (() => { console.log(this); })(); } Test(); //window、window; new Test(); //Test{}、Test{}; //規則同上,不再贅述
3) 作為物件方法的箭頭函式
按照function宣告的邏輯,物件呼叫它下面的方法,this肯定是指向物件的。那麼箭頭函式是否也是如此呢?答案肯定是否定的,因為物件中並沒有明確的this,而且物件還不能new,所以這就悲催了—— 箭頭函式所存在的物件,永遠不可能是它的乾爹(只限於父女關係的箭頭函式與物件,不包括function與箭頭函式混搭的爺孫關係等等) ;
const obj = { test: () => { console.log(this); } fn: function() { (() => { console.log(this) }) } } obj.test() // window,obj.test內沒有明確的this(錢),所以向上找到obj,結果obj也沒有錢,所以最後只能委曲求全,找window obj.fn() // obj,obj.fn中由於function的存在,this指向obj,所以一發命中,直接找obj認爹
4) 事件呼叫下的箭頭函式
其實作為一個拜金女,箭頭函式的生活還是挺無趣的,規則太單一。就拿這個事件呼叫來說吧,還是一個套路。 不管我是不是你親生的,反正你沒錢,我就不認你 。
function Go() { //沒想到唯一的希望也是身無分文……唉,又得window了 window.onmousewheel = () => { console.log(this); // 親爹看來你也沒錢啊! (() => { console.log(this); // 又一個窮貨! (() => { console.log(this); // 這也沒錢! (() => { console.log(this); // 錢呢! })(); })(); })(); } } Go(); // window,因為onmousewheel事件中及Go()函式中沒有明確的this(錢),所以按照作用域鏈查詢,找到window(走投無路) new Go(); // 有錢了 // 全部列印Go{},因為new操作符,Go()函式中聲明瞭this,且指向Go{}物件。 // 而onmousewheel事件也用箭頭函式指定,仍舊遵循查詢原則。就這麼一層一層的找,最後都找到Go函式作用域內的this(錢),最後全部列印Go{}(逮著一個有錢的可勁造,全造它一個)。 // Go{}物件左擁右抱,帝王生活讓人嚮往!
5) function與箭頭函式混搭及JS靜態作用域
俗話說得好哇,一山不容二虎,除非一公一母!還有就是男女搭配,幹活不累!
function與箭頭函式這一男一女遇上後,那是乾柴遇烈火,一拍即合,合作起來非常愉快!
在function這個
缺父愛的男孩幫助下,箭頭函式找乾爹變得容易起來~
以開頭程式碼中挖的坑為例,順帶說一下JS中的靜態作用域
function Test() { console.log(this) innerTest(); } const innerTest = () => { console.log(this); } Test(); // window、window new Test(); // Test{}、window,從這就可以看出端倪了。
按照上邊一路順下來的思路理解的話, 第二次new操作之後,應該列印Test{}和Test{}對不對?
讓我們捋一下思路:
在Test函式裡呼叫innerTest函式,innerTest函式是一個箭頭函式。那麼我在Test裡呼叫它的時候,這拜金女肯定是一步一步的往上找this(錢);
第一次無new直接呼叫
,Test裡沒this(錢),所以找了window;
可是第二次new操作後,Test有this(錢)了,為啥箭頭函式沒找Test?難道嫌它醜?
一張圖說明情況:
從Chrome控制檯列印的作用域中可以看出,innerTest的 作用域鏈中根本沒有Test函式 ,所以它壓根不會在Test中查詢this。
這就表明了JS作用域與作用域鏈的一個問題—— 靜態 。即 函式的作用域及作用域鏈,在函式宣告時形成,並且保持不變 。因為innerTest是在全域性宣告的,所以它的作用域鏈只有Script及Global,就算再Test函式內呼叫,也不會改變,除非 在Test函式內再宣告一個函式 ,那麼該函式的作用域及作用域鏈中就包含了Test函式,不管有沒有通過閉包呼叫Test函式中的變數(不呼叫Test函式內變數的話,Chrome瀏覽器控制檯中打印不出來閉包作用域)。
6) call、apply、bind繫結
一句話總結: 我對this(錢)很專一的!
箭頭函式的this指向,無法通過call、apply、bind改變!賊專一!
function Test() { const fn = () => { console.log(this); } fn(); // Test{} const say = function() { console.log(this); } say() // window function Replace() { console.log(this); // Replace{} fn.call(this); // Text{} say.call(this); // Replace{} } new Replace(); } new Test();
結語
至此,這場認親大戲就到此完畢。整體內容還是有點多的,我相信大多數人是沒耐心讀完的,所以我儘量想寫的幽默有趣一點。就像開夜路怕困,會話會變多、抽菸解困,有的人肯定會反感這種文風,但我也沒那麼多讀者~哈哈哈。
最後,有的點挖的還是不夠深的,沒辦法,水平真是有限,挖不動了。如果能給到各位啟發,希望你能繼續挖下去~