1. 程式人生 > >javascript精雕細琢(四):認親大戲——通過console.log徹底搞清this

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同樣是親爹;

       下面詳細說一下這場認親公益行, 各種情形下的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();

結語

       至此,這場認親大戲就到此完畢。整體內容還是有點多的,我相信大多數人是沒耐心讀完的,所以我儘量想寫的幽默有趣一點。就像開夜路怕困,會話會變多、抽菸解困,有的人肯定會反感這種文風,但我也沒那麼多讀者~哈哈哈。

       最後,有的點挖的還是不夠深的,沒辦法,水平真是有限,挖不動了。如果能給到各位啟發,希望你能繼續挖下去~

如有錯誤或闡述不充分之處,歡迎指正~