什么是函數(shù)
函數(shù)是對(duì)象的一種,也是一段可以重復(fù)使用的代碼塊,開(kāi)發(fā)人員為了完成某項(xiàng)功能,把相關(guān)代碼塊放到一起。
函數(shù)內(nèi)部可以傳參,也可以被當(dāng)做參數(shù)傳遞
目前定義函數(shù)有五種方法
具名函數(shù)來(lái)定義
function f(x, y){
return x + y
}
f.name //'f'
匿名函數(shù)來(lái)定義
var f
f = function(x, y){
return x + y
}
f.name //'f'
具名函數(shù)定義了又賦值給了變量
var f1
f1 = function f(a, b){
return a + b
}
f1.name //'f'
要注意:雖然f1.name='f',但是f只在函數(shù)內(nèi)部可用,實(shí)際上函數(shù)的名字還是f1
window.Function來(lái)構(gòu)造
var f2 = new Function('x', 'y', 'return x + y')
f2.name //'anonymous'
箭頭函數(shù)
var f3 = (x, y) => {return x - y}
var sum = (x, y) => x + y //函數(shù)體內(nèi)只有一行代碼,可以省略大括號(hào)和return
var n2 = n => n*n //只有一個(gè)參數(shù),可以省略小括號(hào)
常用的定義方法是1、2、5這三種方法。
函數(shù)的一些必備知識(shí)
函數(shù)的name屬性
由上面的五種定義方法,我們可以知道函數(shù)具有name屬性,而且不同的定義方法,name屬性也很奇葩。
函數(shù)如何調(diào)用
為了理解后面的this,推薦使用call()方法,而不是使用常見(jiàn)的f()
以第一種定義方法為例
f.call(undefined, 1, 3)
4
call()方法的第一個(gè)參數(shù)就是this,后面的參數(shù)才是函數(shù)的執(zhí)行參數(shù)。
下面用代碼檢驗(yàn)一下
function f1(m, n){
console.log(this)
console.log(m + n)
}
undefined
f1.call(undefined, 1, 3)
Window {postMessage: ?, blur: ?, focus: ?, close: ?, frames: Window, …} //不是應(yīng)該打印undefined,為啥是window呢?
4 //這才是函數(shù)的執(zhí)行內(nèi)容
執(zhí)行f1.call(undefined, 1, 3)后,this不是應(yīng)該打印出undefined嗎,為啥打印了Window呢(注意實(shí)際上是個(gè)小寫(xiě)的window,不是瀏覽器打印的大寫(xiě)的Window),可以用代碼驗(yàn)證打印的就是小寫(xiě)的window
function f1(m, n){
console.log(this === window)
console.log(m + n)
}
undefined
f1.call(undefined, 1, 3)
true //說(shuō)明是小寫(xiě)得到window
4
function f1(m, n){
console.log(this === Window)
console.log(m + n)
}
undefined
f1.call(undefined, 1, 3)
false //并不是大寫(xiě)的Window
4
我真是服啦,那window和Window有啥區(qū)別呢。真是蛋疼啊,竟然考慮這個(gè)問(wèn)題……
答案就是 var object = new Object,那var window = new Window。而且Window毫無(wú)探討的意義,倒是這個(gè)window是個(gè)全局屬性,多少有點(diǎn)用。
有時(shí)候自己真是有點(diǎn)鉆牛角尖,鉆進(jìn)去后,還不會(huì)舉一反三。如果立刻想到obj的例子就不用浪費(fèi)時(shí)間了。
這就是藏著的this
這是因?yàn)闉g覽器搗的鬼,他把undefined變成了window。接下來(lái)使用嚴(yán)格模式,讓undefined現(xiàn)身
function f1(m, n){
'use strict'
console.log(this)
console.log(m + n)
}
undefined
f1.call(undefined, 1, 3)
undefined //這個(gè)undefined就是call()方法的第一個(gè)參數(shù)undefined
4
- 而且call()的第一個(gè)參數(shù)是啥,this就是啥
function f1(m, n){
'use strict'
console.log(this)
console.log(m + n)
}
undefine
f1.call('我是啥this就是啥', 1, 3)
我是啥this就是啥 //打印的依然是call()的第一個(gè)參數(shù)
4
arguments
前面分析了call()的第一個(gè)參數(shù),那后倆參數(shù)是啥呢。
對(duì),你沒(méi)猜錯(cuò),那就是arguments。
當(dāng)你寫(xiě)call(undefined, 1, 3)的時(shí)候。undefined可以被認(rèn)為是this,[1, 3]就是arguments
函數(shù)的call stack
上面我們接觸了call()方法,現(xiàn)在我們學(xué)習(xí)一下當(dāng)有多個(gè)函數(shù)調(diào)用的時(shí)候,JavaScript解析器是如何調(diào)用棧的。
MDN的解釋如下
調(diào)用棧是解析器(如瀏覽器中的的javascript解析器)的一種機(jī)制,可以在腳本調(diào)用多個(gè)函數(shù)時(shí),跟蹤每個(gè)函數(shù)在完成執(zhí)行時(shí)應(yīng)該返回控制的點(diǎn)。(如什么函數(shù)正在執(zhí)行,什么函數(shù)被這個(gè)函數(shù)調(diào)用,下一個(gè)調(diào)用的函數(shù)是誰(shuí))
- 當(dāng)腳本要調(diào)用一個(gè)函數(shù)時(shí),解析器把該函數(shù)添加到棧中并且執(zhí)行這個(gè)函數(shù)。
- 任何被這個(gè)函數(shù)調(diào)用的函數(shù)會(huì)進(jìn)一步添加到調(diào)用棧中,并且運(yùn)行到它們被上個(gè)程序調(diào)用的位置。
- 當(dāng)函數(shù)運(yùn)行結(jié)束后,解釋器將它從堆棧中取出,并在主代碼列表中繼續(xù)執(zhí)行代碼。
- 如果棧占用的空間比分配給它的空間還大,那么則會(huì)導(dǎo)致“堆棧溢出”錯(cuò)誤。
以下是通過(guò)三個(gè)方面去理解call stack這個(gè)概念的。
普通調(diào)用
代碼如下,直觀的動(dòng)圖可以看上述的鏈接
function a(){
console.log('a')
return 'a'
}
function b(){
console.log('b')
return 'b'
}
function c(){
console.log('c')
return 'c'
}
a.call()
b.call()
c.call()
如上的代碼,先有三個(gè)函數(shù)聲明,然后是三個(gè)調(diào)用。瀏覽器先執(zhí)行a.call(),然后執(zhí)行b.call(),c.call(),下面結(jié)合圖具體詳細(xì)分析。
[圖片上傳失敗...(image-57cf0d-1515141332134)]
- 第一步:瀏覽器入口是a.call(),a函數(shù)入棧,執(zhí)行a函數(shù)內(nèi)部代碼
- 第二步:console.log('a')執(zhí)行完畢,就出棧,接著a函數(shù)結(jié)束,出棧死亡
- 第三步:b.call()入棧,執(zhí)行b函數(shù)內(nèi)部代碼
- 第四步: console.log('b')執(zhí)行完畢就出棧,接著b函數(shù)結(jié)束,出棧死亡
- 第五步:c.call()入棧,執(zhí)行c函數(shù)內(nèi)部代碼
- 第六步:console.log('c')執(zhí)行完畢就出棧,接著c函數(shù)結(jié)束,出棧死亡。
- 整個(gè)代碼結(jié)束,瀏覽器恢復(fù)平靜。
嵌套調(diào)用
function a(){
console.log('a1')
b.call()
console.log('a2')
return 'a'
}
function b(){
console.log('b1')
c.call()
console.log('b2')
return 'b'
}
function c(){
console.log('c')
return 'c'
}
a.call()
console.log('end')

- 第一步:瀏覽器的入口還是a.call(),a.call()入棧,執(zhí)行a函數(shù)內(nèi)部的代碼
- 第二步: a函數(shù)的第一行語(yǔ)句console.log('a1'),入棧,打印出a1,這句話就出棧死亡。此時(shí)a函數(shù)繼續(xù)執(zhí)行下面的代碼。
- 第三步: a函數(shù)的第二行語(yǔ)句b.call()入棧。執(zhí)行b函數(shù)內(nèi)部的代碼。
- 第四步:進(jìn)入b函數(shù)內(nèi)部,b函數(shù)的第一行語(yǔ)句console.log('b1')入棧,打印出b1,就出棧死亡。
- 第五步:b函數(shù)的第二行c.call()入棧,又進(jìn)入c函數(shù)內(nèi)部
- 第六步:進(jìn)入c函數(shù)的內(nèi)部,第一行語(yǔ)句console.log('c')入棧,打印出c,就出棧死亡。
- 第七步:c函數(shù)執(zhí)行完畢,出棧死亡。
- 第八步:回到b函數(shù)內(nèi)部,執(zhí)行第三行代碼console.log('b2')入棧,打印出b2,出棧死亡。
- 第九步: b函數(shù)執(zhí)行完畢,出棧死亡。
- 第十步: 回到a函數(shù)內(nèi)部,執(zhí)行第三行代碼console.log('a2'),入棧,打印出a2,就出棧死亡。
- 第十一步:a函數(shù)執(zhí)行完畢,出棧死亡。
- 第十二步:console.log('end')入棧,打印出end,出棧死亡。
- 整個(gè)代碼運(yùn)行完,瀏覽器歸于平靜。
遞歸調(diào)用
遞歸調(diào)用就是上面的嵌套調(diào)用的復(fù)雜變化,細(xì)心點(diǎn),分析就能明白具體的代碼順序。
函數(shù)作用域
除了全局變量,其他變量只能在自己的函數(shù)內(nèi)部被訪問(wèn)到,其他區(qū)域無(wú)法訪問(wèn)。通過(guò)幾個(gè)面試題來(lái)學(xué)習(xí)一下。
- 第一道面試題
var a = 1
function f1(){
alert(a) // 是多少
var a = 2
}
f1.call()
問(wèn):alert出什么東西?
這種題切忌上去就做,容易打錯(cuò)成了 a是2 一定要先把變量提升。變成如下這樣的
var a = 1
function f1(){
var a
alert(a)
a = 2
}
f1.call()
這樣一提升就知道啦,答案:a是undefined。
- 第二道面試題
var a = 1
function f1(){
var a = 2
f2.call()
}
function f2(){
console.log(a) // 是多少
}
f1.call()
問(wèn):a是多少
這個(gè)題用就近原則好做。

用樹(shù)形結(jié)構(gòu)來(lái)分析,當(dāng)上面的代碼被瀏覽器渲染之后
- 全局變量里面有:var a = 1,f1、f2函數(shù)
- f1函數(shù)作用域里面又重新聲明了一個(gè)var a = 2
- f2函數(shù)作用域里面是console.log(a)
所以打印的那個(gè)a就是全局的a,答案是a=1