javascript基礎修煉(2)——What's this(上)

開發者的javascript造詣取決於對【動態】和【異步】這兩個詞的理解水平。html

一.this是什麼

this是javascript關鍵字之一,是javascript可以實現面向對象編程的核心概念。用得好能讓代碼優雅高端,風騷飄逸,用很差也絕對是坑人坑己利器。咱們經常會在一些資料中看到對this的描述是:java

this是一個特殊的與Execution Contexts相關的對象,用於指明當前代碼執行時的Execution Contextsthis在語句執行進入一個Execution Contexts時被賦值,且在代碼執行過程當中不可再改變。
注:Execution Contexts也就是咱們常聽到的"上下文""執行環境"算法

看不懂?看不懂就對了,我也看不懂。
對於this的指向,咱們常會聽到這樣一個原則——this是一個指針,指向當前調用它的對象。但實際使用中,咱們卻發現有時候很難知道當前調用它的是哪一個對象,從而引起了一系列的誤用和奇怪現象。編程

今天,咱們就換一種思路,試試如何從語言的角度一步一步地去理解this,你會發現:
只要你能聽懂中國話,就意味着你能理解this瀏覽器

二.近距離看this

2.1 this的語法意義

javascript是一門程序設計語言,也就是說,它是一種語言,是語言,就有語法特性。若是拋開this的原理和編程中的用法,僅從語文的層面去理解,它的本質就是代詞。什麼是代詞?漢語中的,,,大家,咱們,他們這一類的詞語就是代詞。代詞並不具體指某一個具體的事物,但結合上下文,就能夠知道這類詞語代替的是誰。
好比下面這幾句描述的語境:app

  • 大爺是趙本山
    • 請問:誰大爺是趙本山?
    • 無法回答,由於沒有上下文約束,此處的可能指任何人。
  • 李雷來頭可不小,大爺是趙本山
    • 請問:誰大爺是趙本山?
    • 很容易回答,由於前一句話使得咱們可以得知當前上下文中,"他"指的就是"李雷"
  • ___來頭可不小,大爺是趙本山
    • 請問:誰大爺是趙本山?
    • 此處空格填誰,誰大爺就是趙本山。

小結一下:框架

代詞,用於指代某個具體事物,當結合上下文時,就能夠知道其具體的指向。換句話說,有了上下文時,代詞就有了具體的意義。this在javascript語言中的意義,就如同代詞在漢語中的意義是同樣的。異步

2.2 不一樣做用域中的this

在ES6出現前,javascript中的做用域只分爲全局做用域和函數做用域兩種。(如下部分暫不討論嚴格模式)。函數

  • 全局做用域中使用this

全局做用域中的this是指向window對象的,但window對象上卻並無this這個屬性:

  • 函數做用域使用this

函數做用域中的this也是有指向的(本例中指向window對象),咱們知道函數的原型鏈是會指向Object的,因此函數自己能夠被當作一個對象來看待,但遺憾的是函數的原型鏈上也沒有this這個屬性:

綜上所述,this能夠直觀地理解爲:

this與函數相關,是函數在運行時解釋器自動爲其賦值的一個局部常量。

2.3 javascript代碼編寫方式

a.不使用this

這是有可能發生的。不少初學者會發現,本身在編寫javascript代碼時並無用到this,可是也並不影響本身編寫代碼。前面提到過上下文信息的意義在於讓代詞明確其指向,那麼若是一段話的上下文中並無使用代詞,在語文中咱們就不須要聯繫上下文就能理解這段話;同理,若是函數的函數體中並無使用this關鍵字來指代任何對象,或者不須要關注其調用對象,那實際上就算不肯定this的指向,函數的執行過程也不會有歧義。

/**
 *數據加工轉換類的函數,對開發者來講更關注結果,而並不在意是誰在調用。
*/
function addNumber(a,b) {
    return a + b;
}

不管是計算機對象調用addNumber方法,或是算盤對象調用addNumber方法,甚至是人類對象經過心算調用addNumber方法,都無所謂,由於咱們關注的是結果,而不是它怎麼來的。

b.不使用函數自帶的this

有時候咱們編寫的代碼是須要用到一些關於調用對象的信息的,但因爲不熟悉this的用法,許多開發者使用了另外一種變通的方式,也就是顯式傳參。好比咱們在一個方法中,須要打出上下文對象的名字,下面兩種編寫方式都是能夠實現的。

//方式一.使用this
invoker.whoInvokeMe = function(){
    console.log(this.name);
}

//方式二.不使用this
function whoInvokeMe2(invoker){
    console.log(invoker.name);
}

方式二的方式並非語法錯誤,可讓開發者避開了由於對this關鍵字的誤用而引起的混亂,一樣也避開了this所帶來的對代碼的抽象能力和簡潔性,同時會形成一些性能上的損失,畢竟這樣作會使得每次調用函數時須要處理更多的參數,而這些參數本能夠經過內置的this獲取到。

c.面向對象的編程

提到this,必然會提到另外一個詞語——面向對象。"面向對象"是一種編程思想,請暫時拋開封裝,繼承,多態等高大上的修飾詞帶來的負擔,純粹地感覺一下這種思想自己。有的人說"面向對象"賦予了編程一種哲學的意義,它是使用程序語言的方式對現實世界進行的一種簡化抽象,現實世界的一個用戶,一種策略,一個消息,某個算法,在面向對象的世界裏均將其視爲一個對象,也就是哲學意義上的無分別,每個對象都有其生命週期,它怎麼來,要作什麼,如何消亡,以及它與萬物之間的聯繫。

面向對象的思想,是用程序語言勾勒現實世界框架的方式之一,它的出現不是用來爲難開發者的,而是爲了讓開發者能以更貼近平常生活的認知方式來提高對程序語言的理解能力。

2.4 若是沒有this

咱們來看一下若是javascript中不使用this關鍵字,對程序編寫會形成什麼影響呢?
咱們先來編寫一段簡單的定義代碼:

//假設咱們定義一我的的類
    function Person(name){

    }
     
    // 方法-介紹你本身(使用this編寫)
    Person.prototype.introduceYourselfWithThis = function () {
        if (Object.hasOwnProperty.call(this, 'name')) {
           return `My name is ${this.name}`;
        } 
        return `I have no name`;
    }

    // 方法-介紹你本身(不使用this編寫)
    Person.prototype.introduceYourself = function (invoker) {
        if (Object.hasOwnProperty.call(invoker, 'name')) {
            return `My name is ${invoker.name}`;
        }
        return `I have no name`;
    }

    //生成兩個實例,併爲各自的name屬性賦值
    var liLei = new Person();
    liLei.name = 'liLei';
    var hanMeiMei = new Person();
    hanMeiMei.name = 'hanMeiMei';

在上面的簡單示例中,咱們定義了一個不包含任何實例屬性的類,並使用不一樣的方式爲其定義介紹你本身這個方法,第一種定義使用常規的面向對象寫法,使用this獲取上下文對象,獲取實例的name屬性;第二種定義不使用this,而是將調用者名稱做爲參數傳遞進方法。
咱們在控制檯進行一些簡單的使用:

那麼這兩種不一樣的寫法區別究竟是什麼呢?

  • 函數實際功能的變化
    從上面的示例中不難看出,當開發中不使用this時,須要開發者自行傳入上下文對象,並將其以參數的形式在函數執行時傳入,若是傳入的invoker 對象和 this的指向一致,那麼結果就一致,若是不一致,則會形成混亂。
    • 從編碼角度來看
      introduceYourselfWithThis()方法只是introduceYourself(invoker)方法的特例(當this === invoker時)。
    • 從方法的含義來看
      定義者但願實現自我介紹功能而編寫了introduceYourself()方法,但是使用者在閱讀到introduceYourself()的源碼時看到的代碼表達的意義是:**我告訴你一個名字,你把它填在'My name is __'這句話中再返回給我。而不是一個與調用對象有着緊密聯繫的自我介紹**動做。
  • 多此一舉的參數傳遞
    在正確的使用過程當中,thisinvoker 的指向是一致的,形參invoker的定義不只增長了函數使用的複雜度,也增長了函數運行的負擔,卻沒有爲函數的執行帶來任何新的附加信息。

  • 重複的雷同代碼
    若是編碼中不使用this,也就至關於漢語中不使用代詞,那麼咱們就須要在每個獨立的句子中使用完整的信息。爲了使introduceYourself()方法可以正確的執行,咱們須要在每個實例生成後,爲其綁定確切的實例方法,即:

var liLei = new Person();
    liLei.name = 'liLei';
    //定義實例方法
    liLei.introduceYourself = function (){
        return `My name is liLei`;
    };
   
    var hanMeiMei = new Person();
    hanMeiMei.name = 'hanMeiMei';
    //定義實例方法
    hanMeiMei.introduceYourself = function (){
        return `My name is hanMeiMei`;
    }

即時不使用this,你也不會直接陷入沒法編寫javascript代碼的境地,只是須要將全部的定義和使用場景所有具體化, 須要手動對全部的具體功能編寫具體實現,也就是"面向過程"的編程。

================================我是華麗的分割線======================================

【輕鬆一刻】

話說赤壁之戰後,一日閒來無事,孔明與劉關張三兄弟一塊兒喝酒。孔明說,我出三道題考考各位學識修養,如何啊?三兄弟舉手贊同。
孔明:第一題,主公,赤壁之戰發生在哪裏?
劉備:赤壁啊
孔明:答對了,主公果真厲害。第二題,關將軍,雙方有多少人蔘戰?
關羽:聯軍5萬,曹軍20餘萬。
孔明:答對了,關將軍也是智勇雙全啊。最後一題,他們分別是誰?
張飛:我......我靠

願你可以掌握this,不要在本身的代碼裏搞出他們分別是誰的尷尬,當心被隊友活埋。

================================我是華麗的分割線======================================

三. this的通常指向規則

javascript中有四條關於this指向的基本規則。今天,咱們將一塊兒經過【碼農視角】【語文老師視角】來分別解讀這些規則,你會發現他們理解起來其實很天然。

規則1——做爲函數調用時,this指向全局對象

瀏覽器中的全局對象,指的是window對象。這一規則指的就是咱們在全局做用域或者函數做用域中使用function關鍵字直接聲明或使用函數表達式賦值給標識符的方式建立的函數。爲了在調用時在內存中找到所聲明的方法,咱們須要一個標識符來指向它的位置,具名函數能夠經過它的名字找到,匿名函數則須要經過標識符來找到。做爲函數調用的實質,就是經過方法名直或標識符找到函數並執行它。

通常什麼樣的函數咱們會這樣定義呢?
就是那些不關注調用者的函數,好比上面舉例的addNumber()方法,這類函數每每是將一步或幾步業務邏輯組合在一塊兒,起一個新的名字便於管理和重用,而並不關注使用者究竟是誰。

語文老師解讀版
很好理解,當你想描述一個動做殊不知道或者不關注具體是誰作的,代詞就指向有的人
好比臧克家同窗在做文裏寫的這樣:
有的人活着,可是他已經死了;
有的人死了,可是他還活着;
上文中的指誰?指有的人;那有的人是誰?隨便,愛誰誰。

規則2——做爲方法調用時,this指向上下文對象

上文中咱們看到函數的做用域鏈上是包含Object對象的,因此函數能夠被當作對象來理解。當函數做爲對象被賦值在另外一個對象的屬性上時,這個對象的屬性值裏會保存函數的地址,由於用函數做爲賦值運算的右值時是一個引用類型賦值。若是這個函數正好又是一個匿名函數,那麼執行時只能經過對象屬性中記錄的地址信息來找到這個函數在內存中的位置,從而執行它。因此當函數做爲方法調用時,this中包含的信息的本質是這個函數執行時是怎麼被找查找到的。答案就是:經過this所指向的這個對象的屬性找到的。

通常什麼樣的函數咱們會這樣定義呢?
做爲方法定義的函數,每每是另外一個抽象合集的具體實現。好比前例的addNumber()這個方法,只是將兩個數字相加這樣一個抽象動做,至因而誰經過什麼方式來執行這個計算過程,無所謂,它能夠歸納全部對象將兩個數字相加並給出結果這一動做。可若是它做爲一個對象方法來調用時,就有了更明確的現實指向意義:

  • Computer.addNumber()表達了計算機經過軟硬件聯合做用而給出結果的過程
  • Calculator.addNumber()表達了計算器經過簡易硬件計算給出結果的過程
  • Abacus.addNumber()表達了算盤經過加減珠子的方式給出結果的過程
  • ...

語文老師解讀版
當你想知道一個代詞具體指的是誰時,固然須要聯繫上下文語境進行理解。

規則3——做爲構造函數使用時,this指向生成的實例

做爲構造函數使用,就是new + 構造函數名的方式調用的狀況。
js引擎在調用new操做符的邏輯能夠用僞代碼表示爲:

new Person('liLei') = {
    //生成一個新的空對象
    var obj = {}; 
    //空對象的原型鏈指向構造函數的原型對象
    obj.__proto__ = Person.prototype; 
    //使用call方法執行構造函數並顯式指定上下文對象爲新生成的obj對象
    var result = Person.call(obj,"liLei"); 
    // 若是構造函數調用後返回一個對象,就return這個對象,不然return新生成的obj對象
    return typeof result === 'object'? result : obj;
}

暫不考慮構造函數有返回值的狀況,那麼很容易就能夠明白this爲何指向實例了,由於類定義函數在執行的時候顯式地綁定了this爲新生成的對象,也就是調用new操做符後獲得的實例對象。

語文老師解讀版
有些同窗喜歡抄襲,抄襲這個動做能夠描述爲:"把一份做業Copy一遍,在最後寫上本身的名字。"。若是李雷是喜歡抄襲的人之一,那麼他就掌握了"抄襲"這個方法,那你以爲他每次抄完做業後在署名的地方應該寫本身的名字"李雷"仍是寫這一類人的總稱"喜歡抄襲的人"呢?
擡槓的那個同窗,我記住你了!放學別走!

規則4——使用call/apply/bind方法顯式指定this

call/bind/apply這三個方法是javascript動態性的重要組成部分,後續的篇章會有詳細的講解。這裏只看一下API用法,瞭解一下其對於this指向的影響:

  • func.call(this, arg1, arg2...)
  • func.apply(this, [arg1, arg2...])
  • func.bind(this [, arg1[, arg2[, ...]]])

這個規則很好理解,就是說函數執行時遇到函數體裏有this的語句都用顯式指定的對象來替換。

語文老師解讀版
就是直接告訴你下文中的代詞指什麼,好比:×××憲法(如下簡稱"本法"),那讀者固然就知道後面所說的"本法"指誰。

四. 基本規則示例

爲了更清晰地看到上面兩條原則的區別,咱們來看一個示例:

var heroIdentity = '[Function Version]Iron Man';
        
        function checkIdentity(){
            return this.heroIdentity;
        } 

        var obj = {
            name:'Tony Stark',
            heroIdentity:'[Method Version]Iron Man',
            checkIdentityFromObj:checkIdentity
        }

        function TheAvenger(name){
            this.heroIdentity = name;
            this.checkIdentityFromNew = checkIdentity;
        }

        var tony = new TheAvenger('[New Verison]Iron Man');

        
        console.log('1.直接調用方法時結果爲:',checkIdentity());
        console.log('2.經過obj.checkIdentityFromObj調用同一個方法結果爲:',obj.checkIdentityFromObj());
        console.log('3.new操做符生成的對象:',tony.checkIdentityFromNew());
        console.log('4.call方法顯示修改this指向:',checkIdentity.call({heroIdentity:'[Call Version]Iron Man'}));

控制檯輸出的結果是這樣的:

同一個方法,同一個this,調用的方式不一樣,獲得的結果也不一樣。

五. 後記

在基礎面前,一切技巧都是浮雲。

若是認爲明白了this的基本規則就能夠隨心所欲,那你就真的too young too simple了。
瞭解了基本指向規則,只能讓你在開發中本身儘量少挖坑或者不挖坑。可是想要填別人的坑或者讀懂大師級代碼中簡潔優雅的用法,還須要更多的修煉和反思。實際應用中許多複雜的使用場景是很難一會兒搞明白this的指向以及爲何要指定this的指向的。
筆者將在《javascript基礎修煉(3)——What's this(下)》中詳細講述開發中千奇百怪的this。欲知後事如何,先點個贊先吧!

參考文章:
[1].js中的new()到底作了什麼
[2].ECMA-262-3 in detail. Chapter 1. Execution Contexts

相關文章
相關標籤/搜索