JavaScript系列----函數(Function)篇(4)

1.什麼是函數?

        在W3C中函數的定義是這麼說的:函數是由事件驅動的或者當它被調用時執行的可重複使用的代碼塊。數組

  誠然,從這種抽象的定義中咱們得不到什麼有價值的東西。下面,舉例來列舉出函數的幾種定義方式:
瀏覽器

function add(num1, num2) {
  return num1 + num2;
}
var add = function (num1, num2) {
  return num1 + num2;
}//這是比較常見的兩種

//下面兩種比較少見
var add=new Function("num1","num2","return num1+num2");
var add=Function("num1","num2","return num1+num2");

   上面四種寫法均是建立一個函數正確的語法。可是,常見的通常是前兩種。由於相比於前兩種,後兩種存在着一些缺陷。
閉包

 

  1.  後兩種比較繁瑣不直觀這點從上面的例子中能夠看出。
  2.  後兩種存在着一些致命的缺陷這種函數的建立方式,不能維持一個屬於函數的做用域鏈,在任什麼時候候下(new)Function的建立的函數,都至關於在全局做用域下建立的函 數。如下例子就能夠證實這一點。
    var x = 1;
    var add = (function () { var x = 100; return function (num) { return num + x; } }()); console.log(add(0));//100
    var x = 1;
    var add = (function () { var x = 100; return new Function('num1', 'return num1+x'); }()); console.log(add(0));//1

     也就是說,後兩種方式建立的函數,不能組成完整的函數做用域鏈(後面會講到),也就不可能有所謂的閉包之說。函數

  3.  後兩種的運行效率過低。
     首先,JS對字符串的解析算不上效率很高,而(new)Function均存在着大量的字符串。
     其次,JS的解釋器,對於用function(){},這種形式的函數,都有必定形式的優化。好比下面這樣

    var array = [
    ];
    for (var i = 0; i < 1000; i++) {
    array[i] = function () {
    return 'undefined';
    }
    }//第一種優化


    var array = [
    ];
    for (var i = 0; i < 1000; i++) {
    array[i] = new Function("return undefined");
    }//第二種,this

           這兩種方式在運行效率上存在着很大的差距。對於,第一種只須要執行一次function(){},其餘的999次都是賦值,然後一種要執行一千遍的函數建立並賦值。spa

   正是由於前面的三種緣由,才使得function(){}這種方式比較流行。指針

       另外,你可能也見過下面的這種,可是這種形式只是一種變體。rest

var add = new function () {
  return 1 + 2;
};
console.log(typeof add);//object
var result = add.constructor();/*調用時必須採用這種調用方式*/
console.log(result);//3

        這種形式,new function()建立的實質上是利用一個匿名函數建立一個對象。這個對象的一個constructor屬性正好指向其構造函數,也就是這個匿名函數。因此實際上這是一種醜陋的寫法。code

     到這裏,咱們也就只是敘述了一下,定義函數的幾種方式。經過比較,咱們知道前兩種比較實用,可是即便這樣,第一種和第二中的定義方式也存在着巨大的不一樣。下一小節,咱們接着講這兩種方      式存在的差別。

 

2.函數聲明和函數表達式

 

  函數聲明:

  function 函數名稱 (參數:可選){ 函數體 }

  函數表達式:

  function 函數名稱(可選)(參數:可選){ 函數體 }

因此,能夠看出,若是不聲明函數名稱,它確定是表達式,可若是聲明瞭函數名稱的話,如何判斷是函數聲明仍是函數表達式呢?ECMAScript是通 過上下文來區分的,若是function foo(){}是做爲賦值表達式的一部分的話,那它就是一個函數表達式,若是function foo(){}被包含在一個函數體內,或者位於程序的最頂部的話,那它就是一個函數聲明。

  因此,咱們能夠看出,在第一部分的前兩種建立函數的方式分別爲函數聲明和函數表達式。

function add(num1, num2) {
  return num1 + num2;
}//函數聲明 var add = function (num1, num2) {
  return num1 + num2;
}//函數表達式

 

  另外,還有一些比較容易和函數聲明混淆的函數表達式。 

(function add(num1, num2) {
  return num1 + num2;
});//函數表達式
var add = function foo(num1, num2) {
  return num1 + num2; }//函數表達式

 

 

  ()在JS語言規則中是一個分組操做符,根據W3C標準分組操做符裏面的會默認爲是表達式。

  而下面一種則比較有意思,賦值表達式的左邊是一個函數,關於這點不一樣的解析器對此的處理不一樣,有的認爲這是函數聲明,有的認爲這是一個函數表達式,不一樣的解析器對此的處理各不相同。

可是目前在主流瀏覽器上默認的是函數表達式,並且foo做爲函數標識符,只在其函數內部能被識別。看下面的例題:

var add=function foo(n){
  if(n==1)return 1;
  else return n+foo(n-1);
};

console.log(add(3));//6
console.log(foo(3));//error, foo is not defined

  那麼函數聲明和函數表達式有什麼區別呢?

   回答這些問題,就涉及到函數被調用時的狀況了。

 

3.函數的執行環境和做用域   

 

  咱們都知道,函數運行時是運行在新開闢的棧裏的。那麼函數在運行時,代碼的執行環境是什麼樣的呢? 

  函數被調用時發生了什麼? 

function add(num1, num2) {
  var num3 = 300;
  return num1 + num2 + num3;
}
var result = add(100, 200);
console.log(result);

這一段代碼在執行到第5行的時候會調用咱們聲明的add函數,add函數在被調用時會作如下處理:

1.add函數形參的聲明並賦值。

2.add函數內函數的聲明(若函數變量與函數形參的變量同名,則函數聲明會覆蓋形參聲明)。

3.add函數內變量的聲明。----->函數按順序執行。

下面的幾個例子能夠證實:

例題一:函數內的函數的聲明會覆蓋函數形參的聲明。

function add(num1, num2) {

  console.log(typeof num1);//function
function num1() {}
}
var result = add(100, 200);

例題二:函數內變量的聲明不會覆蓋形參的聲明和函數內函數的聲明

function add(num1, num2) {
  console.log(typeof num1);//number
   var num1="23";
   
}
var result = add(100, 200);

   補充:

   所謂變量的聲明都是相似 var x, y; 這種狀況。而var x=1;

  實際上是 var x ; x=1;分兩步執行。

  變量聲明老是這樣: var  x;

 

函數的執行環境:

  由上述咱們知道,函數執行的時候開闢一個新的棧,而棧內保存着函數內部聲明的變量,變量的值在函數代碼運行以前按照剛纔所討論的三步賦值。也就是說,當一個函數被調用時,在代碼運行以前其棧中已經存在着函數運行時所需的全部的變量。這些變量加一塊兒則構成函數的執行環境。

若是解釋器真的把函數內部全部的聲明都放在棧中的話,那麼解釋器在開闢棧的時候就應該能夠肯定所開闢棧空間的大小。可是若是棧空間大小肯定之後,有如下幾個問題就須要解決了:

  • 變量的類型在運行時被改變。此時棧空間的大小須要不斷調整。
  • 每一個函數在聲明的時候會維持一個屬於本身的做用域鏈,若是做用域鏈上的變量所佔用的空間大小改變的話,須要對整個做用域鏈上的棧調整。
  • 須要針對這種結構重寫垃圾回收機制(標記--清理不適用了)。

改進:引進函數變量

  當函數被調用的時候,並不是將變量的聲明保存在棧中,而是保存在一個對象中。而將這個對象的引用保存在棧中。而這個對象存儲在堆中,具體的工做原理以下: 

function add(num1, num2) {
  var num3 = 300;
  return num1 + num2 + num3;
}
var result = add(100, 200);

 

當函數被調用時(未執行以前),解釋器建立一個addReference對象:

addReference={

  num1:100;

  num2:200;

  num3:undefined;

};

addReference對象的引用被壓入棧中,而對象自己則存在於堆中。

 

補充:

 函數在運行時,棧中還保存着返回值以及this指針。在函數執行完畢退出時,會清空棧,若存在對此函數變量引用的函數,則將此函數變量加入引用函數的做用域鏈上,不然過一段時間,若垃圾回收機制爲未發現有此函數變量的引用,則將該函數變量刪除。

 

改進後:

  • 棧空間大小肯定: 函數運行以前,棧所須要的空間已經能被肯定。只存在三個元素: this指針,函數變量的引用,返回值。(返回值也保存在一個變量中,由解釋器管理)
  • 函數在運行時,若建立一個新的函數。則只須要將函數變量對象加入新函數的做用域鏈上便可。
  • 只須要根據此函數變量的引用計數是否爲0就能夠管理內存,而不須要重寫垃圾回收機制。

 

 

4.函數的做用域鏈 

在第三部分咱們討論出,函數的做用域上保存的都是函數變量。下面咱們經過這個例子來講明這種現象。

 1 var fun;
 2 (function fun1() {
 3   var x = 1;
 4   (function fun2() {
 5     var y = 2;
 6     fun = function () {
 7       return x + y;
 8     }
 9   }());
10 }())
11 
12 var result=fun();
13 console.log(result)//3

 

根據上例,咱們來一步步分析函數執行時都發生了什麼?

  1.  在全局做用域中的變量對象。
    globalReference = {
      .....//之前存在的對象 好比 Object,Math,Date之類 fun: undefined; result: undefined; }
  2. 當函數運行至第二行時,fun1的變量對象
    fun1Reference = {
      x: undefined;
    }

    //執行至第4行的時候,
    fun1Reference = {
    x: 1;
    }
    //fun1的做用域鏈: globalReference
  3. 當函數運行至第四行時,fun2的變量對象
    fun2Reference = {
      y: undefined;
    }
    
    //執行至第5行的時候,
    fun2Reference = {
    y:
    2;
    }
    //fun1的做用域鏈: globalReference--->fun1Reference
  4. 當函數運行至第6行時,fun的做用域鏈
    //fun的做用域鏈: globalReference--->fun1Reference--->fun2Reference
  5. fun2執行完畢退棧--->fun1執行完畢退棧。
  6. 當函數運行至第12行時,fun被調用。
                        globalReference = {
                                    | ....... | fun: undefined; | result: undefined; | } fun1Reference ={x:1;} | | | | fun2Reference={y:2} | | | | funReference:{};
  7. fun執行完畢,result=6;13行,輸出結果。


上述,咱們已經模擬一遍函數的執行時的過程。下面咱們來介紹一下全局做用域對象。

 首先,先明確一點,全局做用域也是一個變量對象。

globalReference = {
  Object:內置的對象構造函數對象;
  Array:內置的數組構造函數對象;
  Function:內置的函數構造函數對象;
  Math:內置的Math對象;
  Date:內置的日期構造函數對象;
  .......;
  window:globalReference
}

 window對象保持這對全局對象的引用。
  補充:在控制檯下執行的代碼都是在eval()函數中執行的,這個函數可以使用而且能改變當前函數所在的執行環境。

 

 5.變量和屬性的區別 

一、變量:能夠被改變的量。只有前面加var(非函數)的才能稱爲變量。函數變量有本身獨特的變量聲明方式。   

var x = 1,y = 2;
var z = 3;
//相似上面這種纔是變量

xxx=1;//這樣的不是變量,下面會講到這種形式

 

二、屬性:一個對象內的變量。

var object={
  x:1,   y:2,   z:3 }; //x,y,z均爲屬性。

 

上面的兩種都很容易區分,可是下面這種又該如何解釋呢?

rest=1;//rest是屬性仍是變量?

這句話通常是在函數執行時候,常常性遇到,這樣寫有很大的弊端。

  1. 查找較慢。rest前面沒有var,則確定不是變量。那麼,在執行的時候就會沿做用域鏈一直向上查找,直至到全局做用域中的變量對象。此時未找到,則根據規則將其做爲全局做用域變量對象的屬性。

  2.改變了全局做用域變量對象。通常來講,咱們在執行代碼的時候應該儘可能避免改變全局做用域對象。

   也就是說,若是咱們使用一個前面沒有加var的「變量」,則在執行期間,會將該「變量」當作全局做用域變量的屬性。

 

三、變量和屬性的區別:

  •   屬性(配置爲可刪除的狀況下)能夠經過其所在的對象被刪除。

    好比:

var object={
x:1
}
delete object.x; //true
y=2;
delete window.y(或者delete y);//true 
  • 變量在聲明的時候會被做爲函數變量的屬性。原則上也是能夠被刪除,可是由於咱們不能獲得函數變量這個對象(window是一個特例),因此在實際操做中,也就致使不可能被刪除。
  • 變量通常是針對聲明時期,而屬性通常針對執行時期。二者在本質上,意義就不同。 
  • 查找普通對象的屬性,若是未找到不會拋出錯誤。可是,查找變量對象的屬性,若是未找到則會拋出錯誤。 
    var object = {
    };
    console.log(object.z);//undefined,-----在普通變量中查找
    console.log(window.v);//undefined -----在普通變量中查找
    console.log(z);//error;           -----在做用域鏈上的變量對象中查找,未找到則報錯。   
相關文章
相關標籤/搜索