A2 一等公民函數(shù)
在傳統(tǒng) OO 語言里杈绸,對象包含數(shù)據(jù)和方法。這些語言里,數(shù)據(jù)和方法通常是不同的概念:javascript另辟蹊徑危彩。
與其他 js 的類型一樣,函數(shù)可以作為對象處理泳桦,如String汤徽、Number、Date等灸撰。
與其他對象類似谒府,函數(shù)可以通過javascript函數(shù)定義 —— 此時函數(shù)可以
- 賦值給變量;
- 賦值給對象屬性浮毯;
- 作為參數(shù)傳遞完疫;
- 作為函數(shù)結(jié)果返回;
- 使用文本創(chuàng)建债蓝。
因為這個語言里壳鹤,函數(shù)與其他對象的處理方式類似,所以我們說函數(shù)是一等對象(first-class object)饰迹。
在 js 里芳誓,函數(shù)可以做不同的事余舶,也可以用不同的方式定義。
A2.1 函數(shù)表達式與函數(shù)聲明
函數(shù)不僅可以調(diào)用值锹淌,而且會證明這是正確的匿值。其中一種定義方式就是 函數(shù)聲明(function declaration)。思考下面代碼:
function doSomethingWonderful(){
alert('Does something wonderful');
}
函數(shù)聲明由關鍵字 function
組成葛圃,接著是函數(shù)的名字千扔,還有參數(shù)或者無參及函數(shù)體。
正確的代碼里库正,定義函數(shù)名字是 doSomethingWonderful
曲楚,這是無參的函數(shù)。當調(diào)用時褥符,就會執(zhí)行函數(shù)體龙誊,這里只調(diào)用了 alert()
彈出消息∨玳梗看起來沒有返回值趟大,但是在 js 里,如果沒前面提到的頂級定義的變量會作為 window
對象的屬性铣焊。 Function
對象也不例外逊朽。如果之前函數(shù)聲明在頂級層次,那么可以創(chuàng)建與函數(shù)名同名的window屬性曲伊。因此叽讳,下面的語句是等價的
function hello() { alert('Hi there!'); }
hello = function hello() { alert('Hi there!'); }
window.hello = function hello() { alert('Hi there!'); }
在支持 ECMAScript 6
規(guī)范的瀏覽器里,可以通過名為 name
的函數(shù)屬性名訪問函數(shù)坟募。
在 js 里岛蚤,函數(shù)可以作為代碼的一部分定義,而且被稱為函數(shù)表達式(function expression)懈糯。函數(shù)表達式的值可以作為函數(shù)對象涤妒。
var myFunc = function(){
alert('this is a function');
}
定義了一個變量myFunc
,賦值了一個函數(shù)赚哗。因為這是一條代碼她紫,注意這個函數(shù)沒有名字(name 屬性是空字符串),所以不能使用函數(shù)名調(diào)用它屿储。但是犁苏,因為已經(jīng)賦值給一個變量,所以可以按下面方法執(zhí)行:
myFunc();
這并非函數(shù)聲明與函數(shù)表達式的唯一區(qū)別扩所。另外一個重要的卻別:函數(shù)聲明是升起的(hoisted)(有的地方叫函數(shù)聲明的聲明提前),而函數(shù)表達式不是朴乖。為了實際理解這個概念祖屏,思考下面的例子:
例子中定義兩個函數(shù) funcDecl()
和 funcExpr()
助赞。但是實際定義之前,我們執(zhí)行了調(diào)用袁勺。首先調(diào)用(funcDecl();)成功雹食,然后調(diào)用(funcExpr();)拋出錯誤。不同的行為是因為 funcDecl()
是升起的期丰,而 funcExpr()
不是群叶。
var
聲明提前:
變量在聲明它們的腳本或函數(shù)中都是有定義的,變量聲明語句會被提前到腳本或函數(shù)的頂部钝荡。但是街立,變量初始化的操作還是在原來var語句的位置執(zhí)行,在聲明語句之前變量的值是undefined埠通。
下部分是實際執(zhí)行的過程赎离。
函數(shù)聲明提前
函數(shù)聲明是在預執(zhí)行期執(zhí)行的,就是說函數(shù)聲明是在瀏覽器準備執(zhí)行代碼的時候執(zhí)行的端辱。因為函數(shù)聲明在預執(zhí)行期被執(zhí)行梁剔,所以到了執(zhí)行期,函數(shù)聲明就不再執(zhí)行了(人家都執(zhí)行過了自然就不再執(zhí)行了)舞蔽。
函數(shù)聲明和函數(shù)表達式 - myvin
可以采用同樣的方式給變量賦值函數(shù)表達式荣病,也可以作為屬性賦值給對象:
var myObj = {
bar: function() {}
};
A2.2 回調(diào)函數(shù)
處理事件或計時器,或執(zhí)行 AJAX 請求時渗柿,WEB 頁面代碼的本性是異步的个盆。其中異步編程最流行的概念就是 回調(diào)函數(shù) 。
下面看計時器例子做祝±。可以調(diào)用計時器來觸發(fā) —— 假設5秒鐘,通過傳遞適當?shù)拈g隔時間給 window.setTimeout()
方法混槐。但是這個方法怎么讓等待時間結(jié)束后執(zhí)行想要的方法编兄?也是通過調(diào)用設置的函數(shù)實現(xiàn)的。
function hello(){alert('Hi there!');}
setTimeout(hello, 5000);
第一個參數(shù)設置給 setTimeout()
方法声登,是函數(shù)的引用狠鸳。傳遞函數(shù)作為參數(shù)與傳遞其他值沒有區(qū)別,就好像傳遞一個數(shù)一樣悯嗓。
當計時器結(jié)束時件舵,調(diào)用 hello
函數(shù)。因為 setTimeout()
方法在代碼里發(fā)起一個回調(diào)脯厨,所以函數(shù)被稱為 回調(diào)函數(shù)(callback function)铅祸。
因為只使用一次函數(shù),沒必要創(chuàng)建函數(shù)名 hello
。除非在其他地方多次調(diào)用函數(shù)临梗,否則沒必要創(chuàng)建 window
屬性來存儲 hello
函數(shù)并把它傳遞給回調(diào)參數(shù)涡扼。更加優(yōu)美的代碼:
setTimeout(function() {alert('Hi there!');}, 5000);
這里在參數(shù)列表直接定義(實際是內(nèi)聯(lián)匿名函數(shù)),無須生成函數(shù)名字盟庞〕曰Γ可在 jQuery 中經(jīng)常看到這種用法什猖,沒必要賦值給頂級屬性票彪。
在這個例子里創(chuàng)建的函數(shù)或者頂級函數(shù)(作為 window
屬性),或者賦值給函數(shù)調(diào)用不狮。也可以復制 Function 對象
給對象的屬性降铸。
A2.3 尋根求源
OO 語言會自動一共一種方式來引用當前方法內(nèi)對象的實例。在JAVA和C#這種語言中荤傲,this
變量指向當前實例的引用垮耳。在JS中,類似的概念存在遂黍,也使用this關鍵字终佛,還可以訪問與函數(shù)關聯(lián)的對象。但是JS實現(xiàn)的 this
與OO語言的不同雾家。
- 在OO語言中铃彰,
this
通常引用的是聲明方法類的實例。 - 在JS中芯咧,函數(shù)是一等對象牙捉,不會作為其他東西的一部分,對于通過
this
引用 —— 稱為函數(shù)上下文(function context) —— 不是由函數(shù)聲明來決定而是由函數(shù)調(diào)用(invoked)來確定的敬飒。
這意味著相同的函數(shù)可以有不同的上下文依賴邪铲,取決于如何調(diào)用它。這一點非常詭異无拗,但是非常有用带到。
默認情況下,函數(shù)調(diào)用的上下文(this)屬性包含了對于調(diào)用函數(shù)引用的對象英染。我們來看看摩托車代碼的例子揽惹,修改對象的創(chuàng)建代碼如下:
var ride = {
make: 'Yamaha',
model: 'XT660R',
year: 2014,
purchased: new Date(2015, 7, 21),
owner: {
name: 'Tg',
occupation: 'bounty hunter'
},
whatAmI: function() {
return this.year + ' ' + this.make + ' ' + this.model;
}
};
新增了代碼:
whatAmI: function() {
return this.year + ' ' + this.make + ' ' + this.model;
}
新代碼添加了一個名為 whatAmI
的屬性,它引用了一個函數(shù) Function
實例四康。新的對象層次搪搏,使用 Function
實例賦值給了一個名為 whatAmI
的屬性。
當函數(shù)通過這個屬性引用調(diào)用時疯溺,代碼如下:
var bike = ride.whatAmI();
函數(shù)上下文(this)設置為 ride
引用的對象實例。結(jié)果變量 bike
設置為 '2014 Ymaha XT660R',因為函數(shù)用過 this
獲得了對象的屬性囱嫩。
對于頂級函數(shù)也一樣嗅辣。記住頂級函數(shù)是 window 對象的屬性,所以調(diào)用函數(shù)上下文是 window 對象挠说。
雖然是通常和隱含的行為,但是JS給了我們現(xiàn)實控制函數(shù)上下文的機會愿题。可以通過 Function
方法 call()
或者 apply()
來設置調(diào)用函數(shù)的上下文损俭。雖然看起有點瘋狂,但是作為一等對象潘酗,函數(shù)有通過 Function 構(gòu)造函數(shù)定義的方法杆兵。
call()
方法調(diào)用函數(shù)指定第一個對象參數(shù)作為上下文,而剩余的參數(shù)作為調(diào)用函數(shù)使用 —— call()
的第二個參數(shù)變成調(diào)用函數(shù)的第一個參數(shù)仔夺,依次類推琐脏。apply()
方法與此方法的工作方式類似,除了第二個參數(shù)是數(shù)組參數(shù)用來調(diào)用函數(shù)使用缸兔。
call() 和 apply()
在 JavaScript 中, 函數(shù)是對象日裙。JavaScript 函數(shù)有它的屬性和方法。
call() 和 apply() 是預定義的函數(shù)方法惰蜜。 兩個方法可用于調(diào)用函數(shù)昂拂,兩個方法的第一個參數(shù)必須是對象本身。
兩者的區(qū)別在于第二個參數(shù): apply傳入的是一個參數(shù)數(shù)組抛猖,也就是將多個參數(shù)組合成為一個數(shù)組傳入格侯,而call則作為call的參數(shù)傳入(從第二個參數(shù)開始)。
在 JavaScript 嚴格模式(strict mode)下, 在調(diào)用函數(shù)時第一個參數(shù)會成為 this 的值财著, 即使該參數(shù)不是一個對象联四。
在 JavaScript 非嚴格模式(non-strict mode)下, 如果第一個參數(shù)的值是 null 或 undefined, 它將使用全局對象替代。
- 通過 call() 或 apply() 方法你可以設置 this 的值, 且作為已存在對象的新方法調(diào)用撑教。
為了強化概念朝墩,我們看一個例子。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Function Conctext Example</title>
</head>
<body>
<script type="text/javascript">
var obj1 = { handle: 'obj1' };
var obj2 = { handle: 'obj2' };
var obj3 = { handle: 'obj3' };
var value = 'test';
window.handle = 'window';
function whoAmI(param) {
return this.handle + '' + param;
}
obj1.identifyMe = whoAmI;
console.log(whoAmI(value)); //windowtest
console.log(obj1.identifyMe(value));//obj1test
console.log(whoAmI.call(obj2, value));//obj2test
console.log(whoAmI.apply(obj3, [value]));//obj3test
</script>
</body>
</html>
代碼里的定義了三個對象驮履,每個對象使用 handel
屬性來區(qū)分對象的引用①鱼辙。同樣也為 handel
實例添加了屬性,因此它易于辨認玫镐。
然后定義了一個頂級函數(shù)倒戏,它可以返回任意作為任意函數(shù)上下文對象的 handel
屬性的值②,并把同一個函數(shù)賦值給 obj1
的 identifyMe
屬性③恐似《捧危可以說在 obj1
上創(chuàng)建了一個名為 identifyMe
的方法,雖然函數(shù)是和對象獨立聲明的。
當 obj
作為函數(shù) func
調(diào)用的上下文時葛闷,函數(shù) func
作為對象 obj
的方法憋槐。為了更進一步演示這個概念,思考下面的代碼:
console.log(obj1.identifyMe.call(obj3)); //obj3undefined
雖然作為 obj1
的屬性引用了函數(shù)淑趾,但這時調(diào)用函數(shù)上下文是 obj3
,也進一步強調(diào)了函數(shù)聲明無法決定上下文而是取決于如何調(diào)用函數(shù)阳仔。
A2.4 閉包
閉包(closure)指的是 Function
實例,與其執(zhí)行需要的局部變量耦合在一起扣泊。當聲明函數(shù)時近范,它可以引用自己范圍內(nèi)的任意變量。但是對于閉包延蟹,這些變量被函數(shù)攜帶评矩,甚至在聲明點之后,已經(jīng)超出了范圍阱飘,關閉聲明斥杜。
回調(diào)函數(shù)在聲明的時候引用局部變量是一個編寫高效 javascript 代碼的必備工具。使用計時器我們來看一個例子沥匈,列表2蔗喂。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Closure Example</title>
</head>
<body>
<div id="display"></div>
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript">
function timer() {
var local = 1;
window.setInterval(
function() {
$('#display').append(
'<div>At ' + new Date() + ' local=' + local + '</div>'
);
local++;
},
2000);
}
timer();
</script>
</body>
</html>
這個例子中,我們創(chuàng)建了一個函數(shù) timer()
咐熙,在定義之后執(zhí)行⑤弱恒。在 timer()
函數(shù)內(nèi)聲明了局部變量 local
②,并且賦值為1棋恼,然后使用 window.setInterval()
方法來建立計時器返弹,每隔2秒觸發(fā)一次③。作為計時器的回調(diào)函數(shù)爪飘,我們指定了一個內(nèi)斂函數(shù)來引用局部變量 local
义起,通過向頁面里名為 display
的元素附加 div
來展示當前時間和 local
變量的值①。作為回調(diào)函數(shù)的一部分师崎,local
變量的值每次遞增1④默终。
如果不了解閉包,也許會認為犁罩,因為回調(diào)函數(shù)會在 timer()
函數(shù)調(diào)用后2秒觸發(fā)齐蔽,local
變量的值在執(zhí)行回調(diào)期間是未定義的。但是床估,加載頁面并運行一小段時間含滴,會看到如圖A.4所示結(jié)果。
雖然當
ready
處理器已經(jīng)退出谈况,local
變量已經(jīng)超出了范圍勺美,函數(shù)聲明的閉包,包含 local
碑韵,仍然存在于函數(shù)的生命周期范圍內(nèi)赡茸。
閉包的另一特性,函數(shù)上下文從來不會作為閉包的一部分祝闻。例如占卧,下面的代碼不會按照我們的預期執(zhí)行。
···js
this.id = 'someID';
$('*').each(function(){
alert(this.id);
});
記住每個函數(shù)調(diào)用都有自己的函數(shù)上下文联喘,所以屉栓,這個代碼的回調(diào)函數(shù)內(nèi)存底給 each()
函數(shù)上下文是 jQuery 集合中的元素(DOM元素),不是外部函數(shù)設置的 someID
屬性耸袜。每次調(diào)用回調(diào)函數(shù)都會輪流顯示 jQuery 集合中的每個元素的 ID。
當訪問作為函數(shù)上下文的對象時牲平,可以在局部變量里使用常見的版本來創(chuàng)建 this
引用的拷貝堤框,這個局部變量將會包括在閉包里。思考下面的代碼:
<div id="display"></div>
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript">
this.id = 'someID';
//var outer = this; //this 引用 window
$('#display').each(function() {
var outer = this; // this 引用 display
console.log(outer.id);
});
</script>
變量(絕大部分時間命名為 that)纵柿,變成了閉包的一部分蜈抓,因為它已經(jīng)在回調(diào)函數(shù)內(nèi)部被引用了,因此可以被訪問昂儒。outer
賦值給任意上下文沟使,而不是回調(diào)函數(shù)定義的上下文。例如渊跋,前面的代碼包括在名為 foo 的函數(shù)內(nèi)腊嗡,outer
變量會引用 foo
函數(shù)的上下文。如果前面的代碼定義在 HTML 頁面里拾酝,而沒有被函數(shù)包裹燕少,則 outer
變量將會引用 window
對象。
修改后的代碼現(xiàn)在顯示警告框來展示字符串 someID
任意多次蒿囤,只要 jQuery 集合中元素就行客们。
我們發(fā)現(xiàn)閉包在使用 jQuery 異步回調(diào)來創(chuàng)建優(yōu)美代碼時非常重要,尤其是在編寫 Ajax 請求和事件處理代碼時材诽。