在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");
上面四種寫法均是建立一個函數正確的語法。可是,常見的通常是前兩種。由於相比於前兩種,後兩種存在着一些缺陷。
閉包
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
也就是說,後兩種方式建立的函數,不能組成完整的函數做用域鏈(後面會講到),也就不可能有所謂的閉包之說。函數
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
到這裏,咱們也就只是敘述了一下,定義函數的幾種方式。經過比較,咱們知道前兩種比較實用,可是即便這樣,第一種和第二中的定義方式也存在着巨大的不一樣。下一小節,咱們接着講這兩種方 式存在的差別。
函數聲明:
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
那麼函數聲明和函數表達式有什麼區別呢?
回答這些問題,就涉及到函數被調用時的狀況了。
咱們都知道,函數運行時是運行在新開闢的棧裏的。那麼函數在運行時,代碼的執行環境是什麼樣的呢?
函數被調用時發生了什麼?
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指針。在函數執行完畢退出時,會清空棧,若存在對此函數變量引用的函數,則將此函數變量加入引用函數的做用域鏈上,不然過一段時間,若垃圾回收機制爲未發現有此函數變量的引用,則將該函數變量刪除。
改進後:
在第三部分咱們討論出,函數的做用域上保存的都是函數變量。下面咱們經過這個例子來講明這種現象。
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
根據上例,咱們來一步步分析函數執行時都發生了什麼?
globalReference = {
.....//之前存在的對象 好比 Object,Math,Date之類
fun: undefined;
result: undefined;
}
fun1Reference = {
x: undefined;
}
//執行至第4行的時候,
fun1Reference = {
x: 1;
}
//fun1的做用域鏈: globalReference
fun2Reference = {
y: undefined;
}
//執行至第5行的時候,
fun2Reference = {
y: 2;
}
//fun1的做用域鏈: globalReference--->fun1Reference
//fun的做用域鏈: globalReference--->fun1Reference--->fun2Reference
globalReference = {
| ....... | fun: undefined; | result: undefined; | } fun1Reference ={x:1;} | | | | fun2Reference={y:2} | | | | funReference:{};
上述,咱們已經模擬一遍函數的執行時的過程。下面咱們來介紹一下全局做用域對象。
首先,先明確一點,全局做用域也是一個變量對象。
globalReference = {
Object:內置的對象構造函數對象;
Array:內置的數組構造函數對象;
Function:內置的函數構造函數對象;
Math:內置的Math對象;
Date:內置的日期構造函數對象;
.......;
window:globalReference
}
window對象保持這對全局對象的引用。
補充:在控制檯下執行的代碼都是在eval()函數中執行的,這個函數可以使用而且能改變當前函數所在的執行環境。
一、變量:能夠被改變的量。只有前面加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是屬性仍是變量?
這句話通常是在函數執行時候,常常性遇到,這樣寫有很大的弊端。
2.改變了全局做用域變量對象。通常來講,咱們在執行代碼的時候應該儘可能避免改變全局做用域對象。
也就是說,若是咱們使用一個前面沒有加var的「變量」,則在執行期間,會將該「變量」當作全局做用域變量的屬性。
三、變量和屬性的區別:
好比:
var object={ x:1 } delete object.x; //true y=2; delete window.y(或者delete y);//true
var object = { }; console.log(object.z);//undefined,-----在普通變量中查找 console.log(window.v);//undefined -----在普通變量中查找 console.log(z);//error; -----在做用域鏈上的變量對象中查找,未找到則報錯。