js裡的函式
js中的函式
函式是一段可以重複呼叫的程式碼塊,也是一個物件,為了解決程式碼重複的問題。
函式的5種宣告方式
1.具名函式
function f(x,y){
return x+y
}
這裡function的作用相當於var,var用來宣告一個變數,而function用來宣告一個函式。var宣告的變數可以有多種型別,而function宣告的函式只能是function。
2. 匿名函式賦給變數
var f = function(x,y){
return x+y
}
在記憶體中開闢一段空間用來儲存這個匿名函式,然後把它的地址賦給var宣告的變數f。如果此時執行f = 1
3. 具名函式賦給變數
var f = function f2(x,y){
return x+y
}
這裡比較容易迷惑,但是確實有這麼變態的宣告方式。你以為同時聲明瞭f2和f兩個函式嗎?不是的,f2根本就不存在!!!f2只活在這個該死的函式內部。在函式的外部,你只能通過f.name看見f2這個字眼。也就是說,這個需要用f.call()來呼叫的函式,函式名居然是f2!!
4. 使用全域性物件window.Function宣告
var f = new Function('x','y','return x+y')
加不加new效果相同,反正也沒人用這種詭異的東西來宣告一個函式。前邊不論有多少個字串都是這個函式的引數,只有最後一個字串是函式體。
字串拼接的時候可以加變數,比如
var n = 2
var f = new Function('x','y','return x+' + n + '+y')
f(1, 2) //1+2+2=5
5.箭頭函式
var f = (x,y) => {
return x+y
}
這個應該是比較省事的一種吧!括號裡邊的是引數,後邊大括號裡邊是函式體。如果函式體裡邊只有一句你可以將大括號和return一起省掉,變成這種形式var f = (x,y) => x+y
,要麼不要去掉要麼return和大括號一起去掉。如果引數只有一個那麼你還可以省掉小括號var f = n => n*n
。
tips
函式一定會有一個返回值,就算你不寫返回值,瀏覽器也會自動給你加上一個return undefined
我們在控制檯打印出
'列印'
這兩個字,這條語句的返回值是undefined。console.log()的返回值和打印出的東西並不相同,打印出的東西是你傳入的東西,而這條語句的返回值總是undefined。 所有的函式都有一個name屬性。
function f(){} //f.name === 'f'
var f = function(){} //f.name === 'f'
var f = function f2(){} //f.name === 'f2'
var f = new Function() //f.name === 'anonymous' !!!!!這個單詞的中文意思是“匿名”
var f = () => {} //f.name === 'f'
函式的使用
一個基本型別的資料,聲明瞭之後便可以直接使用。比如var n = 3; n = n + 1
,而函式的呼叫需要用到函式的共有方法call。但是如果你只寫一個f,那它只是一個簡單的物件,並不能做什麼。函式有一種簡單的呼叫方法f()
,這裡不提。我們要說的是f.call(),通過呼叫call方法來使用這個函式。
函式在記憶體中的儲存方式:
在堆記憶體中函式開闢了一段空間,裡邊存有函式的引數和函式體,以及__proto__
屬性,整個的函式體是以字串的形式儲存的,也就是那種很雞肋的宣告方法中的最後一個引數。函式的共有屬性裡包括有呼叫函式的call方法,call方法可以把函式體存的字串當成程式碼執行。eval()
有著同樣的效果,可以將字串轉成程式碼執行。比如eval('1+1')
得到的結果是2,eval('1+"1"')
得到的結果是11。
所以我們可以把call方法想象成這個樣子
f.call = function(){
eval(f.fbody)
}
//////f是一個變數,f.call是一個屬性,f.call()是一個方法
使用f.call()進行函式的呼叫時,第一個引數是這個函式的本身,也就是在函式體內部的this。後邊的引數才是正經傳入的引數,也就是組成偽陣列arguments的資料。你可以用this得到第一個引數,用arguments[i](i為引數的index)得到後邊的引數。
普通模式下第一個引數如果是undefined瀏覽器會將其封裝為window物件(看上去是Window但this===window
的結果為true),但如果在嚴格模式下
function f(x,y){
'use strict'
console.log(this)
console.log(arguments)
return undefined
}
你傳入的是什麼得到的就是什麼,而不會多此一舉地把它們封裝成物件。因為this本來就是一個引數,並不是物件。
call stack
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()
上邊的程式碼執行結果依次是a1 b1 c b2 a2
,什麼是call stack?在你進入到c.call()之後怎麼知道是回去列印a2還是b2呢?正在執行的語句被壓入stack中,然後等待它上邊的都走了再繼續執行出棧。這就是call stack(呼叫棧)。所謂的棧溢位(stack over flow)就是棧裡存的東西太多溢位來了。
作用域
不知道你們有沒有聽過一句話:如果不寫var那宣告的就是全域性變數。事實上,如果你寫了一句a = 1
瀏覽器會優先認為它是一條賦值語句,然後沿著作用範圍的這棵樹找,如果找到了最上層還沒找到變數a,那麼才會在最上層宣告變數a。所以我們看到的結果就是聲明瞭全域性變數。
js中一個函式就是一個作用範圍,我們可以把函式看作是樹的結點,這就構成了一個表示作用範圍的樹形結構。在使用一個變數的時候,它會沿著這個作用範圍一層一層地找直至找到這個變數的宣告為止,採用的是就近原則。
如果一個函式使用了它範圍之外的值,那麼這個函式和這個變數就構成了閉包。
一個很經典的題
var liTags = document.querySelectorAll('li') //假設這個頁面有6個li
for(var i = 0; i<liTags.length; i++){
liTags[i].onclick = function(){
console.log(i) // 點選第3個 li 時,打印出來的是 2 還是 6 ?
}
}
事實上,在頁面載入完之後onclick這個事件還沒有執行,也就是說,在for迴圈的i已經等於6的時候,onclick這個事件還沒有發生。function這個容器裡邊的內容一直都在改變,它是動態的,執行這個函式的時候,我們打印出來的是i最終的值6,而不是你想當然的結果。
本來想把之前的解釋刪掉的,因為之前雖然強行解釋了但是對打印出來的i
的結果都還有些懵。但是好像刪掉之後又沒什麼好寫的了,因為這個問題懂的時候好像就沒什麼好解釋的了,似乎最後打印出的是6是自然而然的結果。而且寫的好像也沒啥錯2333
為了更好理解一些可以畫記憶體圖如下(emmm點選事件什麼的其實是存在heap裡邊的,畫錯了藍鵝不想改了好費勁而且對要表達的東西沒什麼太大的影響,所以意會一下就好……)
變數i
和事件都儲存在stack記憶體中,而函式都存在了heap記憶體裡,在事件發生的時候進行函式的呼叫,此時函式對全域性變數i
進行呼叫,所以打印出的只能是i
的最終結果,也就是6。