變數類型
在說函數之前,先來說說變數類型。
1、變數:變數在本質上就是命名的記憶體空間。
2、變數的資料類型:就是指變數可以儲存的值的資料類型,比如Number類型、Boolean類型、Object類型等,在ECMAScript中,變數的資料類型是動態,可以在運行時改變變數的資料類型。
3、變數類型:是指變數本身的類型,在ECMAScript中,變數類型就只有兩種:實值型別和參考型別。當變數的資料類型是單一資料型別時,變數類型就是實值型別,當變數的資料類型是物件類型時,變數類型就是參考型別。在不引起歧義的情況下,也可以稱變數的資料類型為變數類型。
那麼,實值型別和參考型別有什麼區別呢?最主要的一個,就是當變數類型為實值型別時,變數儲存的就是變數值本身,而當變數類型為參考型別時,變數儲存的並不是變數值,而只是一個指向變數值的指標,訪問參考型別的變數值時,首先是取到這個指標,然後是根據這個指標去擷取變數值。如果將一個參考型別的變數值賦給另一個變數,最終結果是這兩個變數同時指向了一個變數值,修改其中一個會同時修改到另一個: 複製代碼 代碼如下:var a = {
name:'linjisong',
age:29
};
var b = a;//將參考型別的變數a賦給變數b,a、b同時指向了a開始指向的那個對象
b.name = 'oulinhai';//修改b指向的對象,也就是修改了a指向的對象
console.info(a.name);//oulinhai
b = {//將變數重新賦值,但是b原來指向的對象沒有變更,也就是a指向的對象沒有變化
name:'hujinxing',
age:23
};
console.info(a.name);//oulinhai
好了,關於變數類型先說到這,如果再繼續到記憶體儲存資料結構的話,就怕沉得下去浮不上來。
函數
如果說對象是房間,那麼函數就是有魔幻效應的房間了。函數首先是對象,然後這個函數對象還具有很多魔幻功能……
1、函數
(1)函數是對象
函數也是一種對象,而用於建立函數對象執行個體的函數就是內建的Function()函數(建立對象執行個體需要函數,而函數又是一種對象執行個體,是不是讓你有了先有雞還是先有蛋的困惑?別鑽牛角尖了,只要雞能生蛋,蛋能孵雞就行了,誰先誰後還是留給哲學家吧),但是函數這種對象,又和一般的對象有著極大的不同,以至於對函數對象執行個體使用typeof時返回的不是object而是function了。
(2)函數名是指向函數對象的參考型別變數 複製代碼 代碼如下:function fn(p){
console.info(p);
}
console.info(fn);//fn(p),可以將fn作為一般變數來訪問
var b = fn;
b('function');//function,可以對b使用函數調用,說明b指向的對象(也就是原來fn指向的對象)是一個函數
註:關於函數名,在ES5的strict 模式下,已經不允許使用eval和arguments了,當然,參數名也不能用這兩個了(我想除非你是專業駭客,否則也不會使用這些作為標識符來使用吧)。
2、函數建立
(1)作為一種對象,函數也有和普通對象類似的建立方式,使用new調用建構函式Function(),它可以接受任意數量的參數,最後一個參數作為函數體,而前面的所有參數都作為函數的形式參數,前面的形式參數還可以使用逗號隔開作為一個參數傳入,一般形式為: 複製代碼 代碼如下:var fn = new Function(p1, p2, ..., pn, body);
//或者
var fn = Function(p1, p2, ..., pn, body);
//或者
var fn = new Function("p1, p2, ..., pn", q1, q2, ..., qn, body);
//或者
var fn = Function("p1, p2, ..., pn", q1, q2, ..., qn, body);
例如: 複製代碼 代碼如下:var add = new Function('a','b','return a + b;');
console.info(add(2,1));//3
var subtract = Function('a','b','return a - b;');
console.info(subtract(2,1));//1
var sum = new Function('a,b','c','return a + b + c;');
console.info(sum(1,2,3));//6
這種方式建立函數,會解析兩次代碼,一次正常解析,一次解析函數體,效率會影響,但是比較適合函數體需要動態編譯的情況。
(2)由於函數對象本身的特殊性,我們還可以使用關鍵字function來建立函數: 複製代碼 代碼如下:function add(a, b){
return a + b;
}
console.info(add(2,1));//3
var subtract = function(a, b){
return a - b;
};
console.info(subtract(2,1));//1
從上可以看到,使用function關鍵字建立函數也有兩種方式:函式宣告和函數運算式。這兩種方式都能實現我們想要的效果,那他們之間有什麼區別呢?這就是我們下面要講的。
3、函式宣告和函數運算式
(1)從形式上區分,在ECMA-262的規範中,可以看到: 複製代碼 代碼如下:函式宣告: function Identifier (參數列表(可選)){函數體}
函數運算式:function Identifier(可選)(參數列表(可選)){函數體}
除了函數運算式的標識符(函數名)是可選的之外沒有任何區別,但我們也可以從中得知:沒有函數名的一定是函數運算式。當然,有函數名的,我們就只能從上下文來判斷了。
(2)從上下文區分,這個說起來簡單,就是:只允許運算式出現的上下文中的一定是函數運算式,只允許聲明出現的內容相關的一定是函式宣告。舉一些例子: 複製代碼 代碼如下:function fn(){};//函式宣告
//function fn(){}(); // 異常,函式宣告不能直接調用
var fn = function fn(){};//函數運算式
(function fn(){});//函數運算式,在分組操作符內
+function fn(){console.info(1);}();//1,函數運算式,出現在操作符+之後,因此可以直接調用,這裡,也可以使用其它的操作符,比如new
new function fn(){console.info(2);}();//2,函數運算式,new操作符之後
(function(){
function fn(){};//函式宣告
});
(3)區別:我們為什麼要花這麼大力氣來區分函式宣告和函數運算式呢?自然就是因為它們的不同點了,他們之間最大的不同,就是聲明會提升,關於聲明提升,在前面基礎文法的那一篇文章中,曾經對全域範圍中的聲明提升做過討論,我們把那裡的結論複習一下:
A、引擎在解析時,首先會解析函式宣告,然後解析變數聲明(解析時不會覆蓋類型),最後再執行代碼;
B、解析函式宣告時,會同時解析類型(函數),但不會執行,解析變數聲明時,只解析變數,不會初始化。
在那裡也舉了一些例子來示範(回憶一下),不過沒有同名稱的聲明例子,這裡補充一下: 複製代碼 代碼如下:console.info(typeof fn);//function,聲明提升,以函數為準
var fn = '';
function fn(){
}
console.info(typeof fn);//string,由於已經執行了代碼,這裡fn的類型變為string
try{
fn();//已經是string類型,不能調用了,拋出類型異常
}catch(e){
console.info(e);//TypeError
}
fn = function(){console.info('fn');};//如果想調用fn,只能再使用函數運算式賦值給fn
fn();//fn,可以調用
console.info(typeof gn);//function
function gn(){
}
var gn = '';
console.info(typeof gn);//string
可以看出:不管變數聲明是在前還是在後,在聲明提升時都是以函式宣告優先,但是在聲明提升之後,由於要執行變數初始化,而函式宣告不再有初始化(函數類型在提升時已經解析),因此後面輸出時就成為String類型了。
上面第3行定義了一個函數,然後第7行馬上調用,結果竟然不行!你該明白保持全域命名空間清潔的重要性了吧,要不然,你可能會遇到“我在代碼中明明定義了一個函數卻不能調用”這種鬼事情,反過來,如果你想確保你定義的函數可用,最好就是使用函數運算式來定義,當然,這樣做你需要冒著破壞別人代碼的風險。
還有一個問題,這裡我們怎麼確定變數類型是在初始化時候而不是在變數聲明提升時候改變的呢?看下面的代碼: 複製代碼 代碼如下:console.info(typeof fn);//function
function fn(){
}
var fn;
console.info(typeof fn);//function
可以看到,聲明提升後類型為function,並且由於沒有初始化代碼,最後的類型沒有改變。
關於函式宣告和函數運算式,還有一點需要注意的,看下面的代碼: 複製代碼 代碼如下:if(true){
function fn(){
return 1;
}
}else{
function fn(){
return 2;
}
}
console.info(fn());// 在Firefox輸出1,在Opera輸出2,在Opera中聲明提升,後面的聲明會覆蓋前面的同層級聲明
if(true){
gn = function(){
return 1;
};
}else{
gn = function(){
return 2;
};
}
console.info(gn());// 1,所有瀏覽器輸出都是1
在ECMAScript規範中,命名函數運算式的標識符屬於內部範圍,而函式宣告的標識符屬於定義範圍。 複製代碼 代碼如下:var sum = function fn(){
var total = 0,
l = arguments.length;
for(; l; l--)
{
total += arguments[l-1];
}
console.info(typeof fn);
return total;
}
console.info(sum(1,2,3,4));//function,10
console.info(fn(1,2,3,4));//ReferenceError
上面是一個命名函數運算式在FireFox中的運行結果,在函數範圍內可以訪問這個名稱,但是在全域範圍中訪問出現引用異常。不過命名函數運算式在IE9之前的IE瀏覽器中會被同時作為函式宣告和函數運算式來解析,並且會建立兩個對象,好在IE9已經修正。
除了全域範圍,還有一種函數範圍,在函數範圍中,參與到聲明提升競爭的還有函數的參數。首先要明確的是,函數範圍在函數定義時不存在的,只有在函數實際調用才有函數範圍。 複製代碼 代碼如下:// 參數與內部變數,參數優先
function fn(inner){
console.info(inner);// param
console.info(other);// undefined
var inner = 'inner';
var other = 'other';
console.info(inner);// inner
console.info(other);// other
}
fn('param');
// 參數與內建函式,內建函式優先
function gn(inner){
console.info(inner);// inner()函數
console.info(inner());// undefined
function inner(){
return other;
}
var other = 'other';
console.info(inner);// inner()函數
console.info(inner());// other
}
gn('param');
通過上面的輸出結果,我們得出優先順序:內建函式聲明 > 函數參數 > 內部變數聲明。
這裡面的一個過程是:首先內建函式聲明提升,並將函數名的類型設定為函數類型,然後解析函數參數,將傳入的實際參數值賦給形式參數,最後再內部變數聲明提升,只提升聲明,不初始化,如果有重名,同優先順序的後面覆蓋前面的,不同優先順序的不覆蓋(已經解析了優先順序高的,就不再解析優先順序低的)。
說明一下,這隻是我根據輸出結果的推斷,至於後台實現,也有可能步驟完全相反,並且每一步都覆蓋前一步的結果,甚至是從中間開始,然後做一個優先順序標誌確定是否需要覆蓋,當然,從效率上來看,應該是我推斷的過程會更好。另外,全域範圍其實就是函數範圍的一個簡化版,沒有函數參數。
這裡就不再舉綜合的例子了,建議將這篇文章和前面的基礎文法那一篇一起閱讀,可能效果會更好。關於優先順序與覆蓋,也引出下面要說的一個問題。
4、函數重載
函數是對象,函數名是指向函數對象的參考型別變數,這使得我們不可能像一般物件導向語言中那樣實現重載: 複製代碼 代碼如下:function fn(a){
return a;
}
function fn(a,b){
return a + b;
}
console.info(fn(1)); // NaN
console.info(fn(1,2));// 3
不要奇怪第8行為什麼輸出NaN,因為函數名只是一個變數而已,兩次函式宣告會依次解析,這個變數最終指向的函數就是第二個函數,而第8行只傳入1個參數,在函數內部b就自動賦值為undefined,然後與1相加,結果就是NaN。換成函數運算式,也許就好理解多了,只是賦值了兩次而已,自然後面的賦值會覆蓋前面的: 複製代碼 代碼如下:var fn = function (a){ return a; }
fn = function (a,b){ return a + b;}
那麼,在ECMAScript中,怎麼實現重載呢?回想一下單一資料型別封裝對象(Boolean、Number、String),既可以作為建構函式建立對象,也可以作為轉換函式轉換資料類型,這是一個典型的重載。這個重載其實在前一篇文章中我們曾經討論過:
(1)根據函數的作用來重載,這種方式的一般格式為: 複製代碼 代碼如下:function fn(){
if(this instanceof fn)
{
// 功能1
}else
{
// 功能2
}
}
這種方式雖然可行,但是很明顯作用也是有限的,比如就只能重載兩次,並且只能重載包含建構函式的這種情形。當然,你可以結合apply()或者call()甚至ES5中新增的bind()來動態綁定函數內部的this值來擴充重載,但這已經有了根據函數內部屬性重載的意思了。
(2)根據函數內部屬性來重載 複製代碼 代碼如下:function fn(){
var length = arguments.length;
if(0 == length)//將字面量放到左邊是從Java中帶過來的習慣,因為如果將比較操作符寫成了賦值操作符(0=length)的話,編譯器會提示我錯誤。如果你不習慣這種方式,請原諒我
{
return 0;
}else if(1 == length)
{
return +arguments[0];
}else{
return (+arguments[0])+(+arguments[1]);
}
}
console.info(fn());//0
console.info(fn(1));//1
console.info(fn(true));//1
console.info(fn(1,2));//3
console.info(fn('1','2'));//3
這裡就是利用函數內部屬性arguments來實現重載的。當然,在內部重載的方式可以多種多樣,你還可以結合typeof、instanceof等操作符來實現你想要的功能。至於內部屬性arguments具體是什嗎?這就是下面要講的。
5、函數內部屬性arguments
簡單一點說,函數內部屬性,就是只能在函數體內訪問的屬性,由於函數體只有在函數被調用的時候才會去執行,因此函數內部屬性也只有在函數調用時才會去解析,每次調用都會有相應的解析,因此具有動態特性。這種屬性有:this和arguments,這裡先看arguments,在下一篇文章中再說this。
(1)在函數定義中的參數列表稱為形式參數,而在函數調用時候實際傳入的參數稱為實際參數。一般的類C語言,要求在函數調用時實際參數要和形式參數一致,但是在ECMAScript中,這兩者之間沒有任何限制,你可以在定義的時候有2個形式參數,在調用的時候傳入2個實際參數,但你也可以傳入3個實際參數,還可以只傳入1個實際參數,甚至你什麼參數都不傳也可以。這種特性,正是利用函數內部屬性來實現重載的基礎。
(2)形式參數甚至可以取相同的名稱,只是在實際傳入時會取後面的值作為形式參數的值(這種情況下可以使用arguments來訪問前面的實際參數): 複製代碼 代碼如下:function gn(a,a){
console.info(a);
console.info(arguments[0]);
console.info(arguments[1]);
}
gn(1,2);//2,1,2
gn(1);//undefined,1,undefined
這其實也可以用本文前面關於聲明提升的結論來解釋:同優先順序的後面的覆蓋前面的,並且函數參數解析時同時解析值。當然,這樣一來,安全性就很成問題了,因此在ES5的strict 模式下,重名的形式參數被禁止了。
(3)實際參數的值由形式參數來接受,但如果實際參數和形式參數不一致怎麼辦呢?答案就是使用arguments來儲存,事實上,即便實際參數和形式參數一致,也存在arguments對象,並且保持著和已經接受了實際參數的形式參數之間的同步。將這句話細化一下來理解:
•arguments是一個類數組對象,可以像訪問數組元素那樣通過方括弧和索引來訪問arguments元素,如arguments[0]、arugments[1]。
•arguments是一個類數組對象,除了繼承自Object的屬性和方法(有些方法被重寫了)外,還有自己本身的一些屬性,如length、callee、caller,這裡length表示實際參數的個數(形式參數的個數?那就是函數屬性length了),callee表示當前函數對象,而caller只是為了和函數屬性caller區分而定義的,其值為undefined。
•arguments是一個類數組對象,但並不是真正的數組對象,不能直接對arguments調用數組對象的方法,如果要調用,可以先使用Array.prototype.slice.call(arguments)先轉換為數組對象。
•arguments儲存著函數被調用時傳入的實際參數,第0個元素儲存第一個實際參數,第1個元素儲存第二個實際參數,依次類推。
•arguments儲存實際參數值,而形式參數也儲存實際參數值,這兩者之間有一個同步關係,修改一個,另一個也會隨之修改。
•arguments和形式參數之間的同步,只有當形式參數實際接收了實際參數時才存在,對於沒有接收實際參數的形式參數,不存在這種同步關係。
•arguments對象雖然很強大,但是從效能上來說也存有一定的損耗,所以如果不是必要,就不要使用,建議還是優先使用形式參數。 複製代碼 代碼如下:fn(0,-1);
function fn(para1,para2,para3,para4){
console.info(fn.length);//4,形式參數個數
console.info(arguments.length);//2,實際參數個數
console.info(arguments.callee === fn);//true,callee對象指向fn本身
console.info(arguments.caller);//undefined
console.info(arguments.constructor);//Object(),而不是Array()
try{
arguments.sort();//類數組畢竟不是數組,不能直接調用數組方法,拋出異常
}catch(e){
console.info(e);//TypeError
}
var arr = Array.prototype.slice.call(arguments);//先轉換為數組
console.info(arr.sort());//[-1,0],已經排好序了
console.info(para1);//0
arguments[0] = 1;
console.info(para1);//1,修改arguments[0],會同步修改形式參數para1
console.info(arguments[1]);//-1
para2 = 2;
console.info(arguments[1]);//2,修改形式參數para2,會同步修改arguments[1]
console.info(para3);//undefined,未傳入實際參數的形式參數為undefined
arguments[2] = 3;
console.info(arguments[2]);//3
console.info(para3);//undefined,未接受實際參數的形式參數沒有同步關係
console.info(arguments[3]);//undefined,未傳入實際參數,值為undefined
para4 = 4;
console.info(para4);//4
console.info(arguments[3]);//undefined,為傳入實際參數,不會同步
}
經過測試,arguments和形式參數之間的同步是雙向的,但是《JavaScript進階程式設計(第3版)》中第66頁說是單向的:修改形式參數不會改變arguments。這可能是原書另一個Bug,也可能是FireFox對規範做了擴充。不過,這也讓我們知道,即便經典如此,也還是存有Bug的可能,一切當以實際運行為準。
•結合arguments及其屬性callee,可以實現在函數內部調用自身時與函數名解耦,這樣即便函數賦給了另一個變數,而函數名(別忘了,也是一個變數)另外被賦值,也能夠保證運行正確。典型的例子有求階乘函數、斐波那契數列等。 複製代碼 代碼如下://求階乘
function factorial(num){
if(num <= 1)
{
return 1;
}else{
return num * factorial(num - 1);
}
}
var fn = factorial;
factorial = null;
try{
fn(2);//由於函數內部遞迴調用了factorial,而factorial已經賦值為null了,所以拋出異常
}catch(e){
console.info(e);//TypeError
}
//斐波那契數列
function fibonacci(num){
if(1 == num || 2 == num){
return 1;
}else{
return arguments.callee(num - 1) + arguments.callee(num - 2);
}
}
var gn = fibonacci;
fibonacci = null;
console.info(gn(9));//34,使用arguments.callee,實現了函數對象和函數名的解耦,可以正常執行
遞迴的演算法非常簡潔,但因為要維護運行棧,效率不是很好。關於遞迴的最佳化,也有很多非常酣暢漓淋的演算法,這裡就不深入了。
需要注意的是,arguments.callee在ES5的strict 模式下已經被禁止使用了,這時候可以使用命名的函數運算式來實現同樣的效果: 複製代碼 代碼如下://斐波那契數列
var fibonacci = (function f(num){
return num <= 2 ? 1 : (f(num - 1) + f(num - 2));
});
var gn = fibonacci;
fibonacci = null;
console.info(gn(9));//34,使用命名函數運算式實現了函數對象和函數名的解耦,可以正常執行