JavaScript入門之基本函數(shù)詳解
更新時間:2011年10月21日 20:47:32 作者:
函數(shù),在C語言之類的過程式語言中,是頂級的實體,而在Java/C++之類的面向?qū)ο蟮恼Z言中,則被對象包裝起來,一般稱為對象的方法。而在JavaScript中,函數(shù)本身與其他任何的內(nèi)置對象在低位上是沒有任何區(qū)別的,也就是說,函數(shù)本身也是對象
總的來說,函數(shù)在JavaScript中可以:
◆ 被賦值給一個變量
◆ 被賦值為對象的屬性
◆ 作為參數(shù)被傳入別的函數(shù)
◆ 作為函數(shù)的結(jié)果被返回
◆ 用字面量來創(chuàng)建
函數(shù)對象
1.1 創(chuàng)建函數(shù)
創(chuàng)建JavaScript函數(shù)的一種不長用的方式(幾乎沒有人用)是通過new操作符來作用于Function“構(gòu)造器”:
參數(shù)列表中可以有任意多的參數(shù),然后緊跟著是函數(shù)體,比如:
將會打印結(jié)果:
6
但是,誰會用如此難用的方式來創(chuàng)建一個函數(shù)呢?如果函數(shù)體比較復雜,那拼接這個String要花費很大的力氣,所以JavaScript提供了一種語法糖,即通過字面量來創(chuàng)建函數(shù):
或:
事實上,這樣的語法糖更容易使傳統(tǒng)領域的程序員產(chǎn)生誤解,function關(guān)鍵字會調(diào)用Function來new一個對象,并將參數(shù)表和函數(shù)體準確的傳遞給Function的構(gòu)造器。
通常來說,在全局作用域(作用域?qū)⒃谙乱还?jié)詳細介紹)內(nèi)聲明一個對象,只不過是對一個屬性賦值而已,比如上例中的add函數(shù),事實上只是為全局對象添加了一個屬性,屬性名為add,而屬性的值是一個對象,即function(x, y){return x+y;},理解這一點很重要,這條語句在語法上跟:
并無二致。都是給全局對象動態(tài)的增加一個新的屬性,如此而已。
為了說明函數(shù)跟其他的對象一樣,都是作為一個獨立的對象而存在于JavaScript的運行系統(tǒng),我們不妨看這樣一個例子:
沒有錯,p雖然引用了一個匿名函數(shù)(對象),但是同時又可以擁有屬性,完全跟其他對象一樣,運行結(jié)果如下:
function (){
print("invoke p by ()");
}
func:function
invoke p by ()
1.2 函數(shù)的參數(shù)
在JavaScript中,函數(shù)的參數(shù)是比較有意思的,比如,你可以將任意多的參數(shù)傳遞給一個函數(shù),即使這個函數(shù)聲明時并未制定形式參數(shù),比如:
函數(shù)adPrint在聲明時接受三個形式參數(shù):要打印的串,要打印的長度,是否轉(zhuǎn)換為大小寫的標記。但是在調(diào)用的時候,我們可以按順序傳遞給adPrint一個參數(shù),兩個參數(shù),或者三個參數(shù)(甚至可以傳遞給它多于3個,沒有關(guān)系),運行結(jié)果如下:
Hello, world
Hello
hello
HELLO
事實上,JavaScript在處理函數(shù)的參數(shù)時,與其他編譯型的語言不一樣,解釋器傳遞給函數(shù)的是一個類似于數(shù)組的內(nèi)部值,叫arguments,這個在函數(shù)對象生成的時候就被初始化了。比如我們傳遞給adPrint一個參數(shù)的情況下,其他兩個參數(shù)分別為undefined.這樣,我們可以才adPrint函數(shù)內(nèi)部處理那些undefined參數(shù),從而可以向外部公開:我們可以處理任意參數(shù)。
我們通過另一個例子來討論這個神奇的arguments:
函數(shù)sum沒有顯式的形參,而我們又可以動態(tài)的傳遞給其任意多的參數(shù),那么,如何在sum函數(shù)中如何引用這些參數(shù)呢?這里就需要用到arguments這個偽數(shù)組了,運行結(jié)果如下:
150
108
Error: not a number exception
函數(shù)作用域
作用域的概念在幾乎所有的主流語言中都有體現(xiàn),在JavaScript中,則有其特殊性:JavaScript中的變量作用域為函數(shù)體內(nèi)有效,而無塊作用域,我們在Java語言中,可以這樣定義for循環(huán)塊中的下標變量:
public void method(){
for(int i = 0; i < obj1.length; i++){
//do something here;
}
//此時的i為未定義
for(int i = 0; i < obj2.length; i++){
//do something else;
}
}
而在JavaScript中:
JavaScript的函數(shù)是在局部作用域內(nèi)運行的,在局部作用域內(nèi)運行的函數(shù)體可以訪問其外層的(可能是全局作用域)的變量和函數(shù)。JavaScript的作用域為詞法作用域,所謂詞法作用域是說,其作用域為在定義時(詞法分析時)就確定下來的,而并非在執(zhí)行時確定,如下例:
運行結(jié)果是什么呢?初學者很可能得出這樣的答案:
global
local
而正確的結(jié)果應該是:
undefined
local
因為在函數(shù)scopeTest的定義中,預先訪問了未聲明的變量str,然后才對str變量進行初始化,所以第一個print(str)會返回undifined錯誤。那為什么函數(shù)這個時候不去訪問外部的str變量呢?這是因為,在詞法分析結(jié)束后,構(gòu)造作用域鏈的時候,會將函數(shù)內(nèi)定義的var變量放入該鏈,因此str在整個函數(shù)scopeTest內(nèi)都是可見的(從函數(shù)體的第一行到最后一行),由于str變量本身是未定義的,程序順序執(zhí)行,到第一行就會返回未定義,第二行為str賦值,所以第三行的print(str)將返回”local”。
函數(shù)上下文
在Java或者C/C++等語言中,方法(函數(shù))只能依附于對象而存在,不是獨立的。而在JavaScript中,函數(shù)也是一種對象,并非其他任何對象的一部分,理解這一點尤為重要,特別是對理解函數(shù)式的JavaScript非常有用,在函數(shù)式編程語言中,函數(shù)被認為是一等的。
函數(shù)的上下文是可以變化的,因此,函數(shù)內(nèi)的this也是可以變化的,函數(shù)可以作為一個對象的方法,也可以同時作為另一個對象的方法,總之,函數(shù)本身是獨立的。可以通過Function對象上的call或者apply函數(shù)來修改函數(shù)的上下文:
call和apply
call和apply通常用來修改函數(shù)的上下文,函數(shù)中的this指針將被替換為call或者apply的第一個參數(shù),我們不妨來看看JavaScript入門之對象與JSON中的例子:
//定義一個人,名字為jack
var jack = {
name : "jack",
age : 26
}
//定義另一個人,名字為abruzzi
var abruzzi = {
name : "abruzzi",
age : 26
}
//定義一個全局的函數(shù)對象
function printName(){
return this.name;
}
//設置printName的上下文為jack, 此時的this為jack
print(printName.call(jack));
//設置printName的上下文為abruzzi,此時的this為abruzzi
print(printName.call(abruzzi));
print(printName.apply(jack));
print(printName.apply(abruzzi));
只有一個參數(shù)的時候call和apply的使用方式是一樣的,如果有多個參數(shù):
setName.apply(jack, ["Jack Sept."]);
print(printName.apply(jack));
setName.call(abruzzi, "John Abruzzi");
print(printName.call(abruzzi));
得到的結(jié)果為:
Jack Sept.
John Abruzzi
apply的第二個參數(shù)為一個函數(shù)需要的參數(shù)組成的一個數(shù)組,而call則需要跟若干個參數(shù),參數(shù)之間以逗號(,)隔開即可。
使用函數(shù)
前面已經(jīng)提到,在JavaScript中,函數(shù)可以
◆ 被賦值給一個變量
◆ 被賦值為對象的屬性
◆ 作為參數(shù)被傳入別的函數(shù)
◆ 作為函數(shù)的結(jié)果被返回
我們就分別來看看這些場景:
賦值給一個變量:
//聲明一個函數(shù),接受兩個參數(shù),返回其和
function add(x, y){
return x + y;
}
var a = 0;
a = add;//將函數(shù)賦值給一個變量
var b = a(2, 3);//調(diào)用這個新的函數(shù)a
print(b);
這段代碼會打印”5”,因為賦值之后,變量a引用函數(shù)add,也就是說,a的值是一個函數(shù)對象(一個可執(zhí)行代碼塊),因此可以使用a(2, 3)這樣的語句來進行求和操作。
賦值為對象的屬性:
事實上,這個例子與上個例子的本質(zhì)上是一樣的,第一個例子中的a變量,事實上是全局對象(如果在客戶端環(huán)境中,表示為window對象)的一個屬性。而第二個例子則為obj對象,由于我們很少直接的引用全局對象,就分開來描述。
作為參數(shù)傳遞:
//高級打印函數(shù)的第二個版本
function adPrint2(str, handler){
print(handler(str));
}
//將字符串轉(zhuǎn)換為大寫形式,并返回
function up(str){
return str.toUpperCase();
}
//將字符串轉(zhuǎn)換為小寫形式,并返回
function low(str){
return str.toLowerCase();
}
adPrint2("Hello, world", up);
adPrint2("Hello, world", low);
運行此片段,可以得到這樣的結(jié)果:
HELLO, WORLD
hello, world
應該注意到,函數(shù)adPrint2的第二個參數(shù),事實上是一個函數(shù),將這個處理函數(shù)作為參數(shù)傳入,在adPrint2的內(nèi)部,仍然可以調(diào)用這個函數(shù),這個特點在很多地方都是有用的,特別是,當我們想要處理一些對象,但是又不確定以何種形式來處理,則完全可以將“處理方式”作為一個抽象的粒度來進行包裝(即函數(shù))。
作為函數(shù)的返回值:
先來看一個最簡單的例子:
函數(shù)currying返回一個匿名函數(shù),這個匿名函數(shù)會打印”curring”,簡單的調(diào)用currying()會得到下面的結(jié)果:
如果要調(diào)用currying返回的這個匿名函數(shù),需要這樣:
currying()();
第一個括號操作,表示調(diào)用currying本身,此時返回值為函數(shù),第二個括號操作符調(diào)用這個返回值,則會得到這樣的結(jié)果:
currying
◆ 被賦值給一個變量
◆ 被賦值為對象的屬性
◆ 作為參數(shù)被傳入別的函數(shù)
◆ 作為函數(shù)的結(jié)果被返回
◆ 用字面量來創(chuàng)建
函數(shù)對象
1.1 創(chuàng)建函數(shù)
創(chuàng)建JavaScript函數(shù)的一種不長用的方式(幾乎沒有人用)是通過new操作符來作用于Function“構(gòu)造器”:
復制代碼 代碼如下:
var funcName = new Function( [argname1, [... argnameN,]] body );
參數(shù)列表中可以有任意多的參數(shù),然后緊跟著是函數(shù)體,比如:
復制代碼 代碼如下:
var add = new Function("x", "y", "return(x+y)");
print(add(2, 4));
print(add(2, 4));
將會打印結(jié)果:
6
但是,誰會用如此難用的方式來創(chuàng)建一個函數(shù)呢?如果函數(shù)體比較復雜,那拼接這個String要花費很大的力氣,所以JavaScript提供了一種語法糖,即通過字面量來創(chuàng)建函數(shù):
復制代碼 代碼如下:
function add(x, y){
return x + y;
}
return x + y;
}
或:
復制代碼 代碼如下:
var add = function(x, y){
return x + y;
}
return x + y;
}
事實上,這樣的語法糖更容易使傳統(tǒng)領域的程序員產(chǎn)生誤解,function關(guān)鍵字會調(diào)用Function來new一個對象,并將參數(shù)表和函數(shù)體準確的傳遞給Function的構(gòu)造器。
通常來說,在全局作用域(作用域?qū)⒃谙乱还?jié)詳細介紹)內(nèi)聲明一個對象,只不過是對一個屬性賦值而已,比如上例中的add函數(shù),事實上只是為全局對象添加了一個屬性,屬性名為add,而屬性的值是一個對象,即function(x, y){return x+y;},理解這一點很重要,這條語句在語法上跟:
復制代碼 代碼如下:
var str = "This is a string";
并無二致。都是給全局對象動態(tài)的增加一個新的屬性,如此而已。
為了說明函數(shù)跟其他的對象一樣,都是作為一個獨立的對象而存在于JavaScript的運行系統(tǒng),我們不妨看這樣一個例子:
復制代碼 代碼如下:
function p(){
print("invoke p by ()");
}
p.id = "func";
p.type = "function";
print(p);
print(p.id+":"+p.type);
print(p());
print("invoke p by ()");
}
p.id = "func";
p.type = "function";
print(p);
print(p.id+":"+p.type);
print(p());
沒有錯,p雖然引用了一個匿名函數(shù)(對象),但是同時又可以擁有屬性,完全跟其他對象一樣,運行結(jié)果如下:
function (){
print("invoke p by ()");
}
func:function
invoke p by ()
1.2 函數(shù)的參數(shù)
在JavaScript中,函數(shù)的參數(shù)是比較有意思的,比如,你可以將任意多的參數(shù)傳遞給一個函數(shù),即使這個函數(shù)聲明時并未制定形式參數(shù),比如:
復制代碼 代碼如下:
function adPrint(str, len, option){
var s = str || "default";
var l = len || s.length;
var o = option || "i";
s = s.substring(0, l);
switch(o){
case "u":
s = s.toUpperCase();
break;
case "l":
s = s.toLowerCase();
break;
default:
break;
}
print(s);
}
adPrint("Hello, world");
adPrint("Hello, world", 5);
adPrint("Hello, world", 5, "l");//lower case
adPrint("Hello, world", 5, "u");//upper case
var s = str || "default";
var l = len || s.length;
var o = option || "i";
s = s.substring(0, l);
switch(o){
case "u":
s = s.toUpperCase();
break;
case "l":
s = s.toLowerCase();
break;
default:
break;
}
print(s);
}
adPrint("Hello, world");
adPrint("Hello, world", 5);
adPrint("Hello, world", 5, "l");//lower case
adPrint("Hello, world", 5, "u");//upper case
函數(shù)adPrint在聲明時接受三個形式參數(shù):要打印的串,要打印的長度,是否轉(zhuǎn)換為大小寫的標記。但是在調(diào)用的時候,我們可以按順序傳遞給adPrint一個參數(shù),兩個參數(shù),或者三個參數(shù)(甚至可以傳遞給它多于3個,沒有關(guān)系),運行結(jié)果如下:
Hello, world
Hello
hello
HELLO
事實上,JavaScript在處理函數(shù)的參數(shù)時,與其他編譯型的語言不一樣,解釋器傳遞給函數(shù)的是一個類似于數(shù)組的內(nèi)部值,叫arguments,這個在函數(shù)對象生成的時候就被初始化了。比如我們傳遞給adPrint一個參數(shù)的情況下,其他兩個參數(shù)分別為undefined.這樣,我們可以才adPrint函數(shù)內(nèi)部處理那些undefined參數(shù),從而可以向外部公開:我們可以處理任意參數(shù)。
我們通過另一個例子來討論這個神奇的arguments:
復制代碼 代碼如下:
function sum(){
var result = 0;
for(var i = 0, len = arguments.length; i < len; i++){
var current = arguments[i];
if(isNaN(current)){
throw new Error("not a number exception");
}else{
result += current;
}
}
return result;
}
print(sum(10, 20, 30, 40, 50));
print(sum(4, 8, 15, 16, 23, 42));//《迷失》上那串神奇的數(shù)字
print(sum("new"));
var result = 0;
for(var i = 0, len = arguments.length; i < len; i++){
var current = arguments[i];
if(isNaN(current)){
throw new Error("not a number exception");
}else{
result += current;
}
}
return result;
}
print(sum(10, 20, 30, 40, 50));
print(sum(4, 8, 15, 16, 23, 42));//《迷失》上那串神奇的數(shù)字
print(sum("new"));
函數(shù)sum沒有顯式的形參,而我們又可以動態(tài)的傳遞給其任意多的參數(shù),那么,如何在sum函數(shù)中如何引用這些參數(shù)呢?這里就需要用到arguments這個偽數(shù)組了,運行結(jié)果如下:
150
108
Error: not a number exception
函數(shù)作用域
作用域的概念在幾乎所有的主流語言中都有體現(xiàn),在JavaScript中,則有其特殊性:JavaScript中的變量作用域為函數(shù)體內(nèi)有效,而無塊作用域,我們在Java語言中,可以這樣定義for循環(huán)塊中的下標變量:
public void method(){
for(int i = 0; i < obj1.length; i++){
//do something here;
}
//此時的i為未定義
for(int i = 0; i < obj2.length; i++){
//do something else;
}
}
而在JavaScript中:
復制代碼 代碼如下:
function func(){
for(var i = 0; i < array.length; i++){
//do something here.
}
//此時i仍然有值,及I == array.length
print(i);//i == array.length;
}
for(var i = 0; i < array.length; i++){
//do something here.
}
//此時i仍然有值,及I == array.length
print(i);//i == array.length;
}
JavaScript的函數(shù)是在局部作用域內(nèi)運行的,在局部作用域內(nèi)運行的函數(shù)體可以訪問其外層的(可能是全局作用域)的變量和函數(shù)。JavaScript的作用域為詞法作用域,所謂詞法作用域是說,其作用域為在定義時(詞法分析時)就確定下來的,而并非在執(zhí)行時確定,如下例:
復制代碼 代碼如下:
var str = "global";
function scopeTest(){
print(str);
var str = "local";
print(str);
}
scopeTest();
function scopeTest(){
print(str);
var str = "local";
print(str);
}
scopeTest();
運行結(jié)果是什么呢?初學者很可能得出這樣的答案:
global
local
而正確的結(jié)果應該是:
undefined
local
因為在函數(shù)scopeTest的定義中,預先訪問了未聲明的變量str,然后才對str變量進行初始化,所以第一個print(str)會返回undifined錯誤。那為什么函數(shù)這個時候不去訪問外部的str變量呢?這是因為,在詞法分析結(jié)束后,構(gòu)造作用域鏈的時候,會將函數(shù)內(nèi)定義的var變量放入該鏈,因此str在整個函數(shù)scopeTest內(nèi)都是可見的(從函數(shù)體的第一行到最后一行),由于str變量本身是未定義的,程序順序執(zhí)行,到第一行就會返回未定義,第二行為str賦值,所以第三行的print(str)將返回”local”。
函數(shù)上下文
在Java或者C/C++等語言中,方法(函數(shù))只能依附于對象而存在,不是獨立的。而在JavaScript中,函數(shù)也是一種對象,并非其他任何對象的一部分,理解這一點尤為重要,特別是對理解函數(shù)式的JavaScript非常有用,在函數(shù)式編程語言中,函數(shù)被認為是一等的。
函數(shù)的上下文是可以變化的,因此,函數(shù)內(nèi)的this也是可以變化的,函數(shù)可以作為一個對象的方法,也可以同時作為另一個對象的方法,總之,函數(shù)本身是獨立的。可以通過Function對象上的call或者apply函數(shù)來修改函數(shù)的上下文:
call和apply
call和apply通常用來修改函數(shù)的上下文,函數(shù)中的this指針將被替換為call或者apply的第一個參數(shù),我們不妨來看看JavaScript入門之對象與JSON中的例子:
//定義一個人,名字為jack
var jack = {
name : "jack",
age : 26
}
//定義另一個人,名字為abruzzi
var abruzzi = {
name : "abruzzi",
age : 26
}
//定義一個全局的函數(shù)對象
function printName(){
return this.name;
}
//設置printName的上下文為jack, 此時的this為jack
print(printName.call(jack));
//設置printName的上下文為abruzzi,此時的this為abruzzi
print(printName.call(abruzzi));
print(printName.apply(jack));
print(printName.apply(abruzzi));
只有一個參數(shù)的時候call和apply的使用方式是一樣的,如果有多個參數(shù):
setName.apply(jack, ["Jack Sept."]);
print(printName.apply(jack));
setName.call(abruzzi, "John Abruzzi");
print(printName.call(abruzzi));
得到的結(jié)果為:
Jack Sept.
John Abruzzi
apply的第二個參數(shù)為一個函數(shù)需要的參數(shù)組成的一個數(shù)組,而call則需要跟若干個參數(shù),參數(shù)之間以逗號(,)隔開即可。
使用函數(shù)
前面已經(jīng)提到,在JavaScript中,函數(shù)可以
◆ 被賦值給一個變量
◆ 被賦值為對象的屬性
◆ 作為參數(shù)被傳入別的函數(shù)
◆ 作為函數(shù)的結(jié)果被返回
我們就分別來看看這些場景:
賦值給一個變量:
//聲明一個函數(shù),接受兩個參數(shù),返回其和
function add(x, y){
return x + y;
}
var a = 0;
a = add;//將函數(shù)賦值給一個變量
var b = a(2, 3);//調(diào)用這個新的函數(shù)a
print(b);
這段代碼會打印”5”,因為賦值之后,變量a引用函數(shù)add,也就是說,a的值是一個函數(shù)對象(一個可執(zhí)行代碼塊),因此可以使用a(2, 3)這樣的語句來進行求和操作。
賦值為對象的屬性:
復制代碼 代碼如下:
var obj = {
id : "obj1"
}
obj.func = add;//賦值為obj對象的屬性
obj.func(2, 3);//返回5
id : "obj1"
}
obj.func = add;//賦值為obj對象的屬性
obj.func(2, 3);//返回5
事實上,這個例子與上個例子的本質(zhì)上是一樣的,第一個例子中的a變量,事實上是全局對象(如果在客戶端環(huán)境中,表示為window對象)的一個屬性。而第二個例子則為obj對象,由于我們很少直接的引用全局對象,就分開來描述。
作為參數(shù)傳遞:
//高級打印函數(shù)的第二個版本
function adPrint2(str, handler){
print(handler(str));
}
//將字符串轉(zhuǎn)換為大寫形式,并返回
function up(str){
return str.toUpperCase();
}
//將字符串轉(zhuǎn)換為小寫形式,并返回
function low(str){
return str.toLowerCase();
}
adPrint2("Hello, world", up);
adPrint2("Hello, world", low);
運行此片段,可以得到這樣的結(jié)果:
HELLO, WORLD
hello, world
應該注意到,函數(shù)adPrint2的第二個參數(shù),事實上是一個函數(shù),將這個處理函數(shù)作為參數(shù)傳入,在adPrint2的內(nèi)部,仍然可以調(diào)用這個函數(shù),這個特點在很多地方都是有用的,特別是,當我們想要處理一些對象,但是又不確定以何種形式來處理,則完全可以將“處理方式”作為一個抽象的粒度來進行包裝(即函數(shù))。
作為函數(shù)的返回值:
先來看一個最簡單的例子:
復制代碼 代碼如下:
function currying(){
return function(){
print("curring");
}
}
return function(){
print("curring");
}
}
函數(shù)currying返回一個匿名函數(shù),這個匿名函數(shù)會打印”curring”,簡單的調(diào)用currying()會得到下面的結(jié)果:
復制代碼 代碼如下:
function (){
print("curring");
}
print("curring");
}
如果要調(diào)用currying返回的這個匿名函數(shù),需要這樣:
currying()();
第一個括號操作,表示調(diào)用currying本身,此時返回值為函數(shù),第二個括號操作符調(diào)用這個返回值,則會得到這樣的結(jié)果:
currying