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");
    }//第二種,

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

   正是因為前面的三種原因,才使得function(){}這種方式比較流行。

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

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

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

     到這里,我們也就只是敘述了一下,定義函數的幾種方式。通過比較,我們知道前兩種比較實用,但是即使這樣,第一種和第二中的定義方式也存在着巨大的不同。下一小節,我們接着講這兩種方      式存在的差異。

 

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.變量和屬性的區別 

1、變量:可以被改變的量。只有前面加var(非函數)的才能稱為變量。函數變量有自己獨特的變量聲明方式。   

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

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

 

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

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

 

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

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

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

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

  2.改變了全局作用域變量對象。一般來說,我們在執行代碼的時候應該盡量避免改變全局作用域對象。

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

 

3、變量和屬性的區別:

  •   屬性(配置為可刪除的情況下)可以通過其所在的對象被刪除。

    比如:

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;           -----在作用域鏈上的變量對象中查找,未找到則報錯。   

     


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM