javascript
初探JS的函数
什么是函數
函數是對象的一種,也是一段可以重復使用的代碼塊,開發人員為了完成某項功能,把相關代碼塊放到一起。
函數內部可以傳參,也可以被當做參數傳遞
目前定義函數有五種方法
具名函數來定義
function f(x, y){return x + y } f.name //'f' 復制代碼匿名函數來定義
var f f = function(x, y){return x + y } f.name //'f' 復制代碼具名函數定義了又賦值給了變量
var f1 f1 = function f(a, b){return a + b } f1.name //'f' 復制代碼要注意:雖然f1.name='f',但是f只在函數內部可用,實際上函數的名字還是f1
window.Function來構造
var f2 = new Function('x', 'y', 'return x + y') f2.name //'anonymous' 復制代碼箭頭函數
var f3 = (x, y) => {return x - y} var sum = (x, y) => x + y //函數體內只有一行代碼,可以省略大括號和return var n2 = n => n*n //只有一個參數,可以省略小括號 復制代碼常用的定義方法是1、2、5這三種方法。
函數的一些必備知識
函數的name屬性
由上面的五種定義方法,我們可以知道函數具有name屬性,而且不同的定義方法,name屬性也很奇葩。
函數如何調用
為了理解后面的this,推薦使用call()方法,而不是使用常見的f()
以第一種定義方法為例
f.call(undefined, 1, 3) 4 復制代碼call()方法的第一個參數就是this,后面的參數才是函數的執行參數。
下面用代碼檢驗一下
function f1(m, n){console.log(this)console.log(m + n) } undefined f1.call(undefined, 1, 3) Window {postMessage: ?, blur: ?, focus: ?, close: ?, frames: Window,?…} //不是應該打印undefined,為啥是window呢? 4 //這才是函數的執行內容 復制代碼執行f1.call(undefined, 1, 3)后,this不是應該打印出undefined嗎,為啥打印了Window呢(注意實際上是個小寫的window,不是瀏覽器打印的大寫的Window),可以用代碼驗證打印的就是小寫的window
function f1(m, n){console.log(this === window)console.log(m + n) } undefined f1.call(undefined, 1, 3) true //說明是小寫的window 4 function f1(m, n){console.log(this === Window)console.log(m + n) } undefined f1.call(undefined, 1, 3) false //并不是大寫的Window 4 復制代碼我真是服啦,那window和Window有啥區別呢。真是蛋疼啊,竟然考慮這個問題……
答案就是 var object = new Object,那var window = new Window。而且Window毫無探討的意義,倒是這個window是個全局屬性,多少有點用。
有時候自己真是有點鉆牛角尖,鉆進去后,還不會舉一反三。如果立刻想到obj的例子就不用浪費時間了。
這就是藏著的this
這是因為瀏覽器搗的鬼,他把undefined變成了window。接下來使用嚴格模式,讓undefined現身
function f1(m, n){ console.log(this)console.log(m + n) } undefined f1.call(undefined, 1, 3) undefined //這個undefined就是call()方法的第一個參數undefined 4 復制代碼- 而且call()的第一個參數是啥,this就是啥
arguments
前面分析了call()的第一個參數,那后倆參數是啥呢。
對,你沒猜錯,那就是arguments。
當你寫call(undefined, 1, 3)的時候。undefined可以被認為是this,[1, 3]就是arguments
函數的call stack
上面我們接觸了call()方法,現在我們學習一下當有多個函數調用的時候,JavaScript解析器是如何調用棧的。
MDN的解釋如下
調用棧是解析器(如瀏覽器中的的javascript解析器)的一種機制,可以在腳本調用多個函數時,跟蹤每個函數在完成執行時應該返回控制的點。(如什么函數正在執行,什么函數被這個函數調用,下一個調用的函數是誰)
- 當腳本要調用一個函數時,解析器把該函數添加到棧中并且執行這個函數。
- 任何被這個函數調用的函數會進一步添加到調用棧中,并且運行到它們被上個程序調用的位置。
- 當函數運行結束后,解釋器將它從堆棧中取出,并在主代碼列表中繼續執行代碼。
- 如果棧占用的空間比分配給它的空間還大,那么則會導致“堆棧溢出”錯誤。
以下是通過三個方面去理解call stack這個概念的。
普通調用
代碼如下,直觀的動圖可以看上述的鏈接
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() 復制代碼如上的代碼,先有三個函數聲明,然后是三個調用。瀏覽器先執行a.call(),然后執行b.call(),c.call(),下面結合圖具體詳細分析。
- 第一步:瀏覽器入口是a.call(),a函數入棧,執行a函數內部代碼
- 第二步:console.log('a')執行完畢,就出棧,接著a函數結束,出棧死亡
- 第三步:b.call()入棧,執行b函數內部代碼
- 第四步: console.log('b')執行完畢就出棧,接著b函數結束,出棧死亡
- 第五步:c.call()入棧,執行c函數內部代碼
- 第六步:console.log('c')執行完畢就出棧,接著c函數結束,出棧死亡。
- 整個代碼結束,瀏覽器恢復平靜。
嵌套調用
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()入棧,執行a函數內部的代碼
- 第二步: a函數的第一行語句console.log('a1'),入棧,打印出a1,這句話就出棧死亡。此時a函數繼續執行下面的代碼。
- 第三步: a函數的第二行語句b.call()入棧。執行b函數內部的代碼。
- 第四步:進入b函數內部,b函數的第一行語句console.log('b1')入棧,打印出b1,就出棧死亡。
- 第五步:b函數的第二行c.call()入棧,又進入c函數內部
- 第六步:進入c函數的內部,第一行語句console.log('c')入棧,打印出c,就出棧死亡。
- 第七步:c函數執行完畢,出棧死亡。
- 第八步:回到b函數內部,執行第三行代碼console.log('b2')入棧,打印出b2,出棧死亡。
- 第九步: b函數執行完畢,出棧死亡。
- 第十步: 回到a函數內部,執行第三行代碼console.log('a2'),入棧,打印出a2,就出棧死亡。
- 第十一步:a函數執行完畢,出棧死亡。
- 第十二步:console.log('end')入棧,打印出end,出棧死亡。
- 整個代碼運行完,瀏覽器歸于平靜。
遞歸調用
遞歸調用就是上面的嵌套調用的復雜變化,細心點,分析就能明白具體的代碼順序。
函數作用域
除了全局變量,其他變量只能在自己的函數內部被訪問到,其他區域無法訪問。通過幾個面試題來學習一下。
- 第一道面試題
問:alert出什么東西?
這種題切忌上去就做,容易打錯成了 a是2 一定要先把變量提升。變成如下這樣的
var a = 1 function f1(){var a alert(a) a = 2 } f1.call() 復制代碼這樣一提升就知道啦,答案:a是undefined。
- 第二道面試題
問:a是多少
這個題用就近原則好做。
用樹形結構來分析,當上面的代碼被瀏覽器渲染之后
- 全局變量里面有:var a = 1,f1、f2函數
- f1函數作用域里面又重新聲明了一個var a = 2
- f2函數作用域里面是console.log(a)
所以打印的那個a就是全局的a,答案是a=1
總結
- 上一篇: .net运行项目的几种形式
- 下一篇: Kotlin系列之类和属性