這篇文章,我將就以下幾個方面來認識JavaScript中的函數。
JavaScript中最有意思的恐怕是函數了,因為和其他語言不同,在JavaScript中,每個函數都是Function類型的實例,而我們知道:Object是一個基礎類型,其他所有類型都是從Object繼承了基本的行為。也就是說Function也是從Object引用類型繼承而來的,那麼作為Function類型的實例,函數也就是對象這一點就不難理解了。
那麼如何定義一個函數呢?一般來說,有三種方式可以來定義函數。
第一:函數聲明。這種語法和其他語言的形式一樣,是我們最為常見的一種方式。如下:
function add(num){ return num+10; }
第二:函數表達式。如下:
var add=function(num){ return num+10; };
我們可以注意到函數表達式的定義函數方法把函數看作了一個表達式,因此最後要以分號;結尾,並且在function之後沒有函數名,那麼怎麼調用呢?實際上,通過add即可引用了。(實際上,這裡的add是全局對象global在浏覽器中表現為window對象的一個屬性或方法)
第三:使用Function構造函數。如下:
var add=new Function("num","return num+10");
Function構造函數可以接收任意多的參數,其中最後一個是函數體,前面所有的是函數的參數。這種方法是我們所不推薦的,因為它會導致解析兩次代碼(第一次是解析常規的ECMAScript代碼,第二次是解析傳入構造函數中的字符串),從而影響了性能。但是這種方法有利於我們理解:函數是對象,函數名是指針。
由於我們不推薦第三種方法來創建函數,並且在實際中也用的很少。那麼前兩種方法又有什麼區別呢?
實際上,區別僅僅在於是否函數聲明的方法會使得代碼在開始執行之前,解析器就已經通過了函數聲明提升讀取並將函數添加到了執行環境中去,於是在代碼求知識,JavaScript引擎會將它們放到源代碼樹的頂部,而函數表達式則不會。這裡可能不好理解,看下面例子:
a
function say(){ console.log("I like coding"); } say();
此時在控制台輸出了I like coding.
b
var say=function (){ console.log("I like coding"); } say();
同樣,這時在控制台也輸出了I like coding.
c
say(); function say(){ console.log("I like coding"); }
這裡我們將say()這個調用函數的語句放在最上面了,同樣控制台也輸出了I like coding.
d
say(); var say=function (){ console.log("I like coding"); };
然而,這裡卻出現了錯誤。讓我們捕獲以下錯誤。
try{ say(); var say=function (){ console.log("I like coding"); }; }catch(err){ console.log("錯誤是:"+err); }
控制台提示:錯誤是:TypeError: say is not a function。以上代碼之所以會在運行期間產生錯誤,是因為函數沒有位於一個函數聲明中,而是位於一個初始化的語句中,這樣如果沒有執行到該語句,那麼變量sum就不會保存有對函數的引用。而使用函數聲明,JavaScript引擎將函數聲明提升到了最頂端,那麼函數名sum就保存了對函數的引用。
為了更深刻的理解函數是對象,函數名是指針,我們可以以下面的例子講解:
function add(num){ return num+10; } console.log(add(10));//20 var addCopy=add; //這時我們把add這個指針賦值給addCopy,於是addCopy也指向了同一個函數對象 console.log(addCopy(10));//20 sum=null; //null 的一大作用就是用於保存空的對象,這時sum指向了一個空的對象 console.log(sum(10));//Uncaught TypeError: sum is not a function(…) sum是一個空對象,因此會出現語法錯誤 console.log(addCopy(10));//20 而addCopy指向了那個函數對象,便可以得到正確的答案
也正是因為函數是對象,其函數名(指針)可以有多個(指向的是同一個對象),因此也不難理解函數沒有重載。
一般,函數中的參數都是變量。而因為函數名是指針,也是變量,因此我們就可以把函數作為值來使用了。
如下:
function onefunc(antherfunc,argum){ return antherfunc(argum); } function antherfunc(str){ return str+"I am js"; } console.log(onefunc(antherfunc,"ha "));//ha I am js
除此之外,一個函數也可以作為另一個函數的結果返回。如下:
function createComparisonFunction(propName){ return function(object1,object2){ //這是一個比較函數,雖然有形參,但是不需要傳遞實參,只是為了便於下面調用 var value1=object1[propName]; var value2=object2[propName]; //這裡使用方括號操作符的好處在於:當定義一個對象數組時,裡面的對象是用對象字面量方法來創建的,於是屬性名可以加引號,這時屬性名即使不規范也不會影響結果,應當注意的是,這時,最後調用的時候還是需要使用方括號調用。。 if(value1<value2){ return -1; }else if(value1>value2){ return 1; }else{ return 0; } }; //注意:雖然這裡不用分號也可以運行,但是最好寫上,這樣才規范。 } var data=[{name:"zhuzhen",age:21},{name:"heting",age:18}]; //這裡表示按照何種屬性排序 data.sort(createComparisonFunction("name")); console.log(data[0].name);//heting data.sort(createComparisonFunction("age")); console.log(data[0].age);//18
我們可以先來總結一下函數一共有哪些屬性和方法且函數有哪些對象。
函數的內部對象:this ,arguments(它還有一個callee屬性和length屬性)
函數的方法:繼承自Object的有toString0(),valueOf(),toLocaleString()。函數自身添加的方法有call() apply() bind()
函數的屬性:length prototype caller
在javascript的函數中,函數是不介意傳過來多少參數的。即最終傳進來的參數個數不一定就是在聲明時希望的接受的參數個數。這就是因為函數中存在一個類數組對象arguments,函數接收到的就是這個“數組”,我們可以通過這個“數組”獲取傳遞給函數的每一個參數。說它是類數組對象,而不是數組對象,是因為它並不是引用類型Array的實例,只是我們可以使用方括號法來訪問其中的每一個元素罷了。第三部分的開頭我介紹到arguments有兩個屬性,一個是length一個是callee。
其中length屬性返回的是函數接收到的實際參數個數。比如:
function number(){ console.log(arguments.length+" "+arguments[0]); } number(12,45);//2 12 number("hah","hei","you");//3 hah number();//0 undefined
從上述代碼可以看出,雖然我們在聲明函數時,並不希望接收參數,但是到了實際調用,我們可以傳遞任意多的參數。
且我們在長度後面也輸出了傳入的第一個參數,由於Number()並沒有傳入參數,所以它的第一項是undefined。
而callee屬性也是函數內部對象arguments的一個屬性,這個屬性是一個指針,指向擁有這個arguments對象的函數。通過這個屬性我們可以改寫求階乘的遞歸函數。首先,我們,我們看看最普通的遞歸函數。
function factorial(num){ if(num<=1){ return 1; }else{ return num*factorial(num-1); } } console.log(factorial(3));//6
這中遞歸函數很好理解。即我們使用factorial(3)進行調用時,這時立馬進入了factorial的函數執行環境,於是立即創建了num這個局部變量(形式參數雖然沒有用var,但它的確是函數內部的局部變量)並賦值為3,判斷後返回3*factorial(2),因為我們認為return多少,這個函數最終就得到多少。接下來,由於出現了factorial(2),則再次進入了factorial執行環境,最終返回了3*2*factorial(1).由於出現了factorial(1),相當於又調用了一次函數,這時,再次進入函數執行環境最終得到3*2*1=6。這時跳出了局部執行環境進入全局執行環境並銷毀了num這個局部變量。
但是,這裡有一個問題,由於函數是對象,函數名是指針,如果我們把factorial指針賦值給另外一個指針比如anotherFactorial,並讓這個factorial指針指向一個空對象,那麼此時調用anotherFactorial會怎麼樣呢?見下面代碼:
function factorial(num){ if(num<=1){ return 1; }else{ return num*factorial(num-1); } } var anotherFactorial=factorial; factorial=null; console.log(anotherFactorial(3));//Uncaught TypeError: factorial is not a function(…)
這是沒有用的,因為內部的factorial依然存在,它緊緊地和函數耦合在了一起。但是,只要使用arguments的callee這個屬性就可以很好的解決問題了。
function factorial(num){ if(num<=1){ return 1; }else{ return num*arguments.callee(num-1); } } var anotherFactorial=factorial; factorial=null; console.log(anotherFactorial(3));//6
function onefunc(){ if(arguments.length==1){ console.log(arguments[0]+10); }else if(arguments.length==2){ console.log(arguments[0]+arguments[1]); }else{ console.log("please input one or two numbers"); } } onefunc(45);//55 onefunc(12,37);//49 onefunc();//please input one or two numbers
this對象我們一般又稱為上下文對象,這個對象是在函數運行是自動生成的一個內部對象,且由於它是屬於函數的,故它只能在函數中使用(在函數中出現為this. 如果在函數外最多出現this,後面沒有.)。我們可以分為四種情況來做講解。理解:一般,this在在哪個環境下被調用,this就指向哪裡。
第一種:純粹的函數調用。
這時函數的最通常的用法,屬於全局屬性調用,因此this就代表window。可以看以下例子:
var color="red"; var color="pink"; var o={color:"blue"}; function sayColor(){ console.log(this.color); } sayColor();// pink
這裡最終得到了pink,是因為這時是純粹的函數調用,且在全局環境中調用,最終指向的是window。即我們將得到window.color,而這時候得到了pink,這時因為pink在後,覆蓋了red,color最終為pink 。同理,如果pink在前,red在後,最終將得到red而非pink。
提出疑問:this可能指向的是sayColor函數嗎?這裡是因為sayColor中沒有color,所以沿著作用域鏈向上一直搜索到window對象呢?
請看下面這個例子:
var color="red"; var o={color:"blue"}; function sayColor(){ var color="black"; console.log(this.color); } sayColor();//red
這時,又得到了red,而沒有得到函數中的black,說明this確實指向的是window對象,而不是sayColor函數,即並不是沿著作用域鏈向上搜尋的結果。
那如果我們把sayColor()函數中的color定義為全局變量呢?如下:
color="red"; var o={color:"blue"}; color="pink"; function sayColor(){ color="black"; alert(this.color); } sayColor();//black
這裡為什麼沒有得到pink而是得到sayColor中的black了呢? 這說明,sayColor函數一經調用,這時即進入了sayColor()函數的執行環境,於是函數內部的變量開始創建,由於函數內部的color沒有用var聲明,因此函數知道,這時創建了一個全局變量,於是函數想:你心不在我這我干嘛要你,並隨即將該全局變量扔到了全局環境中,但是總得有個先來後到吧,於是color="black"被扔到了color="pink"的後面。或者說就是按照先來後到的過去,無論如何,它最後會覆蓋前面定義的color。
第二種:作為對象方法的調用。
函數也可以作為某個對象的方法調用(這句話需細細理解),這時this就指向了這個上級對象。如下面的例子:
color="red"; var o={color:"blue"}; function sayColor(){ var color="black"; alert(this.color); } sayColor();//red o.sayColor=sayColor; o.sayColor();//blue
其中的o.sayColor=sayColor;這句意思是將函數作為對象的方法。所以this指向的是這個上級對象o,最終得到blue就不難理解了。值得注意的是:函數的名字僅僅是包含指針的變量而已,因此,即使在不同的環境中執行(引用),全局的sayColor()函數和o.sayColor()函數仍指向的是同一個函數。
第三種:作為構造函數
所謂構造函數,即通過這個函數生成一個新的對象,此時,this就指向了這個新的對象。如下面的例子所示:
function Num(x){ this.x=x; }; var o=new Num(2); console.log(o.x);//2
我們通過Num這個構造函數並初始化了對象o,於是this就指向了這個對象o,也就是說this.x實際上就成了o.x,我們在初始化時傳入了2,實際上就是o.x=2。
第四種情況:函數的apply()方法、函數的call()方法以及函數的bind()方法。
這些方法我將在下面進行講解。
函數作為Function引用類型的實例,那麼它一定繼承了一些Object的方法,但又為了保證它的獨特性,它一定還擁有非繼承的方法。
函數從Object引用類型繼承得到的方法有valueOf()方法、toString()方法以及toLocaleString()方法。它們最終都會返回函數的代碼:
function add(){ return 20; } console.log(add.toString()); console.log(add.toLocaleString()); console.log(add.valueOf());
不出意外地,它們最終都返回了函數本身,即:
function add(){ return 20; }
非繼承的方法包括apply(),call()以及bind(),這些方法也正是剛剛我在講this對象的第四種情況時省略的方法,這裡我將講解。
apply()方法和call()方法兩者的作用都是在特定的作用域中調用函數,實際上等於設置函數體內this對象的值。同時,兩者也都只接收兩個參數,第一個參數都是運行函數的作用域,而第二個參數略有不同,apply()方法的第二個參數要接收一個數組(Array的實例)或者是一個arguments對象,而call()方法的第二個參數為列舉出所有要傳遞給函數的參數。舉例如下:
function sum(num1,num2){ return num1+num2; } function callSum(num1,num2){ return sum.call(this,num1,num2);//call()方法必須全部列舉出來 } function callSum1(num1,num2){ return sum.apply(this,arguments);//apply()方法可以傳遞一個arguments對象 }
function callSum3(){
return sum.apply(this,arguments);//注意:這裡即使沒有形參也可以
} function callSum2(num1,num2){ return sum.apply(this,[num1,num2]);//apply()方法還可以傳遞數組(Array的實例) } console.log(callSum(10,10));//20 console.log(callSum1(10,10));//20 console.log(callSum2(10,10));//20
console.log(callSum3(10,10));//20 我們沒有給callSum3傳遞形參也可以
上面的例子並沒有改變this的作用域,知識介紹了第二個參數的傳遞方法。而apply()和call()方法的真正作用在於擴充函數來以運行的作用域。
window.color="red"; var o={color:"blue"}; function sayColor(){ console.log(this.color); } sayColor();//red sayColor.call(this);//red sayColor.call(window);//red sayColor.call(o);//blue
第一個是隱式地在全局作用域中調用函數,而第二第三個是顯式地在全局作用域中調用函數,最用一個是在對象o中調用函數,故this指向o,最後的到的是o.color。總結就是,call和apply的第一個參數是什麼,就是在哪裡調用函數,亦即this指向哪裡。
最後一個方法即為bind(),它只能接收一個參數,表示調用函數的作用域,也就是說它和前兩種方法的效果一致。只是,它必須先賦值給一個變量,再調用這個變量。
window.color="red"; var o={color:"blue"}; function sayColor(){ console.log(this.color); } sayColor();//red var s=sayColor.bind(o); s();//blue
我們說過,函數一般具有三種屬性:length,caller,prototype
第一:length屬性
這個屬性很簡單,由函數調用,返回函數希望接受的參數的個數。如下所示:
function a(num1,num2){ return num1+num2; } function b(num1,num2,num3){ return num1+num2+num3; } console.log(a.length);//2 console.log(b.length);//3
注意區分:函數的length屬性是函數希望接受的參數的個數,而函數的內部對象arguments的length屬性是真正傳入函數的參數的個數,兩者是不一樣的。
第二:caller屬性
這個屬性會返回調用當前函數的函數。如下所示:
function a(){ b(); } function b(){ console.log(b.caller); } a();
在調用a()之後,我們可以在控制台中看到:
function a(){ b(); }
注意區分:caller返回的是調用當前函數的函數,而callee返回的是arguments對象所從屬的函數。
第三:prototype屬性
這個屬性非常重要,三言兩語無法表達完全,我會在後面的博文中單獨介紹。
點擊這裡回到博文開頭
如果把你的人生拍成一部電影,你能吸引多少觀眾?