JavaScript高級程序設計(第3版)學習筆記7 js函數(shù)(上)
變量類型
在說函數(shù)之前,先來說說變量類型。
1、變量:變量在本質上就是命名的內存空間。
2、變量的數(shù)據(jù)類型:就是指變量可以存儲的值的數(shù)據(jù)類型,比如Number類型、Boolean類型、Object類型等,在ECMAScript中,變量的數(shù)據(jù)類型是動態(tài)的,可以在運行時改變變量的數(shù)據(jù)類型。
3、變量類型:是指變量本身的類型,在ECMAScript中,變量類型就只有兩種:值類型和引用類型。當變量的數(shù)據(jù)類型是簡單數(shù)據(jù)類型時,變量類型就是值類型,當變量的數(shù)據(jù)類型是對象類型時,變量類型就是引用類型。在不引起歧義的情況下,也可以稱變量的數(shù)據(jù)類型為變量類型。
那么,值類型和引用類型有什么區(qū)別呢?最主要的一個,就是當變量類型為值類型時,變量存儲的就是變量值本身,而當變量類型為引用類型時,變量存儲的并不是變量值,而只是一個指向變量值的指針,訪問引用類型的變量值時,首先是取到這個指針,然后是根據(jù)這個指針去獲取變量值。如果將一個引用類型的變量值賦給另一個變量,最終結果是這兩個變量同時指向了一個變量值,修改其中一個會同時修改到另一個:
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
好了,關于變量類型先說到這,如果再繼續(xù)到內存存儲數(shù)據(jù)結構的話,就怕沉得下去浮不上來。
函數(shù)
如果說對象是房間,那么函數(shù)就是有魔幻效應的房間了。函數(shù)首先是對象,然后這個函數(shù)對象還具有很多魔幻功能……
1、函數(shù)
(1)函數(shù)是對象
函數(shù)也是一種對象,而用于創(chuàng)建函數(shù)對象實例的函數(shù)就是內置的Function()函數(shù)(創(chuàng)建對象實例需要函數(shù),而函數(shù)又是一種對象實例,是不是讓你有了先有雞還是先有蛋的困惑?別鉆牛角尖了,只要雞能生蛋,蛋能孵雞就行了,誰先誰后還是留給哲學家吧),但是函數(shù)這種對象,又和一般的對象有著極大的不同,以至于對函數(shù)對象實例使用typeof時返回的不是object而是function了。
(2)函數(shù)名是指向函數(shù)對象的引用類型變量
function fn(p){
console.info(p);
}
console.info(fn);//fn(p),可以將fn作為一般變量來訪問
var b = fn;
b('function');//function,可以對b使用函數(shù)調用,說明b指向的對象(也就是原來fn指向的對象)是一個函數(shù)
注:關于函數(shù)名,在ES5的嚴格模式下,已經不允許使用eval和arguments了,當然,參數(shù)名也不能用這兩個了(我想除非你是專業(yè)黑客,否則也不會使用這些作為標識符來使用吧)。
2、函數(shù)創(chuàng)建
(1)作為一種對象,函數(shù)也有和普通對象類似的創(chuàng)建方式,使用new調用構造函數(shù)Function(),它可以接受任意數(shù)量的參數(shù),最后一個參數(shù)作為函數(shù)體,而前面的所有參數(shù)都作為函數(shù)的形式參數(shù),前面的形式參數(shù)還可以使用逗號隔開作為一個參數(shù)傳入,一般形式為:
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
這種方式創(chuàng)建函數(shù),會解析兩次代碼,一次正常解析,一次解析函數(shù)體,效率會影響,但是比較適合函數(shù)體需要動態(tài)編譯的情況。
(2)由于函數(shù)對象本身的特殊性,我們還可以使用關鍵字function來創(chuàng)建函數(shù):
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關鍵字創(chuàng)建函數(shù)也有兩種方式:函數(shù)聲明和函數(shù)表達式。這兩種方式都能實現(xiàn)我們想要的效果,那他們之間有什么區(qū)別呢?這就是我們下面要講的。
3、函數(shù)聲明和函數(shù)表達式
(1)從形式上區(qū)分,在ECMA-262的規(guī)范中,可以看到:
函數(shù)聲明: function Identifier (參數(shù)列表(可選)){函數(shù)體}
函數(shù)表達式:function Identifier(可選)(參數(shù)列表(可選)){函數(shù)體}
除了函數(shù)表達式的標識符(函數(shù)名)是可選的之外沒有任何區(qū)別,但我們也可以從中得知:沒有函數(shù)名的一定是函數(shù)表達式。當然,有函數(shù)名的,我們就只能從上下文來判斷了。
(2)從上下文區(qū)分,這個說起來簡單,就是:只允許表達式出現(xiàn)的上下文中的一定是函數(shù)表達式,只允許聲明出現(xiàn)的上下文的一定是函數(shù)聲明。舉一些例子:
function fn(){};//函數(shù)聲明
//function fn(){}(); // 異常,函數(shù)聲明不能直接調用
var fn = function fn(){};//函數(shù)表達式
(function fn(){});//函數(shù)表達式,在分組操作符內
+function fn(){console.info(1);}();//1,函數(shù)表達式,出現(xiàn)在操作符+之后,因此可以直接調用,這里,也可以使用其它的操作符,比如new
new function fn(){console.info(2);}();//2,函數(shù)表達式,new操作符之后
(function(){
function fn(){};//函數(shù)聲明
});
(3)區(qū)別:我們?yōu)槭裁匆ㄟ@么大力氣來區(qū)分函數(shù)聲明和函數(shù)表達式呢?自然就是因為它們的不同點了,他們之間最大的不同,就是聲明會提升,關于聲明提升,在前面基礎語法的那一篇文章中,曾經對全局作用域中的聲明提升做過討論,我們把那里的結論復習一下:
A、引擎在解析時,首先會解析函數(shù)聲明,然后解析變量聲明(解析時不會覆蓋類型),最后再執(zhí)行代碼;
B、解析函數(shù)聲明時,會同時解析類型(函數(shù)),但不會執(zhí)行,解析變量聲明時,只解析變量,不會初始化。
在那里也舉了一些例子來演示(回憶一下),不過沒有同名稱的聲明例子,這里補充一下:
console.info(typeof fn);//function,聲明提升,以函數(shù)為準
var fn = '';
function fn(){
}
console.info(typeof fn);//string,由于已經執(zhí)行了代碼,這里fn的類型變?yōu)閟tring
try{
fn();//已經是string類型,不能調用了,拋出類型異常
}catch(e){
console.info(e);//TypeError
}
fn = function(){console.info('fn');};//如果想調用fn,只能再使用函數(shù)表達式賦值給fn
fn();//fn,可以調用
console.info(typeof gn);//function
function gn(){
}
var gn = '';
console.info(typeof gn);//string
可以看出:不管變量聲明是在前還是在后,在聲明提升時都是以函數(shù)聲明優(yōu)先,但是在聲明提升之后,由于要執(zhí)行變量初始化,而函數(shù)聲明不再有初始化(函數(shù)類型在提升時已經解析),因此后面輸出時就成為String類型了。
上面第3行定義了一個函數(shù),然后第7行馬上調用,結果竟然不行!你該明白保持全局命名空間清潔的重要性了吧,要不然,你可能會遇到“我在代碼中明明定義了一個函數(shù)卻不能調用”這種鬼事情,反過來,如果你想確保你定義的函數(shù)可用,最好就是使用函數(shù)表達式來定義。
還有一個問題,這里我們怎么確定變量類型是在初始化時候而不是在變量聲明提升時候改變的呢?看下面的代碼:
console.info(typeof fn);//function
function fn(){
}
var fn;
console.info(typeof fn);//function
可以看到,聲明提升后類型為function,并且由于沒有初始化代碼,最后的類型沒有改變。
關于函數(shù)聲明和函數(shù)表達式,還有一點需要注意的,看下面的代碼:
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規(guī)范中,命名函數(shù)表達式的標識符屬于內部作用域,而函數(shù)聲明的標識符屬于定義作用域。
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
上面是一個命名函數(shù)表達式在FireFox中的運行結果,在函數(shù)作用域內可以訪問這個名稱,但是在全局作用域中訪問出現(xiàn)引用異常。不過命名函數(shù)表達式在IE9之前的IE瀏覽器中會被同時作為函數(shù)聲明和函數(shù)表達式來解析,并且會創(chuàng)建兩個對象,好在IE9已經修正。
除了全局作用域,還有一種函數(shù)作用域,在函數(shù)作用域中,參與到聲明提升競爭的還有函數(shù)的參數(shù)。首先要明確的是,函數(shù)作用域在函數(shù)定義時不存在的,只有在函數(shù)實際調用才有函數(shù)作用域。
// 參數(shù)與內部變量,參數(shù)優(yōu)先
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');
// 參數(shù)與內部函數(shù),內部函數(shù)優(yōu)先
function gn(inner){
console.info(inner);// inner()函數(shù)
console.info(inner());// undefined
function inner(){
return other;
}
var other = 'other';
console.info(inner);// inner()函數(shù)
console.info(inner());// other
}
gn('param');
通過上面的輸出結果,我們得出優(yōu)先級:內部函數(shù)聲明 > 函數(shù)參數(shù) > 內部變量聲明。
這里面的一個過程是:首先內部函數(shù)聲明提升,并將函數(shù)名的類型設置為函數(shù)類型,然后解析函數(shù)參數(shù),將傳入的實際參數(shù)值賦給形式參數(shù),最后再內部變量聲明提升,只提升聲明,不初始化,如果有重名,同優(yōu)先級的后面覆蓋前面的,不同優(yōu)先級的不覆蓋(已經解析了優(yōu)先級高的,就不再解析優(yōu)先級低的)。
說明一下,這只是我根據(jù)輸出結果的推斷,至于后臺實現(xiàn),也有可能步驟完全相反,并且每一步都覆蓋前一步的結果,甚至是從中間開始,然后做一個優(yōu)先級標志確定是否需要覆蓋,當然,從效率上來看,應該是我推斷的過程會更好。另外,全局作用域其實就是函數(shù)作用域的一個簡化版,沒有函數(shù)參數(shù)。
這里就不再舉綜合的例子了,建議將這篇文章和前面的基礎語法那一篇一起閱讀,可能效果會更好。關于優(yōu)先級與覆蓋,也引出下面要說的一個問題。
4、函數(shù)重載
函數(shù)是對象,函數(shù)名是指向函數(shù)對象的引用類型變量,這使得我們不可能像一般面向對象語言中那樣實現(xiàn)重載:
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,因為函數(shù)名只是一個變量而已,兩次函數(shù)聲明會依次解析,這個變量最終指向的函數(shù)就是第二個函數(shù),而第8行只傳入1個參數(shù),在函數(shù)內部b就自動賦值為undefined,然后與1相加,結果就是NaN。換成函數(shù)表達式,也許就好理解多了,只是賦值了兩次而已,自然后面的賦值會覆蓋前面的:
var fn = function (a){ return a; }
fn = function (a,b){ return a + b;}
那么,在ECMAScript中,怎么實現(xiàn)重載呢?回想一下簡單數(shù)據(jù)類型包裝對象(Boolean、Number、String),既可以作為構造函數(shù)創(chuàng)建對象,也可以作為轉換函數(shù)轉換數(shù)據(jù)類型,這是一個典型的重載。這個重載其實在前一篇文章中我們曾經討論過:
(1)根據(jù)函數(shù)的作用來重載,這種方式的一般格式為:
function fn(){
if(this instanceof fn)
{
// 功能1
}else
{
// 功能2
}
}
這種方式雖然可行,但是很明顯作用也是有限的,比如就只能重載兩次,并且只能重載包含構造函數(shù)的這種情形。當然,你可以結合apply()或者call()甚至ES5中新增的bind()來動態(tài)綁定函數(shù)內部的this值來擴展重載,但這已經有了根據(jù)函數(shù)內部屬性重載的意思了。
(2)根據(jù)函數(shù)內部屬性來重載
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
這里就是利用函數(shù)內部屬性arguments來實現(xiàn)重載的。當然,在內部重載的方式可以多種多樣,你還可以結合typeof、instanceof等操作符來實現(xiàn)你想要的功能。至于內部屬性arguments具體是什么?這就是下面要講的。
5、函數(shù)內部屬性arguments
簡單一點說,函數(shù)內部屬性,就是只能在函數(shù)體內訪問的屬性,由于函數(shù)體只有在函數(shù)被調用的時候才會去執(zhí)行,因此函數(shù)內部屬性也只有在函數(shù)調用時才會去解析,每次調用都會有相應的解析,因此具有動態(tài)特性。這種屬性有:this和arguments,這里先看arguments,在下一篇文章中再說this。
(1)在函數(shù)定義中的參數(shù)列表稱為形式參數(shù),而在函數(shù)調用時候實際傳入的參數(shù)稱為實際參數(shù)。一般的類C語言,要求在函數(shù)調用時實際參數(shù)要和形式參數(shù)一致,但是在ECMAScript中,這兩者之間沒有任何限制,你可以在定義的時候有2個形式參數(shù),在調用的時候傳入2個實際參數(shù),但你也可以傳入3個實際參數(shù),還可以只傳入1個實際參數(shù),甚至你什么參數(shù)都不傳也可以。這種特性,正是利用函數(shù)內部屬性來實現(xiàn)重載的基礎。
(2)形式參數(shù)甚至可以取相同的名稱,只是在實際傳入時會取后面的值作為形式參數(shù)的值(這種情況下可以使用arguments來訪問前面的實際參數(shù)):
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
這其實也可以用本文前面關于聲明提升的結論來解釋:同優(yōu)先級的后面的覆蓋前面的,并且函數(shù)參數(shù)解析時同時解析值。當然,這樣一來,安全性就很成問題了,因此在ES5的嚴格模式下,重名的形式參數(shù)被禁止了。
(3)實際參數(shù)的值由形式參數(shù)來接受,但如果實際參數(shù)和形式參數(shù)不一致怎么辦呢?答案就是使用arguments來存儲,事實上,即便實際參數(shù)和形式參數(shù)一致,也存在arguments對象,并且保持著和已經接受了實際參數(shù)的形式參數(shù)之間的同步。將這句話細化一下來理解:
•arguments是一個類數(shù)組對象,可以像訪問數(shù)組元素那樣通過方括號和索引來訪問arguments元素,如arguments[0]、arugments[1]。
•arguments是一個類數(shù)組對象,除了繼承自Object的屬性和方法(有些方法被重寫了)外,還有自己本身的一些屬性,如length、callee、caller,這里length表示實際參數(shù)的個數(shù)(形式參數(shù)的個數(shù)?那就是函數(shù)屬性length了),callee表示當前函數(shù)對象,而caller只是為了和函數(shù)屬性caller區(qū)分而定義的,其值為undefined。
•arguments是一個類數(shù)組對象,但并不是真正的數(shù)組對象,不能直接對arguments調用數(shù)組對象的方法,如果要調用,可以先使用Array.prototype.slice.call(arguments)先轉換為數(shù)組對象。
•arguments保存著函數(shù)被調用時傳入的實際參數(shù),第0個元素保存第一個實際參數(shù),第1個元素保存第二個實際參數(shù),依次類推。
•arguments保存實際參數(shù)值,而形式參數(shù)也保存實際參數(shù)值,這兩者之間有一個同步關系,修改一個,另一個也會隨之修改。
•arguments和形式參數(shù)之間的同步,只有當形式參數(shù)實際接收了實際參數(shù)時才存在,對于沒有接收實際參數(shù)的形式參數(shù),不存在這種同步關系。
•arguments對象雖然很強大,但是從性能上來說也存有一定的損耗,所以如果不是必要,就不要使用,建議還是優(yōu)先使用形式參數(shù)。
fn(0,-1);
function fn(para1,para2,para3,para4){
console.info(fn.length);//4,形式參數(shù)個數(shù)
console.info(arguments.length);//2,實際參數(shù)個數(shù)
console.info(arguments.callee === fn);//true,callee對象指向fn本身
console.info(arguments.caller);//undefined
console.info(arguments.constructor);//Object(),而不是Array()
try{
arguments.sort();//類數(shù)組畢竟不是數(shù)組,不能直接調用數(shù)組方法,拋出異常
}catch(e){
console.info(e);//TypeError
}
var arr = Array.prototype.slice.call(arguments);//先轉換為數(shù)組
console.info(arr.sort());//[-1,0],已經排好序了
console.info(para1);//0
arguments[0] = 1;
console.info(para1);//1,修改arguments[0],會同步修改形式參數(shù)para1
console.info(arguments[1]);//-1
para2 = 2;
console.info(arguments[1]);//2,修改形式參數(shù)para2,會同步修改arguments[1]
console.info(para3);//undefined,未傳入實際參數(shù)的形式參數(shù)為undefined
arguments[2] = 3;
console.info(arguments[2]);//3
console.info(para3);//undefined,未接受實際參數(shù)的形式參數(shù)沒有同步關系
console.info(arguments[3]);//undefined,未傳入實際參數(shù),值為undefined
para4 = 4;
console.info(para4);//4
console.info(arguments[3]);//undefined,為傳入實際參數(shù),不會同步
}
經過測試,arguments和形式參數(shù)之間的同步是雙向的,但是《JavaScript高級程序設計(第3版)》中第66頁說是單向的:修改形式參數(shù)不會改變arguments。這可能是原書另一個Bug,也可能是FireFox對規(guī)范做了擴展。不過,這也讓我們知道,即便經典如此,也還是存有Bug的可能,一切當以實際運行為準。
•結合arguments及其屬性callee,可以實現(xiàn)在函數(shù)內部調用自身時與函數(shù)名解耦,這樣即便函數(shù)賦給了另一個變量,而函數(shù)名(別忘了,也是一個變量)另外被賦值,也能夠保證運行正確。典型的例子有求階乘函數(shù)、斐波那契數(shù)列等。
//求階乘
function factorial(num){
if(num <= 1)
{
return 1;
}else{
return num * factorial(num - 1);
}
}
var fn = factorial;
factorial = null;
try{
fn(2);//由于函數(shù)內部遞歸調用了factorial,而factorial已經賦值為null了,所以拋出異常
}catch(e){
console.info(e);//TypeError
}
//斐波那契數(shù)列
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,實現(xiàn)了函數(shù)對象和函數(shù)名的解耦,可以正常執(zhí)行
遞歸的算法非常簡潔,但因為要維護運行棧,效率不是很好。關于遞歸的優(yōu)化,也有很多非常酣暢漓淋的算法,這里就不深入了。
需要注意的是,arguments.callee在ES5的嚴格模式下已經被禁止使用了,這時候可以使用命名的函數(shù)表達式來實現(xiàn)同樣的效果:
//斐波那契數(shù)列
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,使用命名函數(shù)表達式實現(xiàn)了函數(shù)對象和函數(shù)名的解耦,可以正常執(zhí)行
相關文章
JavaScript高級程序設計(第3版)學習筆記12 js正則表達式
前面在分析PhoneGap源碼的時候,曾經總結過一次正則表達式的用法,為了不同系列文章的完整性,這里將那里的總結遷移過來2012-10-10
JavaScript SetInterval與setTimeout使用方法詳解
本文講解了JavaScript SetInterval與setTimeout的區(qū)別,并用代碼示例演示了使用方法2013-11-11
微信小程序自定義數(shù)據(jù)實現(xiàn)級聯(lián)省市區(qū)組件功能
這篇文章主要介紹了微信小程序自定義數(shù)據(jù)實現(xiàn)級聯(lián)省市區(qū)組件功能,本文通過實例代碼給大家介紹的非常詳細,感興趣的朋友跟隨小編一起看看吧2024-03-03

