JavaScript Function 函式深入總結

venoral發表於2016-03-19

整理了JavaScript中函式Function的各種,感覺函式就是一大物件啊,各種知識點都能牽扯進來,不單單是 Function 這個本身原生的引用型別的各種用法,還包含執行環境,作用域,閉包,上下文,私有變數等知識點的深入理解。

函式中的return

  1.  return 語句可以不帶有任何返回值,在這種情況下( return; 或函式中不含 return 語句時),函式在停止執行後將返回 undefiend 值。這種用法一般在需要提前停止函式執行而又不需要返回值的情況下。
  2.  return false 可以阻止元素的預設事件。
  3.  return 返回的是其所在函式的返回值
    function n(){
      (function(){
         return 5;
      })();
    }
    n();// undefined
    //立即執行匿名函式中的return語句其實是返回給它所在的匿名函式的。
    
    function n(){
      var num= (function(){
         return 5;
      })();
      console.log(num);
    }

Function型別

函式實際上是物件,每個函式實際上都是 Function 型別的例項。而且與其他引用型別一樣具有屬性和方法。函式名實際上是一個指向記憶體堆中某個函式物件的指標。

定義函式的方式

  1. 函式宣告
    function sum(num1,num2){
      return num1+num2;
    }
  2. 函式表示式
    var sum=function(num1,num2){
     return num1+num2;
    };

    定義了一個變數 sum 並將其初始化為一個函式,注意到 function 關鍵字後面並沒有函式名,這是因為在使用函式表示式定義函式,沒必要使用函式名,通過變數 sum 即可引用函式。還要注意函式末尾有個分號,就像宣告其他變數一樣。

  3.  new 建構函式,雖然這種用法也是函式表示式,但該用法不推薦。因為這種語法會導致解析兩次程式碼(第一次是解析常規的ECMAScript程式碼,第二次是解析傳入建構函式中的字串),影響效能。
    使用 Function 建構函式,建構函式可以接受任意數量的引數,但最後一個引數始終都被看成是函式體,前面的引數則列舉出了新函式的引數。
    var sum=new Function('num1','num2','return num1+num2;');
    sum;// 
    function anonymous(num1,num2
    /**/) {
    return num1+num2;
    }

    當使用不帶圓括號的函式名是訪問函式指標,而非呼叫函式。

理解引數

ECMAScript中所有引數傳遞的都是值(即使是引用也是傳遞的地址值,不是引用傳遞引數(可參考JavaScript傳遞引數是按值傳遞還是按引用傳遞))。ECMAScript函式不介意傳遞進來多少個引數,也不在乎傳進來的引數是什麼資料型別。之所以這樣,是因為ECMAScript中的引數在內部是用一個陣列表示的。函式接收到的始終都是這個陣列,而不關心陣列中包含哪些引數。在函式體內,可以通過 arguments 物件來訪問這個陣列。從而獲取傳遞給函式的每個引數。

function func(){
 console.log(Object.prototype.toString.call(arguments));
}

func();// [object Arguments]
  1. 關於 arguments 的行為,它的值永遠與對應命名引數的值保持同步。因為 arguments 物件中的值會自動反映到對應的命名引數。所以修改 arguments[1] ,也就修改了 num2 。不過這並不是說讀取這兩個值會訪問相同的記憶體空間,它們的記憶體空間是獨立的,但他們值會同步(WHY??),要是JavaScript能直接訪問記憶體就好了驗證一下。
  2. 但如果只傳入了一個引數,那麼 arguments[1] 設定的值不會反映到命名引數中,這是因為 arguments 物件的長度是由傳入引數個數決定的,不是由定義函式時的命名引數個數決定的,沒有傳遞值的命名引數將自動被賦予 undefiend 值,這就跟定義了變數但沒初始化一樣。
    function doAdd(num1,num2){
      console.log(arguments.length);
      console.log(num2)
      arguments[1]=10;
      console.log(num2);
    }
    doAdd(5,0);//2 0 10
    
    doAdd(5);//1 undefiend undefined

沒有過載

ECMAScript函式不能像傳統意義上那樣實現過載,而在其他語言中(Java),可以為一個函式編寫兩個定義,只要這兩個定義的簽名(接收引數的型別和數量)不同即可。

不能實現過載的原因:

  1. ECMAScript函式沒有簽名,因為其引數是由包含零個或多個值的陣列來表示的。沒有函式簽名,真正的過載是不可能做到的。在ECMAScript中定義兩個名字相同的的函式,則該名字只屬於後定義的函式。如何實現類似於Java中的過載呢,其實可以通過判斷傳入函式的引數型別和個數來做出不同響應。
    function reload(){
       if(arguments.length==0){
           console.log('沒傳參');
       }else if(arguments.legth==1){
          console.log('傳了一個引數');
      }
    }
  2. 深入理解:將函式名想象為指標,也有助於理解為什麼ECMAScript中沒有函式過載的概念。
    function add(){
      return 100;
    }
    function add(num){
     return num+200; 
    }
    
    //實際上和下面程式碼沒什麼區別
    function add(){
      return 100;
    }
    add=function(num){
     return num+200; 
    }

函式宣告和函式表示式

實際上解析器在向執行環境中載入資料時,對函式宣告和函式表示式並非一視同仁。

JavaScript執行機制淺探 中瞭解到對於解釋型語言來說,編譯步驟為:

  1. 詞法分析(將字元流轉換為記號流,是一對一的硬性翻譯得到的是一堆難理解的記號流)
  2. 語法分析(這裡進行所謂的變數提升操作,其實我覺得是把這些提升的變數儲存在語法樹中。要構造語法樹,若發現無法構造就會報語法錯誤,並結束整個程式碼塊的解析)
  3. 之後可能有語義檢查,程式碼優化等。得到語法樹後就開始解釋執行了。解釋性語言沒有編譯成二進位制程式碼而是從語法樹開始執行。

解析器會先讀取函式宣告,並使其在執行任何程式碼之前可用。至於函式表示式,則必須等到執行階段才會被真正賦值。什麼意思呢?雖然兩者都進行了變數提升,待真正執行時構造活動物件從語法樹種取宣告新增到執行環境中,但一個是函式提升,一個是變數提升。

//函式宣告
console.log(func);//function func(){}
function func(){

}

//函式表示式
console.log(func1);// undefined
var func1=function(){};
console.log(func1);// function(){}

作為值的函式

因為ECMAScript中的函式名本身就是變數,所以函式也可以作為值來使用。不僅可以像傳遞引數一樣把一個函式傳遞給另一個函式,而且可以將一個函式作為另一個函式的結果返回。

function callSomeFunction(someFunction,someArgument){
  return someFunction(someArgument);
}

function concated(str){
  return "Hi "+str;
}

callSomeFunction(concated,'xx');// 'Hi xx'

從一個函式中返回另一個函式的應用:假設有一個物件陣列,想要根據某個物件屬性對陣列進行排序,但傳給 sort() 方法的比較函式要接收兩個引數,即要比較的。我們需要一種方式來指明按照哪個屬性來排序。我們可以定義一個函式它接收一個屬性名,然後根據這個屬性名來建立一個比較函式。預設情況下, sort 函式會呼叫每個物件的 toString() 方法以確定它們的次序。

function createCompare(property){
  return function(obj1,obj2){
    var value1=obj1[property],
        value2=obj2[property];   
    if(value1<value2) return -1;
    else if(value1>value2)  return 1;
    else return 0;
  }
}
var data=[{name:'aa',age:20},{name:'bb',age:12},{name:'cc',age:30}];
data.sort(createCompare("age"));// [{name:'bb',age:12},{name:'aa',age:20},{name:'bb',age:30}]

函式的內部屬性

arguments :類陣列物件,包含傳入函式中所有引數。是每個函式自身的屬性,之所以可以直接訪問 arguments ,是因為名稱空間??以下變化是為了加強JavaScript語言的安全性,這樣第三方程式碼就不能在相同的環境下窺視其他程式碼了。

  •  callee 屬性:是一個指標,指向擁有 arguments 物件的函式。嚴格模式訪問會導致錯誤。
    //一般階乘函式
    function factorial(num){
       if(num<=1){ return 1;}
       else {
         return num*factorial(num-1);
      }
    }

    定義階乘函式用到遞迴演算法,這樣定義是沒問題。
    缺點:這個函式的執行與函式名 factorial 緊緊耦合在一起。萬一出現改變函式指向的這種情況就不太好了,

    factorial=function(){}
    factorial(3);// undefiend

    為了消除這種現象。

    function factorial(num){   
       if(num<=1){     return 1;    }
       else{     
          return num*arguments.callee(num-1);    
      } 
    }
  • 這樣無論引用函式使用的是什麼名字都可以保證完成遞迴。
  •  caller 屬性:不過在非嚴格模式下這個屬性始終是 undefiend 。即使在嚴格模式下訪問也會出錯。增加這個屬性是為了分清 arguments.caller 和函式物件上的 caller 屬性。
    function a(){
    return Object.getOwnPropertyNames(arguments);
    }
    a();// ["length", "callee"]

this :行為與Java/C#中的 this 大致類似。 this 引用的是函式據以執行環境物件(當在網頁的全域性作用域中呼叫函式時, this 物件引用的就是 window )。

caller :不止是ECMAScript5中新增函式物件上的屬性,還是 arguments 上的屬性。儲存著呼叫當前函式的函式的引用。如果是在全域性作用域中呼叫當前函式,它的值為 null 。
Object.getOwnPropertyNames(Function);// ["length", "name", "arguments", "caller", "prototype"]

function outer(){
  inner();
}
function inner(){
  console.log(inner.caller); //為了實現更鬆散的耦合,arguments.callee.caller
}

outer();// function outer(){ inner()}

嚴格模式下不能為函式的 caller 屬性賦值,否則會導致出錯。

函式的屬性和方法

  • length:表示函式希望接收的命名引數的個數(也就是定義的形參的個數)。
    function sayName(name){
      //
    }
    function sum(num1,num2){
      //
    }
    function sayHi(){
     // 
    }
    
    sayName.length;// 1
    sum.length;// 2
    sayHi.length;// 0
  • prototype:對於ECMAScript中的引用型別而言,prototype是儲存它們所有例項方法的真正所在。諸如toString和valueOf等方法實際上都儲存在Object.prototype名下(原生建構函式比如Function,Array等 在自己原型上重寫了toString)。在ECMAScript5中,prototype屬性是不可列舉的,因此使用for-in無法發現。 Object.getOwnPropertyDescriptor(Function,’prototype’);//Object {writable: false, enumerable: false, configurable: false}
  • 每個函式上有兩個可用的方法:apply和call。這兩個方法實際上是在Function.prototype上, Object.getOwnPropertyNames(Function.prototype);// ["length", "name", "arguments", "caller", "apply", "bind", "call", "toString", "constructor"] 它是在JavaScript引擎內部實現的。因為是屬於Function.prototype,所以每個Function的例項都可以用(自定義的函式也是Function的例項)。都是在特定的作用域或自定義的上下文中呼叫執行函式,實際上等於設定函式體內 this 物件的值。
  1.  apply :引數一為在其中執行函式的作用域,引數二為引數陣列(可以是陣列,也可以是 arguments 物件)。
    function sum(num1,num2){
      return num1+num2;
    }
    
    function callSum1(num1,num2){
      return sum.apply(this,arguments);//sum.apply(this,[num1,num2])
    }
    
    callSum1(10,30);// 40

    嚴格模式下,未指定環境物件而呼叫函式, this 值不會轉型為 window 。除非明確把函式新增到某個物件或者呼叫 apply 或 call ,否則 this 值將是 undefined

  2.  call :引數一沒有變化,變化的是其餘引數都是直接傳遞給函式,引數必須都列出來。
    function callSum1(num1,num2){
      retrun sum.call(this,num1,num2);
    }
    
    callSum1(10,30);// 40

    call 和 apply 真正強大的地方是能夠擴充函式賴以執行的作用域,改變函式的執行環境。

  3.  bind :ECMAScript5定義的方法,也是 Function.prototype 上的方法。用於控制函式的執行上下文,返回一個新函式,這個函式的 this 值會被繫結到傳給 bind() 函式中的值。
    window.color="red";
    var o={color:'blue'};
    function sayColor(){
      console.log(this.color);
    }
    
    var newobj=sayColor.bind(o);
    newobj;// function sayColor(){
      console.log(this.color);
    }
    newobj==sayColor;// false
    newobj();// blue

    深入理解:可以將函式繫結到指定環境的函式。接收一個函式和一個環境,返回在給定環境中呼叫給定函式的函式。

    function bind(func,context){
      return function(){
        func.apply(context,arguments);//這裡建立了一個閉包,arguments使用的返回的函式的,而不是bind的
      }
    }

    當呼叫返回的函式時,它會在給定環境中執行被傳入的函式並給出所有引數。

    function bind(func,context,args){
       return function(){
          func.call(context,args);
       };
    }
  4.  toString,toLocaleString :返回函式程式碼的字串形式,返回格式因瀏覽器而異,有的返回原始碼,有的返回函式程式碼的內部表示,由於存在差異,用這個也實現不了什麼功能。
  5.  valueOf :返回函式的自身引用。

變數,作用域,記憶體問題

JavaScript接近詞法作用域,變數的作用域是在定義時決定而不是在執行時決定,也就是說詞法作用域取決於原始碼。

JavaScript引擎在執行每個函式例項時,都會為其建立一個執行環境,執行環境中包含一個AO變數物件,用來儲存內部變數表,內嵌函式表,父級引用列表等語法分析結構(變數提升在語法分析階段就已經得到了,並儲存在語法樹中,函式例項執行時會將這些資訊複製到AO上)。

ECMA-262定義,JavaScript鬆散型別的本質決定了它只在特定時間用於儲存特定值的一個名字而已,由於不存在定義某個變數必須要儲存何種資料型別值得規則,變數的值及其資料型別可在指令碼的生命週期內改變。

  • 基本型別和引用型別的值:ECMAScript變數可能包含兩種不同資料型別的值:基本型別值,引用型別值。
  1. 基本型別值:簡單的資料段。
  2. 引用型別值:那些可能由多個值構成的物件。是儲存在記憶體中的物件,JavaScript不允許直接訪問記憶體中的位置,也就說不能直接操作物件的記憶體空間。在操作物件時實際上是在操作物件的引用而不是實際的物件。為此,引用型別值是按引用訪問的。(這種說法不嚴密,當複製儲存著物件的某個變數時,操作的是物件的引用。但在為物件新增屬性時,操作的是實際的物件)
    在將一個值賦給變數時,解析器必須確定這個值是基本型別值還是引用型別值。5種基本資料型別: Undefined,Null,Boolean,Number,String (很多語言中字串以物件形式來表示因此被認為是引用型別,但ECMAScript放棄這一傳統)。這5種基本型別是按值訪問的,因此可以操作儲存在變數中的實際的值。
  • 動態的屬性
  • 複製變數的值:在從一個變數向另一個變數複製基本型別值和引用型別值時,也存在不同。
    如果從一個變數向另一個變數複製基本型別的值,會在變數物件上建立一個新值,然後把該值複製到為新變數分配的位置上。
    當從一個變數向另一個變數賦值引用型別值值時,同樣也會將儲存在變數物件中的值複製一份放到為新變數分配的空間中,不同的是,這個值的副本實際上是個指標(可以理解為複製了地址值),而這個指標指向儲存在堆中一個物件。複製操作結束後兩個變數實際上將引用同一個物件。
  • 傳遞引數:ECMAScript中所有函式的引數都是按值傳遞的,把函式外部的值複製給函式內部的引數,就和把值從一個變數複製到另一個變數一樣。基本型別值得傳遞如同基本型別變數的複製一樣,引用型別值的傳遞如同引用型別變數的複製一樣。很多人錯誤認為:在區域性作用域中修改的物件會在全域性作用域中反映出來這就說明是按引用傳遞的。為了證明物件是按值傳遞的,
    function setName(obj){
      obj.name="xx";
      obj=new Object();
      obj.name="bb";
    }
    
    var p=new Object();
    setName(p);
    p.name;// "xx"

    如果是按引用傳遞的,即傳遞的不是地址值而是堆記憶體中整個p物件,在 setName 中為其新增了一個新名字叫 obj ,又給其新增 name 屬性後,將這個 obj 內容重新填充為新物件,那麼之前的那個物件就不存在了更別說有 ”xx” 的名字屬性,但是 p.name 仍然訪問到了。這說明即使在函式內部修改了引數值,但原始的引用仍然保持未變。實際上,當在函式內部重寫 obj 時,這個變數引用的就是一個區域性物件了,而這個區域性物件會在函式執行完畢後被立即銷燬。

  • 型別檢測:檢測一個變數是不是基本資料型別用 typeof 是最佳工具,但如果變數的值是除了函式的物件或 null  typeof [];// ”object” typeof null;// ”object” ,變數值為函式時 typeoffunction(){};// ”function” (ECMA-262規定任何在內部實現 [[call]] 方法的物件都應該在應用 typeof 操作符返回 ”function” )。但在檢測引用型別值時,這個操作符用處不大,因為我們並不是想知道它是個物件,而是想知道它是某種型別物件。如果變數是給定引用型別的例項, instanceof 操作符會返回 true 。所有引用型別值都是 Object 的例項。如果使用 instanceof 操作符檢測基本型別的值,則該操作符始終會返回 false ,因為基本型別不是物件。

執行環境及作用域

  • 執行環境(execution context):也稱為作用域,定義了變數或函式有權訪問的其他資料,決定了它們各自的行為。全域性執行環境是最外圍的一個執行環境,跟據ECMAScript實現所在的宿主環境不同,表示執行環境的物件也不一樣,web瀏覽器中全域性執行環境是 window 物件。某個執行環境中所有程式碼執行完畢後該環境被銷燬,儲存在其中的所有變數和函式定義也隨之銷燬(全域性執行環境直到應用程式退出例如關閉網頁或瀏覽器時才被銷燬)。每個函式都有自己的執行環境,當執行流進入一個函式時,函式的環境就會被推入一個環境棧中,在函式執行後,棧將其環境彈出,將控制權返回給之前的執行環境。ECMAScript程式中的執行流正是由這個機制控制著。函式的每次呼叫都會建立一個新的執行環境。執行環境分為建立和執行兩個階段,
  1. 建立:解析器初始化變數物件或者活動物件,它由定義在執行環境中的變數,函式宣告,引數組成。在這個階段,作用域鏈會被初始化,this的值也最終會被確定。
  2. 執行:程式碼被解釋執行
  • 變數物件(variable object):環境中定義的所有變數和函式都儲存在這個物件中。雖然用程式碼無法訪問它,但解析器在處理資料時會在後臺使用它。如果這個環境是函式,則將活動物件(activation object)作變數物件
  • 作用域(scope)和上下文(context):函式的每次呼叫都有與之緊密相關的作用域和上下文。作用域是基於函式的,上下文是基於物件的。作用域涉及到被調函式中變數的訪問,上下文始終是 this 關鍵字的值,它是擁有當前所執行程式碼的物件的引用。上下文通常取決於函式是如何被呼叫的。
  • 作用域鏈(scope chain):當程式碼在一個環境中執行時,會建立變數物件的一個作用域鏈。它是保證對執行環境有權訪問的所有變數和函式的有序訪問。作用域鏈的前端始終都是當前執行的程式碼所在環境的變數物件。活動物件在最開始時只包含一個變數即 arguments 物件(這個物件在全域性環境中不存在),作用域鏈的下一個變數物件來自包含(外部)環境,再下一個變數物件則來自下一個包含環境,這樣一直延續到全域性執行環境。
    var color = "blue";
    function changeColor(){
      if(color=="blue"){
         color="red";
      }else{
         color="blue";
      }
    }
    changeColor();
    console.log(color);// red

    識別符號解析是沿著作用域鏈一級一級地搜尋識別符號的過程,函式 changeColor 作用域鏈包含兩個物件:它自己的變數物件(其中定義著 arguments 物件)和全域性環境的變數物件。可以在函式內部訪問到變數 color 就是因為可以在這個作用域鏈中找到它。內部環境可以通過作用域鏈訪問所有外部環境,但外部環境不能訪問內部環境的任何變數和函式。函式引數也被當作變數來對待,因此其訪問規則與執行環境中的其他變數相同。

  • 延長作用域鏈:有些語句可以在作用域的前端臨時新增一個變數物件,該變數物件會在程式碼執行後被移除。當執行流進入下列語句時,作用域鏈就會加長。
  1.  try-catch 語句的 catch 塊:對 catch 語句來說,會建立一個新的變數物件,其中包含的是被丟擲的錯誤物件的宣告。<=IE8版本中,在 catch 語句中捕獲的錯誤物件會被新增到執行環境的變數物件而不是 catch 語句的變數物件,換句話說,即使是在 catch 塊的外部也可以訪問到錯誤物件。
  2.  with 語句:會將指定的物件新增到作用域鏈中。
    function buildUrl(){
       var qs="?debug=true";
       with(location){
          var url=href+qs;
       }
      return url;
    }
    buildUrl();// "http://i.cnblogs.com/EditPosts.aspx?postid=5280805?debug=true"

    with 語句接收的是一個 location 物件,因此其變數物件中就含有 location 物件的所有屬性和方法,且這個變數物件被新增到了作用域鏈的最前端。當在 with 語句中引用變數 href (實際引用的是 location.href )可以在當前的執行環境中找到,當引用變數 qs 時,引用的則是在下一級執行環境中的變數。由於JavaScript中沒有塊級作用域,所以在函式內部可以訪問 url 才能 return 成功,說明 url 並不是新增到 location 所在的變數物件中。
    這兩個語句都會在作用域的前端新增一個變數物件。

  • 沒有塊級作用域:在其他類C的語言中,由花括號封閉的程式碼塊都有自己的作用域(如果用ECMAScript的話來講,就是他們自己的執行環境),因而支援根據條件來定義變數。如果是在C/C++/Java中, color 會在 if 語句執行完後被銷燬,但在JavaScript中, if 語句中的變數宣告會將變數新增到當前的執行環境中。
    if(true){
      var color="red";
    }
    
    console.log(color);// red
  1. 宣告變數:使用 var 宣告的變數會自動被新增到最接近的環境中。在函式內部,最接近的環境就是函式的區域性環境;在 with 語句中,最接近的環境是函式環境。如果初始化變數時沒有使用 var 宣告,該變數會自動被新增到全域性環境。
  2. 查詢識別符號:當在某個環境中為了讀取或寫入而引用一個識別符號時,必須通過搜尋來確定該識別符號代表什麼。搜尋過程從作用域鏈的前端開始,向上逐級查詢與給定名字匹配的識別符號。如果在區域性環境中找到了該識別符號,搜尋過程停止,變數就緒。如果在區域性環境中未找到該變數名,則繼續沿作用域鏈向上搜尋。搜尋過程將一直追溯到全域性環境。如果區域性環境存在同名識別符號,就不會使用位於父環境中的識別符號。

函式表示式

if(condition){
  function sayHi(){
      console.log("Hi");
  }
}else{
  function sayHi(){
      console.log("Yo");
  }
}

以上程式碼會在 condition 為 true 時使用 sayHi() 的定義,否則就使用另一個定義。實際上這在ECMAScript中屬於無效語法,JavaScript引擎會嘗試修正錯誤,將其轉換為合理的狀態。但問題是瀏覽器嘗試修正的做法不一樣。大多數瀏覽器會返回第二個宣告。此種方式很危險,不應該出現你的程式碼中。在chrome中:

if(true){
  function sayHi(){
      console.log("Hi");
  }
}else{
  function sayHi(){
      console.log("Yo");
  }
}//function sayHi(){ 沒有函式宣告的變數提升??
      console.log("Hi");
  }
if(false){
  function say(){
      console.log("Hi");
  }
}else{
  function say(){
      console.log("Yo");
  }
}//function say(){
      console.log("Yo");
  }
console.log(sa);//undefined 能輸出undefiend說明函式宣告並沒有提升而是進行的變數提升
if(false){
  function sa(){
      console.log("Hi");
  }
}else{
  function sa(){
      console.log("Yo");
  }
}//function sa(){
      console.log("Yo");
  }

修正:使用函式表示式,那就沒什麼問題了。

var sayHi;
if(condition){
  sayHi=function(){
     console.log("Hi");
  }
}else{
  sayHi=function(){
     console.log("Yo");
  }
}

遞迴

在嚴格模式下,不能通過指令碼訪問 arguments.callee 。不過可以使用命名函式表示式來完成相同結果。

var factorial=(function f(num){
    if(num<=1){
        return 1;
    }else{
        return num*f(num-1);
   } 
});

注意是用命名函式表示式,單單把命名函式賦值給 factorial 也可以,但是並不能通過f的名字訪問

閉包

閉包是指有權訪問另一個函式作用域中的變數的函式。建立閉包的常見方式,就是在一個函式內部建立另一個函式。之所以能夠訪問外部作用域的變數,是因為內部函式的作用域鏈中包含外部作用域。當一個函式被呼叫的時候,

  1. 建立一個執行環境(execution context)及相應的作用域鏈
  2. 使用 arguments 和其他命名引數的值來初始化活動物件(activation object),但在作用域鏈中,外部函式的活動物件始終始終處於第二位…直至作為作用域鏈終點的全域性執行環境。
    function compare(value1,value2){
       if(value1<value2){
          return -1;
       }else if(value1>value2){
          return 1;
       }else{
          return 0;
       }
    }
    
    var result=compare(5,10);

    當呼叫 compare() 時,會建立一個包含 arguments , value1 , value2 的活動物件,全域性執行環境的變數物件(包含 result 和 compare )在 compare() 執行環境的作用域鏈中處於第二位。

    後臺的每個執行環境都有一個表示變數的物件(變數物件),全域性環境的變數物件始終存在,而像 compare() 函式這樣的區域性環境的變數物件,則只在函式執行過程中存在。在建立 compare() 函式時,會建立一個預先包含全域性物件的作用域鏈,這個作用域鏈被儲存在 compare 內部的 [[Scope]] 屬性中。當呼叫 compare() 函式時,會為函式建立一個執行環境,然後通過複製函式的 [[Scope]] 屬性中的物件構建起執行環境的作用域鏈。此後又有一個活動物件被建立並被推入執行環境作用域鏈的最前端。對於這個例子中, compare 函式的執行環境而言,其作用鏈包含兩個變數物件:本地活動物件和全域性變數物件。顯然,作用域鏈的本質上是一個指向變數物件的指標列表,它只引用但不包含實際的變數物件。

無論什麼時候在函式中訪問一個變數,就會從作用域鏈中搜尋具有相應名字的變數,一般來講當函式執行完後,區域性活動物件會被銷燬,記憶體中僅保留著全域性作用域(全域性執行環境的變數物件)。但是閉包的情況又有所不同。在另一個函式內部定義的函式會將包含函式(外部函式)的活動物件新增到它的作用域鏈裡,當外部函式執行完後其活動物件不會被銷燬,因為匿名函式的作用域鏈仍然在引用這個活動物件。換句話說只是外部函式它自己的作用域鏈被銷燬,但活動物件還存在記憶體中。直到內部函式被銷燬後(例如在外部解除了對閉包即內部函式的引用: func=null; ,解除相當於是閉包僅是執行完後),外部函式的活動物件才會被銷燬。

由於閉包會攜帶包含它的函式的作用域,因此會比其他函式佔用更多的記憶體。過多使用閉包可能會導致記憶體佔用過多,建議只在絕對必要再考慮使用。但有的優化後的JavaScript引擎如V8會嘗試回收被閉包占用的記憶體。

閉包缺點:作用域鏈的這種配置機制引出了一個副作用即閉包只能取得包含函式中任何變數的最後一個值。因為閉包儲存的是整個變數物件,而不是某個特殊的變數。

function createFunctions(){
  var result=new Array();
  for(var i=0;i<3;i++){
     result[i]=function(){
        return i;
     };
  }
  return result;
}
createFunctions()[0]();// 3
createFunctions()[1]();// 3
createFunctions()[2]();// 3

當執行 createFunctions 時,它的活動物件裡有 arguments=[] , result=undefiend , i=undefiend ,執行完 createFunctions 後, result=[function(){return i},function(){return i},function(){return i}],i=3 ;當此時執行 result 陣列時,訪問到的i的值總是為3,因為沿著 function(){return i;} 的作用域鏈查詢變數,在外層函式的活動物件上找到i總是為3。陣列中每個函式的作用域鏈中都儲存著 createFunctions 的活動物件,所以這些函式們引用的都是同一個活動物件,同一個變數i。

解決方案:要的就是當時執行時的變數i,那麼當時把這個i臨時儲存一下就可以了,但是儲存在哪呢?將i儲存在 function(){return i;} 的活動物件中,怎麼儲存呢?傳給 arguments 就好了,只傳進來還不行

function createFunctions(){
  var result=new Array();
  for(var i=0;i<3;i++){
     result[i]=function(i){
        return i;
     };
  }
  return result;
}

createFunctions()[0]();// undefiend

因為訪問i的時候先從自己所在函式的執行環境的活動物件搜尋起,找到i發現 i=undefiend 有值就停止向上搜尋了。問題就出在上一步中將i儲存在活動物件中, result[i]=function(i){return i;} 這句的執行並沒有給匿名函式傳參,這只是表示式的賦值操作,又不是執行匿名函式。所以現在需要的就是通過某種方式去執行函式的操作把i的值當實參傳進去,簡單!在匿名函式外部加一層立即執行的匿名函式(這也增加了一層作用域了)。

function createFunctions(){
  var result=new Array();
  for(var i=0;i<3;i++){
     result[i]=(function(i){
         return function(){
              return i;
          }
       })(i);
   }
  return result;
}

createFunctins()[0]();// 0

this物件

this 物件是在執行時基於函式的執行環境繫結的:

  1. 全域性函式中, this 等於 window
  2. 函式被作為某個物件的方法呼叫時, this 等於那個物件
  3. 匿名函式的執行環境具有全域性性, this 指向 window
  4. 通過 call() 或 apply() 改變函式執行環境的情況下, this 就會指向其他物件。

由於閉包編寫的方式不同, this 的表現:

var name="the window";

var obj={
   name:"the obj",
   getNameFunc:function(){
       //console.log(this==obj);
       return function(){
         console.log(this.name);
     }
  }
}

obj.getNameFunc()();// the window

obj.getNameFunc() 返回了一個新函式,然後在再全域性環境中執行該函式。為什麼匿名函式沒有取得其包含作用域(外部作用域)的 this 物件呢?每個函式在被呼叫時,都會自動獲得兩個特殊的變數: this (建立作用域時獲得)和 arguments (建立活動物件獲得),內部函式在搜尋這兩個變數時,只會搜尋到自己的活動物件為止,因此永遠不可能直接訪問外部函式的這兩個變數。不過把外部函式作用域的 this 儲存在一個閉包能夠訪問到的變數裡就可以讓閉包訪問該物件了。

下面幾種情況特殊的 this :

var name="the window";
var obj={
   name:"the obj",
   getName:function(){
       return this.name;
   }
};

obj.getName();// "the obj"
(obj.getName)();// "the obj"
(obj.getName=obj.getName)();// "the window"

第一個是直接呼叫,第二個是呼叫後立即執行的表示式,第三個是執行了一條賦值語句,然後再呼叫返回的結果,賦值語句的返回了一個函式,然後全域性環境下呼叫這個函式,見下圖

模仿塊級作用域

function outputNumber(count){
  for(var i=0;i<count;i++){
     console.log(i);
  }
  var i;// 只變數提升,到後面執行程式碼步驟時候略過此
  console.log(i);
}

outputNumber(3);// 0 1 2 3

JavaScript不會告訴你是否多次宣告瞭同一個變數,遇到這種情況,它只會對後續的宣告視而不見(不過它會執行後續宣告中的變數初始化)。

匿名函式可以用來模仿塊級作用域(私有作用域),語法如下:

(function(){
  //這裡是塊級作用域
})();

以上程式碼定義並立即呼叫了一個匿名函式,將函式宣告包含在一對圓括號中,表示它實際上是一個函式表示式。對於這種語法的理解:

var count=5;
outputNumbers(count);

這裡初始化了變數 count 將其值設為5。但是這裡的變數是沒有必要的,因為可以把值直接傳給函式  outputNumbers(5);  這樣做之所以可行,是因為變數不過是值的另一種表現形式,因此用實際的值替換變數沒有問題。

var someFunc=function(){
   //這裡是塊級作用域 
};
someFunc();

既然可以使用實際的值來取代變數 count ,那這裡也用實際的值替換函式名。

function(){
  //這裡是塊級作用域
}();

然而會報錯,是因為JavaScript將 function 關鍵字當作一個函式宣告的開始,而函式宣告後面不能跟圓括號。但是函式表示式後面可以圓括號,這也就是為什麼這樣可以執行

var someFunc=function(){
 //這裡是塊級作用域
}();

要將函式宣告轉化為函式表示式,

(function(){
   //這裡是塊級作用域
})();
function outputNumber(count){
  (function(){
     for(var i=0;i<count;i++){
       console.log(i);
     }
  })();
   console.log(i);// 報錯
}

outputNumber(3);// 0 1 2

在 for 迴圈外邊加了一個私有作用域,在匿名函式中定義的任何變數都會在執行結束時被銷燬。在私有作用域中訪問變數 count ,是因為這個匿名函式是一個閉包,它能訪問包含作用域的所有變數。這種技術經常在全域性作用域中被用在函式外部從而限制向全域性作用域中新增過多的變數和函式。這種做法還可以減少閉包占用記憶體問題,因為沒有指向匿名函式的引用,只要函式執行完畢,就可以立即銷燬其作用域鏈了。

私有變數

嚴格來講,JavaScript中沒有私有成員的概念,所有物件屬性都是公有的。不過有私有變數的概念,任何在函式中定義的變數,都可認為是私有變數,因為不能在函式外部訪問這些變數。私有變數包括函式的引數,區域性變數,在函式內定義的其他函式。如果在函式內部建立一個閉包,那麼閉包通過自己的作用域也可以訪問這些變數。利用這一點建立用於訪問私有變數的公有方法。

把有權訪問私有變數和私有函式的方法叫特權方法(privileged method)。

  • 兩種在自定義物件上建立特權方法的方式
  1. 在建構函式中定義特權方法
    function MyObject(){
       //私有變數和私有函式
       var privateVariable=10;
       function privateFunction(){
          return false;
      } 
      // 特權方法
      this.publicMethod=function(){
         privateVariable++;
         return privateFunction();
      };
    
    }
    
    new MyObject();

    這個模式在建構函式內部定義了所有私有變數和函式,又繼續建立了能夠訪問這些私有成員的特權方法。能在建構函式中定義特權方法是因為特權方法作為閉包有權訪問在建構函式中定義的所有變數和函式。對這個例子而言,變數 privateVariable 和方法 privateFunction 只能通過特權方法 publicMethod 訪問。在建立 MyObject 例項後除了使用 publicMethod() 這一途徑外沒任何辦法可以直接訪問私有變數和函式。
    利用私有和特權成員,可以隱藏那些不應該被直接修改的資料

    function Person(name){
      this.getName=function(){
         return name;
      };
      this.setName=function(value){
         name=value;
      };
    }
    
    var p1=new Person("aa");
    p1.getName();// "aa"
    
    var p2=new Person("bb");
    p2.getName();// "bb"
    
    p1.getName();// "aa"

    以上方法定義兩個特權方法,在 Person 建構函式外部沒有任何辦法直接訪問 name ,由於這兩個方法是在建構函式內部定義的,它們作為閉包能夠通過作用域鏈訪問 name 。私有變數 name 在每個 Person 例項都不相同,這麼說吧,每次呼叫建構函式都會重新建立這兩個方法, p1.getName 和 p2.getName 是不同的函式,雖然呼叫的是記憶體中同一個 Person 函式。但 new 構造新例項的步驟是:先建立新例項物件;再在該例項上呼叫 Person 函式初始化作用域及作用域鏈 this 等;再新增屬性等。不管換成是

    var o1={},o2={};
    Person.call(o1,'aa');
    Person.call(o2,'bb');
    o1.getName();// "aa"

    還是換成

    function Person(obj,name){
      obj.getName=function(){
         return name;
      };
      obj.setName=function(value){
         name=value;
      };
    }
    var o1={},o2={};
    Person(o1,"aa");
    Person(o2,"bb");
    o1.getName();// "aa"

    都呼叫了兩次 Person ,因為每次呼叫 Person 就會初始化 Person 的作用域,所以 p1.getName 和 p2.getName 所處的外圍作用域是不一樣的(之前還認為因為是呼叫了記憶體中同一個 Person ,以為 p1.getName 和 p2.getName 有同一個外圍作用域,沒考慮到每次呼叫函式例項都會重新初始化作用域)。
    缺點:在建構函式中定義特權方法要求你必須使用建構函式模式來達到這個目的。建構函式模式的缺點是針對每個例項都會建立同一組新方法,使用靜態私有變數來實現特權方法就可以避免這個問題。

  2.  靜態私有變數通過在私有作用域中定義私有變數和函式,也可以建立特權方法。基本模式如下:
    (function(){
       //私有變數和私有函式
       var privateVariable=10;
       function privateFunction(){
           return false;
       }
       //建構函式
       MyObject=function(){};
       //公有/特權方法
       MyObject.prototype.publicMethod=function(){
           privateVariable++;
           return privateFunction();
      };
    })();

    這個模式建立了個私有作用域,並在其中封裝了一個建構函式和相應方法。公有方法在原型上定義,這一點體現典型原型模式。注意到這個模式在定義建構函式時並沒使用函式宣告,而是使用函式表示式,因為函式宣告只能建立區域性函式,我們也沒有在宣告 MyObject 時使用 var 關鍵字,就是想讓它成為一個全域性變數,能夠在私有作用域之外被訪問。但嚴格模式下未經宣告的變數賦值會導致出錯。可以修改為

    'use strict';
    var MyObject;
    (function(){
       //私有變數和私有函式
       var privateVariable=10;
       function privateFunction(){
           return false;
       }
       //建構函式
       MyObject=function(){};
       //公有/特權方法
       MyObject.prototype.publicMethod=function(){
           privateVariable++;
           return privateFunction();
      };
    })();

    其實我覺得不用立即執行的匿名函式也可以實現這種在私有作用域中定義私有變數函式的模式,只要把這些放在一個函式中就可以了,然後再執行這個函式。

    function staticFunction(){
       //私有變數和私有函式
       var privateVariable=10;
       function privateFunction(){
           return false;
       }
       //建構函式
       MyObject=function(){};
       //公有/特權方法
       MyObject.prototype.publicMethod=function(){
           privateVariable++;
           return privateFunction();
      };
    }
    
    staticFunction();

    —–分割線—-
    這種模式在與建構函式中定義特權方法的主要區別就在於私有變數和函式是由例項共享的因為只呼叫了即只初始化了一次父環境(意思就是 p1.getName 和 p2.getName 所在的父環境都是同一個,不像建構函式模式中那樣擁有各自父環境)。由於特權方法是在原型上定義的,因此所有例項都使用同一個函式。而這個特權方法作為一個閉包總是儲存著對包含作用域的引用。

    (function(){
       var name="";
       Person=function(value){
          name=value;
       };
       Person.prototype.getName=function(){
          return name;
       };
       Person.prototype.setName=function(value){
          name=value;
       };
    })();
    
    var p1=new Person("aa");
    p1.getName();// "aa"
    var p2=new Person("bb");
    p2.getName();// "bb"
    p1.getName();// "bb"

    Person 建構函式和 getName 和 setName 都有權訪問私有變數 name 。在這種模式下,變數 name 就成了一個靜態的,由所有例項共享的屬性。在一個例項上呼叫 setName() 會影響所有例項。
    以這種模式建立靜態私有變數會因為使用原型而增進程式碼複用,但每個例項都沒有自己的私有變數。
    多查詢作用域鏈中的一個層次,就會在一定程度上影響查詢速度,這正是使用閉包和私有變數的一個不足之處。

  • 模組模式:前面的模式用於為自定義型別建立私有變數和特權方法。道格拉斯所說的模組模式則是為單例建立私有變數和特權方法,所謂單例(singleton)指的就是隻有一個例項的物件,JavaScript是以物件字面量的方式來建立單例物件的。
    var singleton={
      name:value,
      method:function(){
         //這裡是方法的程式碼
      }
    };

    模組模式通過為單例新增私有變數和特權方法能夠使其得到增強。語法如下:

    var singleton=function(){
       //私有變數和私有函式
       var privateVariable=10;
       function privateFunction(){
           return false;
       }
       //特權/公有方法和屬性
       return {
          publicProperty:true,
          publicMethod:function(){
              privateVariable++;
              return privateFunction();
          }
       } 
    }();

    這個模式使用了一個返回物件的匿名函式,將一個物件字面量作為函式返回。本質上這個物件字面量定義的是一個單例的公共介面。這種模式在需要對單例進行某些初始化同時又需要維護其私有變數時是非常有用的。

    var application=function(){
       //私有變數和函式
       var components=new Array();
       //初始化
       components.push(new BaseComponent());
       //公共
       return {
          getComponentCount:function(){
              return components.length;
          },
          registerComponent:function(component){
              if(typeof component=="object"){
                  components.push(component);
              }
          }
       }
    }();

    在web應用程式中,經常需要使用一個單例來管理應用程式級的資訊。如果必須建立一個物件並以某些資料對其進行初始化,同時還要公開一些能夠訪問這些私有資料的方法,那就可以使用模組模式。這種模式建立的每個單例都是 Object 的例項。

  • 增強的模組模式:如果想讓單例是某種型別的例項,改進了模組模式,在返回物件之前加入對其增強的程式碼。同時還必須新增某些屬性和方法對其加以增強。
    var singleton=function(){
      //私有變數和私有函式
      var privateVariable=10;
      function privateFunction(){
        return false;
      }
      //建立物件
      var obj=new CustomType();
      //新增特權/公有屬性和方法
      obj.publicProperty=true;
      obj.publicMethod=function(){
          privateVariable++;
          return privateFunction();
      }
      return obj;
    }();
    var application=function(){
      //私有變數和函式
      var components=new Array();
      //初始化
      components.push(new BaseComponent());
      //建立application的一個區域性版本
      var app=new BaseComponent();
      app.getComponentCount=function(){
         return components.length;
      }; 
      app.registerComponent=function(component){
         if(typeof component=="object"){
             components.push(component);
         }
      };
      return app;
    }();

閉包的作用總結:

  1. 使用閉包可以在JavaScript中模仿塊級作用域
    建立並立即呼叫一個函式,這樣既可以執行其中程式碼,又不會在記憶體中留下對該函式的引用。結果就是函式內部的所有變數都會被立即銷燬除非將某些變數賦值給了包含作用域中的變數
  2. 閉包還可用於在物件中建立私有變數
    通過閉包來實現公有方法,通過公有方法可以訪問在包含作用域中定義的變數。有權訪問私有變數的公有方法叫特權方法。可以使用建構函式模式,原型模式來實現自定義型別的特權方法,使用模組模式,增強的模組模式實現單例的特權方法。

相關文章