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");
上面四種寫法均是創建一個函數正確的語法。但是,常見的一般是前兩種。因為相比於前兩種,后兩種存在着一些缺陷。
- 后兩種比較繁瑣不直觀。這點從上面的例子中可以看出。
- 后兩種存在着一些致命的缺陷。這種函數的創建方式,不能維持一個屬於函數的作用域鏈,在任何時候下(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
也就是說,后兩種方式創建的函數,不能組成完整的函數作用域鏈(后面會講到),也就不可能有所謂的閉包之說。
- 后兩種的運行效率太低。
首先,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
根據上例,我們來一步步分析函數執行時都發生了什么?
- 在全局作用域中的變量對象。
globalReference = {
.....//以前存在的對象 比如 Object,Math,Date之類 fun: undefined; result: undefined; } - 當函數運行至第二行時,fun1的變量對象
fun1Reference = { x: undefined; }
//執行至第4行的時候,
fun1Reference = {
x: 1;
}
//fun1的作用域鏈: globalReference - 當函數運行至第四行時,fun2的變量對象
fun2Reference = { y: undefined; }
//執行至第5行的時候,
fun2Reference = {
y: 2;
}//fun1的作用域鏈: globalReference--->fun1Reference
- 當函數運行至第6行時,fun的作用域鏈
//fun的作用域鏈: globalReference--->fun1Reference--->fun2Reference
- fun2執行完畢退棧--->fun1執行完畢退棧。
- 當函數運行至第12行時,fun被調用。
globalReference = { | ....... | fun: undefined; | result: undefined; | } fun1Reference ={x:1;} | | | | fun2Reference={y:2} | | | | funReference:{};
- 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是屬性還是變量?
這句話一般是在函數執行時候,經常性遇到,這樣寫有很大的弊端。
- 查找較慢。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; -----在作用域鏈上的變量對象中查找,未找到則報錯。