深入理解JavasCript原型和閉包

1.對象是什么

  • 對象就是若干屬性的集合。

  • 在JS中一切引用類型都是對象:數組是對象,函數是對象,對象還是對象。對象里面的一切都是屬性,只有屬性,沒有方法。那么這樣方法如何表示呢?方法也是一種屬性。因為它的屬性表示為鍵值對的形式。如:

    var obj = {
      a: 10,
      b: function() {},
      c: {
        c1: '100',
        c2: function() {}
      }
    }
    
  • 上面說到函數、數組都是對象,那么它們也可以隨意添加屬性。如:

    function fn() {}
    fn.a = 10
    fn.b = function() {
      return 100
    }
    這個特性有什么作用呢?
    jQeury中'$'或者'jQuery',這個變量其實是一個函數。
    typeof $  // function
    而通過$可以調用很多方法,如:$.trim('abc ') //'abc'
    很明顯這是在$函數上加了一個trim屬性,而這個屬性是一個函數。
    
  • 如何判斷一個變量是否是對象:xxx instanceof Object

2.函數和對象的關系

  • 第一節說到,函數是一種對象,因為通過instanceof函數可以判斷。但是函數與對象之間,卻不僅僅是一種包含和被包含的關系,函數和對象之間的關系比較復雜,甚至有一點雞生蛋蛋生雞的邏輯。如:

    function fn() {
      this.name = 'bill',
      this.age = '99'
    }
    var fn1 = new fn()
    // fn1:{name: "bill", age: "99"}
    這個例子說明了對象可以通過函數來創建。
    
  • 其實所有對象都是通過函數來創建的,有人可能有疑問:

    var obj = {}
    var arr = []
    同過這種方式也能創建對象啊!
    但這種方式只是‘快捷方式’而已,在編程語言中一般稱其為‘語法糖’。以上代碼的本質是:
    var obj = new Object()
    var arr = new Array()
    typeof Object // function
    typeof Array  // function
    綜上,可以說對象都是通過函數來創建的。
    

3.prototype(原型)

  • 每個函數都默認有個prototype屬性,這個屬性的值是一個對象,而這個對象默認只有一個屬性constructor,指向這個函數本身。如圖:(左側是一個函數,而右側是它的原型)


  • 既然prototype是個對象,那么就可以為它們添加屬性:

    function fn () {}
    fn.prototype.name = 'bill'
    fn.prototype.age = function () {
      return 99
    }
    
  • 每個對象都有一個隱藏屬性——"__proto__",這個屬性引用了創建這個對象的函數的prototype。也就是說,每個對象都可以共同使用它們的父函數的原型對象中的屬性。

    第一節例子中所說的jQery的其實就是根據這個原理實現的:
    $.prototype.trim = function() {
      ....
    }
    
  • 函數的prototype也是一個對象,那么這個對象也有一個"__proto_"屬性,它指向Object.prototype。但是Object.prototype是一個特例——它的__proto_指向的是null

  • 上面所過函數也是一個對象,那么函數的"__proto__"屬性指向什么?

    function fn(){} 等價于=》
    var fn = new Function()
    所以說函數的\_\_proto__就指向Function的prototype。
    而Funtion也是一個函數,既然是函數,那么它一定是被Function創建,它的\_\_proto__就指向它自身的prototype。
    

4.instanceof

  • instanceof用于判斷一個對象的類型,那么它的判斷規則是什么?
    • Instanceof運算符的第一個變量是一個對象,暫時稱為A;第二個變量一般是一個函數,暫時稱為B。

    • Instanceof的判斷規則是:沿著A的__proto__這條線來找,同時沿著B的prototype這條線來找,如果兩條線能找到同一個引用,即同一個對象,那么就返回true。如果找到終點還未重合,則返回false。

      通過以上規則可以解釋很多怪異現象
      Object instanceof Function //true
      //Object是一個函數,所以Object.__proto__指向Function.prototype
      
      Function instanceof Object //true
      //Funtion是一個函數,所以Function.__proto__指向Function.prototype,而Function.prototype.__proto__指向Object.prototype
      

5.繼承/原型鏈

  • JS中的繼承是通過原型鏈實現的,先看下面的例子:

    function Foo() {}
    var f1 = new Foo()
    f1.a = 10
    Foo.prototype.a = 100
    Foo.prototype.b = 200
    f1.a //10
    f1.b //200
    以上代碼中,f1是Foo函數new出來的對象,f1.a是f1對象的基本屬性,f1.b是怎么來的呢?——從Foo.prototype得來,因為f1.__proto__指向的是Foo.prototype
    
  • 訪問一個對象的屬性時,先在基本屬性中查找,如果沒有,再沿著proto這條鏈向上找,這就是原型鏈。

  • 那么我們在實際應用中如何區分一個屬性到底是基本的還是從原型中找到的呢?大家可能都知道答案了——hasOwnProperty,特別是在for…in…循環中,一定要注意。

    f1.hasOwnProperty(b) // false
    f1本身沒有這個方法,它是從Object.prototype中來的。如圖
    
  • 由于所有的對象的原型鏈都會找到Object.prototype,因此所有的對象都會有Object.prototype的方法。這就是所謂的“繼承”。你可以利用原型鏈來實現自己的繼承。

  • 如果繼承的方法不合適,可以做出添加/修改。

    var obj = {a: 1, b: 2}
    obj.toString()  //[object Object]
    var arr = [1, 2, 3]
    arr.toString() // 1,2,3
    Object和Array的toString()方法不一樣。肯定是Array.prototype.toString()方法做了修改。
    

6.執行上下文

  • 先看一段代碼:

    console.log(a)  // referenceError
    
    console.log(b) // undifined
    var b
    
    console.log(c) // undifined
    var c = 1
    
    第一句報錯,a未定義,很正常。第二句、第三句輸出都是undefined,說明瀏覽器在執行console.log(b)時,已經知道了b是undefined,但卻不知道c是1。
    
  • 在一段js代碼拿過來真正一句一句運行之前,瀏覽器已經做了一些“準備工作”,其中就包括對變量的聲明,而不是賦值。變量賦值是在賦值語句執行的時候進行的。可用下圖模擬:


  • 第二種情況:第一種情況只是對變量進行聲明(并沒有賦值),而此種情況直接給this賦值。這也是“準備工作”情況要做的事情之一。


  • 第三種情況:需要注意代碼注釋中的兩個名詞——“函數表達式”和“函數聲明”。雖然兩者都很常用,但是這兩者在“準備工作”時,卻是兩種待遇。


  • 在“準備工作”中,對待函數表達式就像對待“ var a = 10 ”這樣的變量一樣,只是聲明。而對待函數聲明時,卻把函數整個賦值了。

  • 我們總結一下,在“準備工作”中完成了哪些工作:

    • 變量、函數表達式——變量聲明,默認賦值為undefined;
    • this——賦值;
    • 函數聲明——賦值;
    • 這三種數據的準備情況我們稱之為“執行上下文”或者“執行上下文環境”。
  • javascript在執行一個"代碼段"之前,都會進行這些“準備工作”來生成執行上下文。這個“代碼段”其實分三種情況——全局代碼,函數體,eval代碼。eval不常用,也不推薦大家用。

  • 如果在函數中,除了以上數據之外,還會有其他數據。先看以下代碼:


  • 以上代碼展示了在函數體的語句執行之前,arguments變量和函數的參數都已經被賦值。從這里可以看出,函數每被調用一次,都會產生一個新的執行上下文環境。因為不同的調用可能就會有不同的參數。

  • 函數在定義的時候(不是調用的時候),就已經確定了函數體內部變量的作用域。如圖:


  • 給執行上下文環境下一個通俗的定義——在執行代碼之前,把將要用到的所有的變量都事先拿出來,有的直接賦值了,有的先用undefined占個空。

7.this

  • 在函數中this到底取何值,是在函數真正被調用執行的時候確定的,函數定義的時候確定不了。因為this的取值是執行上下文環境的一部分,每次調用函數,都會產生一個新的執行上下文環境。

  • this的取值,分四種情況。

  • 情況1:構造函數:所謂構造函數就是用來new對象的函數。

    • 其實嚴格來說,所有的函數都可以new一個對象,但是有些函數的定義是為了new一個對象,而有些函數則不是。
    • 如果函數作為構造函數用,那么其中的this就代表它即將new出來的對象。
    • 不僅僅是構造函數的prototype,即便是在整個原型鏈中,this代表的也都是當前對象的值。
  • 情況2:函數作為對象的一個屬性

    • 如果函數作為對象的一個屬性時,并且作為對象的一個屬性被調用時,函數中的this指向該對象。
  • 情況3:函數用call或者apply調用

    • 當一個函數被call和apply調用時,this的值就取傳入的對象的值。
  • 情況4:全局 & 調用普通函數

    • 在全局環境下,this永遠是window.
    • 普通函數在調用時,其中的this也都是window。

8.執行上下文棧

  • 執行全局代碼時,會產生一個執行上下文環境,每次調用函數都又會產生執行上下文環境。當函數調用完成時,這個上下文環境以及其中的數據都會被消除,再重新回到全局上下文環境。處于活動狀態的執行上下文環境只有一個。這是一個壓棧出棧的過程。如下圖(藍色表示活動狀態):


9.作用域

  • “javascript沒有塊級作用域”。所謂“塊”,就是大括號“{}”中間的語句。所以,我們在編寫代碼的時候,不要在“塊”里面聲明變量,要在代碼的一開始就聲明好了。以避免發生歧義。
  • javascript除了全局作用域之外,只有函數可以創建的作用域。
  • 所以,我們在聲明變量時,全局代碼要在代碼前端聲明,函數中要在函數體一開始就聲明好。除了這兩個地方,其他地方都不要出現變量聲明。而且建議用“單var”形式。
  • 如上圖,全局代碼和fn、bar兩個函數都會形成一個作用域。而且,作用域有上下級的關系,上下級關系的確定就看函數是在哪個作用域下創建的。例如,fn作用域下創建了bar函數,那么“fn作用域”就是“bar作用域”的上級。
  • 作用域最大的用處就是隔離變量,不同作用域下同名變量不會有沖突。
  • 除了全局作用域之外,每個函數都會創建自己的作用域,作用域在函數定義時就已經確定了。而不是在函數調用時確定。
  • 作用域只是一個“地盤”,一個抽象的概念,其中沒有變量。要通過作用域對應的執行上下文環境來獲取變量的值。同一個作用域下,不同的調用會產生不同的執行上下文環境,繼而產生不同的變量的值。所以,作用域中變量的值是在執行過程中產生的確定的,而作用域卻是在函數創建時就確定了。
  • 如果要查找一個作用域下某個變量的值,就需要找到這個作用域對應的執行上下文環境,再在其中尋找變量的值。
  • 什么是“自由變量”:
    • 在A作用域中使用的變量x,卻沒有在A作用域中聲明(即在其他作用域中聲明的),對于A作用域來說,x就是一個自由變量。
    • 那么到哪里去取自由變量呢? 要到創建這個函數的那個作用域中取值——是“創建”,而不是“調用”
    • 如果跨了一步,還沒找到呢?——接著跨!——一直跨到全局作用域為止。要是在全局作用域中都沒有找到,那就是真的沒有了。這個一步一步“跨”的路線,我們稱之為——作用域鏈。

10.閉包

  • 在一個作用域中引用另一個作用域中的變量就形成了閉包。
  • 閉包應用的兩種情況:
    • 第一,函數作為返回值:


    • 如上代碼,bar函數作為返回值,賦值給f1變量。執行f1(15)時,用到了fn作用域下的max變量的值。
    • 第二,函數作為參數被傳遞:


    • 如上代碼中,fn函數作為一個參數被傳遞進入另一個函數,賦值給f參數。執行f(15)時,max變量的取值是10,而不是100。
  • 有些情況下,函數調用完成之后,其執行上下文環境不會接著被銷毀。這就是需要理解閉包的核心內容。
  • 如上圖當bar()被作為函數返回時,fn函數執行完畢,正常情況下fn的執行上下文環境應該被銷毀,而此時bar函數中有一個自由變量max引用自fn的執行上下文,所以fn的執行上下文就不會被銷毀,否則就找不到max的值了。

11.上下文環境和作用域的關系

  • 上下文環境:
    • 儲存著作用域中所有變量的對象。
    • 對于函數來說,上下文環境是在調用時創建的,這個很好理解。拿參數做例子,你不調用函數,我哪兒知道你要給我傳什么參數?
  • 作用域:
    • 首先,它很抽象。第二,記住一句話:除了全局作用域,只有函數才能創建作用域。創建一個函數就創建了一個作用域,無論你調用不調用,函數只要創建了,它就有獨立的作用域,就有自己的一個“地盤”。
  • 關系:
    • 一個作用域下可能包含若干個上下文環境。有可能從來沒有過上下文環境(函數從來就沒有被調用過);有可能有過,現在函數被調用完畢后,上下文環境被銷毀了;有可能同時存在一個或多個(閉包)。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容