函數(shù)表達(dá)式
JS定義函數(shù)的方式有兩種:一種是函數(shù)聲明窄绒,另一種就是函數(shù)表達(dá)式彰导。函數(shù)聲明的語(yǔ)法是這樣的位谋。
function functionName(arg0, arg1, arg2) {
//function body
}
函數(shù)聲明掏父,它的一個(gè)重要特征就是函數(shù)聲明提升(function declaration hoisting),意思是在執(zhí)行代碼之前會(huì)先讀取函數(shù)聲明仅讽。這就意味著可以把函數(shù)聲明放在調(diào)用它的語(yǔ)句后面洁灵。
sayHi()
function sayHi(){
alert("Hi!")
}
第二種創(chuàng)建函數(shù)的方式是使用函數(shù)表達(dá)式徽千。函數(shù)表達(dá)式有幾種不同的語(yǔ)法形式罐栈。下面是最常見的一種形式。
var functionName = function(arg0, arg1, arg2){
//function body
}
這種形式實(shí)際上跟常規(guī)的變量賦值沒有區(qū)別, 就是創(chuàng)建一個(gè)變量, 然后將函數(shù)賦值給變量, 但是我們需要注意的是, 上面這種方式創(chuàng)建的函數(shù)function后面是沒有跟函數(shù)名的, 我們叫這種函數(shù)為匿名函數(shù)或者拉姆達(dá)函數(shù)
這里需要注意的是, 函數(shù)表達(dá)式?jīng)]有函數(shù)提升
函數(shù)表達(dá)式在遞歸中的應(yīng)用
假設(shè)我們有以下階乘函數(shù)
function factorial(num) {
if(num < 1) {
return 1
}
return num * factorial(num - 1)
}
這個(gè)函數(shù)看起來(lái)并沒有什么問(wèn)題, 但實(shí)際上確有一個(gè)隱藏的漏洞, 例如下面這種情況
let fn = factorial
factorial = null
console.log(fn(5))
上面的代碼中, 我們先將factorial賦值給fn, 然后將factorial指向null, 當(dāng)我們調(diào)用fn函數(shù)時(shí), 遞歸函數(shù)在運(yùn)行到return num * factorial(num - 1)
這句, 由于factorial指向了null, 所以函數(shù)就會(huì)報(bào)錯(cuò)
我們可以通過(guò)arguments.callee
來(lái)解決這個(gè)問(wèn)題, arguments.callee
時(shí)一個(gè)指向正在執(zhí)行的函數(shù)的指針, 所以可以實(shí)現(xiàn)遞歸調(diào)用
function factorial(num) {
if(num < 1) {
return 1
}
return num * arguments.callee(num - 1)
}
但是, arguments.callee
在es標(biāo)準(zhǔn)中已經(jīng)漸漸被拋棄, 并且在嚴(yán)格模式下不能訪問(wèn)arguments.callee
, 因此我們需要更好的方法來(lái)解決這個(gè)問(wèn)題, 用函數(shù)表達(dá)式就是一個(gè)不錯(cuò)的選擇
let factorial = (function fn(num) {
if(num < 1) {
return 1
}
return num * fn(num - 1)
})
上面的代碼我們創(chuàng)建了一個(gè)fn具名函數(shù)表達(dá)式, 然后將fn賦值給factorial, 這時(shí)候, 即使把函數(shù)賦值給另外的其他變量, fn仍然有效, 就避免了上面的問(wèn)題
閉包
在《JavaScript高級(jí)程序設(shè)計(jì)》中, 對(duì)于閉包有著以下定義:
閉包是指有權(quán)訪問(wèn)另一個(gè)函數(shù)作用域中的變量的函數(shù)。
創(chuàng)建閉包的常見方式, 就是在一個(gè)函數(shù)內(nèi)部創(chuàng)建另一個(gè)函數(shù)
function fn() {
var a = 1
function foo() {
console.log(a)
}
return foo
}
let bar = foo()
bar() //2 閉包
上面的代碼就是一個(gè)典型的閉包, 我們?cè)谌汁h(huán)境中訪問(wèn)到了fn函數(shù)作用域中的變量a, 為了了解清楚這種情況的細(xì)節(jié), 我們必須要從作用域鏈入手
當(dāng)某個(gè)函數(shù)被調(diào)用時(shí), 會(huì)創(chuàng)建一個(gè)執(zhí)行環(huán)境(execution context)及相應(yīng)的作用域鏈聂抢。然后, 使用arguments和其他命名參數(shù)的值來(lái)初始化函數(shù)的活動(dòng)對(duì)象(activation object)琳疏。但在作用域鏈中, 外部函數(shù)的活動(dòng)對(duì)象始終處于第二位, 外部函數(shù)的外部函數(shù)的活動(dòng)對(duì)象處于第三位, 以此類推, 直到全局執(zhí)行環(huán)境為止空盼。
函數(shù)執(zhí)行時(shí), 為了讀取或者寫入變量的值, 就需要在作用域鏈中查找變量, 假設(shè)我們有下面的例子
function compare(value1, value2) {
if(value1 < value2) {
return -1
}else if(value1 > value2) {
return 1
}else {
return 0
}
}
let result = compare(5, 10)
當(dāng)我們調(diào)用compare()時(shí), 會(huì)創(chuàng)建一個(gè)compare的執(zhí)行環(huán)境, 該環(huán)境會(huì)初始化一個(gè)包含arguments, value1和value2的活動(dòng)對(duì)象揽趾。在創(chuàng)建執(zhí)行環(huán)境的同時(shí)還會(huì)創(chuàng)建其作用域鏈, 這條作用域鏈包含兩個(gè)執(zhí)行環(huán)境, 一個(gè)compare執(zhí)行環(huán)境和全局執(zhí)行環(huán)境(全局執(zhí)行環(huán)境中有一個(gè)包含result和compare的活動(dòng)對(duì)象)。 compare執(zhí)行環(huán)境處于作用域鏈的第一位, 全局執(zhí)行環(huán)境則處在作用域鏈的第二位
每個(gè)執(zhí)行環(huán)境都有一個(gè)表示變量的對(duì)象——變量對(duì)象俐筋。全局環(huán)境的變量對(duì)象始終存在严衬,而像compare()函數(shù)這樣的局部環(huán)境的變量對(duì)象,則只在函數(shù)執(zhí)行的過(guò)程中存在腰奋。在創(chuàng)建compare()函數(shù)時(shí)抱怔,會(huì)創(chuàng)建一個(gè)預(yù)先包含全局變量對(duì)象的作用域鏈屈留,這個(gè)作用域鏈被保存在內(nèi)部的[[Scope]]屬性中灌危。當(dāng)調(diào)用compare()函數(shù)時(shí),會(huì)為函數(shù)創(chuàng)建一個(gè)執(zhí)行環(huán)境沫勿,然后通過(guò)復(fù)制函數(shù)的[[Scope]]屬性中的對(duì)象構(gòu)建起執(zhí)行環(huán)境的作用域鏈产雹。此后蔓挖,又有一個(gè)活動(dòng)對(duì)象(在此作為變量對(duì)象使用)被創(chuàng)建并被推入執(zhí)行環(huán)境作用域鏈的前端。對(duì)于這個(gè)例子中compare()函數(shù)的執(zhí)行環(huán)境而言续膳,其作用域鏈中包含兩個(gè)變量對(duì)象:本地活動(dòng)對(duì)象和全局變量對(duì)象。顯然,作用域鏈本質(zhì)上是一個(gè)指向變量對(duì)象的指針列表,它只引用但不實(shí)際包含變量對(duì)象邪财。
無(wú)論什么時(shí)候在函數(shù)中訪問(wèn)一個(gè)變量時(shí),就會(huì)從作用域鏈中搜索具有相應(yīng)名字的變量糠馆。一般來(lái)講怎憋,當(dāng)函數(shù)執(zhí)行完畢后九昧,局部活動(dòng)對(duì)象就會(huì)被銷毀铸鹰,內(nèi)存中僅保存全局作用域(全局執(zhí)行環(huán)境的變量對(duì)象)蹋笼。但是剖毯,閉包的情況又有所不同教馆。
假設(shè)我們有下面這個(gè)閉包
function createComparisonFunction(propertyName) {
return function(object1, object2){
var value1 = object1[propertyName]
var value2 = object2[propertyName]
if (value1 < value2){
return -1
} else if (value1 > value2){
return 1
} else {
return 0
}
}
}
var compareNames = createComparisonFunction('name')
var result = compareNames({ name: 'Nicholas'}, { name: 'Greg' })
//解除對(duì)匿名函數(shù)的引用(以便釋放內(nèi)存)
compareNames = null
當(dāng)一個(gè)函數(shù)定義在另一個(gè)函數(shù)內(nèi)部時(shí), 該函數(shù)會(huì)將它的上層函數(shù)的活動(dòng)對(duì)象也添加到他的作用域中, 因此上面這個(gè)例子中的匿名函數(shù)function(object1, object2){}
的作用域鏈中, 不但包含它自己的活動(dòng)對(duì)象, 還包含了createComparisonFunction()
和全局活動(dòng)對(duì)象, 這樣, 匿名函數(shù)就可以訪問(wèn)createComparisonFunction()
中定義的所有變量胶滋。更重要的一點(diǎn), createComparisonFunction()
函數(shù)在執(zhí)行完畢后, 由于匿名函數(shù)的作用域鏈仍然引用這個(gè)活動(dòng)對(duì)象, 所以這個(gè)活動(dòng)對(duì)象不會(huì)被銷毀, 知道匿名函數(shù)被銷毀后, 它才會(huì)被銷毀镀钓。
由于閉包會(huì)攜帶包含它的函數(shù)的作用域,因此會(huì)比其他函數(shù)占用更多的內(nèi)存探遵。過(guò)度使用閉包可能會(huì)導(dǎo)致內(nèi)存占用過(guò)多箱季。雖然像V8等優(yōu)化后的JavaScript引擎會(huì)嘗試回收被閉包占用的內(nèi)存藏雏,但請(qǐng)大家還是要慎重使用閉包掘殴。
閉包和變量
作用域鏈有一個(gè)副作用我們必須要注意, 就是閉包只能取得變量的最后一個(gè)值粟誓。因?yàn)殚]包保存的時(shí)整個(gè)活動(dòng)對(duì)象而不是某個(gè)特殊的變量值
function fn() {
var result = []
for(val i=0; i<10; i++) {
result[i] = function() {
return i
}
}
return result
}
這個(gè)函數(shù)會(huì)返回一個(gè)函數(shù)數(shù)組。表面上看病瞳,似乎每個(gè)函數(shù)都應(yīng)該返自己的索引值,即位置0 的函數(shù)返回0亲善,位置1 的函數(shù)返回1蛹头,以此類推掘而。但實(shí)際上袍睡,每個(gè)函數(shù)都返回10斑胜。因?yàn)槊總€(gè)函數(shù)的作用域鏈中都保存著fn() 函數(shù)的活動(dòng)對(duì)象嫌吠, 所以它們引用的都是同一個(gè)變量i 辫诅。當(dāng)fn()函數(shù)返回后么夫,變量i 的值是10肤视,此時(shí)每個(gè)函數(shù)都引用著保存變量i 的同一個(gè)變量對(duì)象邢滑,所以在每個(gè)函數(shù)內(nèi)部i 的值都是10
我們可以通過(guò)一個(gè)匿名函數(shù)強(qiáng)制讓閉包符合預(yù)期行為
function fn() {
var result = []
for(var i=0; i<10; i++) {
result[i] = (function(num) {
return function() {
return num
}
})(i)
}
return result
}
在重寫了前面的fn()函數(shù)后乐纸,每個(gè)函數(shù)就會(huì)返回各自不同的索引值了摇予。在這個(gè)版本中庶喜,我們沒有直接把閉包賦值給數(shù)組,而是定義了一個(gè)匿名函數(shù)秩冈,并將立即執(zhí)行該匿名函數(shù)的結(jié)果賦給數(shù)組入问。這里的匿名函數(shù)有一個(gè)參數(shù)num稀颁,也就是最終的函數(shù)要返回的值匾灶。在調(diào)用每個(gè)匿名函數(shù)時(shí)阶女,我們傳入了變量i秃踩。由于函數(shù)參數(shù)是按值傳遞的,所以就會(huì)將變量i 的當(dāng)前值復(fù)制給參數(shù)num鸟赫。而在這個(gè)匿名函數(shù)內(nèi)部,又創(chuàng)建并返回了一個(gè)訪問(wèn)num 的閉包。這樣一來(lái)对碌,result 數(shù)組中的每個(gè)函數(shù)都有自己num 變量的一個(gè)副本荆虱,因此就可以返回各自不同的數(shù)值了。
關(guān)于this對(duì)象
在閉包中使用this也需要特別注意, 比如下面的例子
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name
}
}
}
alert(object.getNameFunc()()); //"The Window"(在非嚴(yán)格模式下)
按照作用域鏈來(lái)理解, 匿名函數(shù)應(yīng)該會(huì)一層一層向上找活動(dòng)對(duì)象, 那么這個(gè)this最終應(yīng)該會(huì)指向其上層對(duì)象, 但是實(shí)際情況this似乎并沒有指向其上層對(duì)象, 而是指向了window, 造成這個(gè)結(jié)果的原因是因?yàn)? 對(duì)于this和arguments這個(gè)兩個(gè)特殊變量, 內(nèi)部函數(shù)只會(huì)搜索到其自身的活動(dòng)對(duì)象為止, 并不會(huì)通過(guò)作用域鏈訪問(wèn)其外層活動(dòng)對(duì)象