JavaScript之Function函數深刻總結

整理了JavaScript中函數Function的各類,感受函數就是一大對象啊,各類知識點都能牽扯進來,不僅僅是 Function 這個自己原生的引用類型的各類用法,還包含執行環境,做用域,閉包,上下文,私有變量等知識點的深刻理解。javascript

函數中的returnhtml

  1.  return 語句能夠不帶有任何返回值,在這種狀況下( return; 或函數中不含 return 語句時),函數在中止執行後將返回 undefiend 值。這種用法通常在須要提早中止函數執行而又不須要返回值的狀況下。
  2.  return false 能夠取消元素的默認行爲,但不能阻止事件傳播。只能是return false,return別的值不行,這裏的處理方式是DOM的事件處理機制作出的約定。DOM的事件傳播有兩個類型,一個是捕獲(從父節點到子節點),一個是冒泡(從子節點到父節點),因此一個事件觸發時能夠有多個處理器去處理它,事實上,僅僅是在HTML事件屬性 和 DOM0級事件處理方法中 才能經過返回 return false 的形式組織事件宿主的默認行爲。詳見:https://www.zhihu.com/question/19867421
  3.  return 返回的是其所在函數的返回值
    function n(){
      (function(){
         return 5;
      })();
    }
    n();// undefined
    //當即執行匿名函數中的return語句實際上是返回給它所在的匿名函數的。
    
    function n(){
      var num= (function(){
         return 5;
      })();
      console.log(num);
    }

 

Function類型
函數其實是對象,每一個函數實際上都是 Function 類型的實例。並且與其餘引用類型同樣具備屬性和方法。函數名其實是一個指向內存堆中某個函數對象的指針。前端

定義函數的方式java

  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;
    }

    當使用不帶圓括號的函數名是訪問函數指針,而非調用函數。git

 

理解參數
ECMAScript中全部參數傳遞的都是值(即便是引用也是傳遞的地址值,不是引用傳遞參數(可參考 JavaScript傳遞參數是按值傳遞仍是按引用傳遞))。ECMAScript函數不介意傳遞進來多少個參數,也不在意傳進來的參數是什麼數據類型。之因此這樣,是由於ECMAScript中的參數在內部是用一個數組表示的。函數接收到的始終都是這個數組,而不關心數組中包含哪些參數。在函數體內,能夠經過 arguments 對象來訪問這個數組。從而獲取傳遞給函數的每一個參數。github

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

 

沒有重載web

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; 
    }

 

函數聲明和函數表達式chrome

實際上解析器在向執行環境中加載數據時,對函數聲明和函數表達式並不是一視同仁。
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是保存它們全部實例方法的真正所在。諸如toStringvalueOf等方法實際上都保存在Object.prototype名下(原生構造函數好比Function,Array等 在本身原型上重寫了toString)。在ECMAScript5中,prototype屬性是不可枚舉的,所以使用for-in沒法發現。 Object.getOwnPropertyDescriptor(Function,'prototype');// Object {writable: false, enumerable: false, configurable: false}
  • 每一個函數上有兩個可用的方法:applycall。這兩個方法其實是在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" ,變量值爲函數時 typeof function(){};// "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. 閉包還可用於在對象中建立私有變量
    經過閉包來實現公有方法,經過公有方法能夠訪問在包含做用域中定義的變量。有權訪問私有變量的公有方法叫特權方法。可使用構造函數模式,原型模式來實現自定義類型的特權方法,使用模塊模式,加強的模塊模式實現單例的特權方法。

 

參考:
《JavaScript高級程序設計》

深刻解讀JavaScript面向對象編程實踐

JS核心系列:漫談JS引擎的運行機制

相關文章
相關標籤/搜索