1. 程式人生 > >js裡的函式

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。像下圖:

  我們在控制檯打印出'列印'這兩個字,這條語句的返回值是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。