變數型別 在說函數之前,先來說說變數型別。
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的嚴格模式下,已經不允許使用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, 身體);
例如:
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) ;//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 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){ .info(a);
console.info(arguments[0]);
console.info(arguments[1]);
}
gn(1,2);//2, 1,2
gn(1);//undefined,1,undefined
這其實也可以用本文前面關於聲明提升的結論來解釋:同優先級的後面的覆蓋前面的,且函數參數解析時同時解析值。當然,這樣一來,安全性就很成問題了,因此在ES5的嚴格模式下,重名的形式參數被禁止了。
(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 faial(ial(oneat) {
if(num {
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的嚴格模式下已經被禁止使用了,這時候可以使用命名的函數表達式來實現同樣的效果:
//斐波那契數列
var fibonacci = (function f(num){
return num });
var gn = fibonacci;
fibonacci = null; console .info(gn(9));//34,使用命名函數表達式實作了函數物件和函數名稱的解耦,可以正常執行