"Code tailor",爲前端開發者提供技術相關資訊以及系列基礎文章,微信關注「小和山的菜鳥們」公衆號,及時獲取最新文章。
在開始學習以前,咱們想要告訴您的是,本文章是對JavaScript
語言知識中 "函數" 部分的總結,若是您已掌握下面知識事項,則可跳過此環節直接進入題目練習javascript
若是您對某些部分有些遺忘,👇🏻 已經爲您準備好了!前端
函數是全部編程語言的核心部分,由於它們能夠封裝語句,且被定義後能夠在任何地方、任什麼時候間執行。ECMAScript
中的函數使用 function
關鍵字進行聲明,後跟一組參數,而後是函數體。java
如下是函數的基本語法:express
function functionName(arg0, arg1,...,argN) { //表達式 }
下面是一個例子:編程
function sayHi(name, message) { console.log('Hello ' + name + ', ' + message) }
能夠經過函數名來調用函數,要傳給函數的參數放在括號裏(若是有多個參數,則用逗號隔開)。數組
下面是調用函數 sayHi()
的示例:微信
sayHi('xhs', 'do you study today?')
調用這個函數的輸出結果是 "Hello xhs, do you study today?"
。 參數 name
和 message
在函數內部做爲字符串被拼接在了一塊兒,最終經過 console.log()
輸出到控制檯。編程語言
ECMAScript
中的函數不須要指定是否返回值,也不限制返回值的類型。任何函數在任什麼時候間均可以使用 return
語句來返回函數的值,用法是後跟要返回的值,返回值能夠是任何類型。好比:函數
function sum(num1, num2) { return num1 + num2 }
函數 sum()
會將兩個值相加並返回結果。注意,除了 return
語句以外沒有任何特殊聲明代表該函數有返回值。而後就能夠這樣調用它:學習
const result = sum(5, 10)
值得注意的是,只要執行到 return
語句,函數就會當即中止執行並退出。所以,return
語句後面的代碼不會被執行。好比:
function sum(num1, num2) { return num1 + num2 console.log('Hello world') //不會執行 }
在這個例子中,console.log()
不會執行,由於它在 return
語句後面,解釋器在執行到 return
語句後就中止對該函數的執行。
一個函數裏能夠有多個 return
語句,像這樣:
function subtract(num1, num2) { if (num1 < num2) { return num2 - num1 } else { return num1 - num2 } }
這個 subtract()
函數用於計算兩個數值的差。若是第一個數值小於第二個,則用第二個減第一個;不然,就用第一個減第二個。代碼中每一個分支都有本身的 return
語句,返回正確的差值。return
語句也能夠不帶返回值。這時候,函數會當即中止執行並返回 undefined
。這種用法最經常使用於提早終止函數執行,並非爲了返回值。好比在下面的例子中,console.log()
不會執行:
function sayHi(name, message) { return console.log('Hello ' + name + ', ' + message) // 不會執行 }
根據開發經驗而言,一個函數要麼返回值,要麼不返回值。只在某個條件下返回值的函數會帶來麻煩,尤爲是調試時。
函數名是一個指向函數的指針,因此它們跟其餘包含對象指針的變量具備相同的行爲。這意味着一個函數能夠有多個函數名,以下所示:
function sum(num1, num2) { return num1 + num2 } console.log(sum(10, 10)) // 20 let xhsSum = sum console.log(xhsSum(10, 10)) // 20 sum = null console.log(xhsSum(10, 10)) // 20
以上代碼定義了一個名爲 sum
的函數,用於求兩個數之和。而後又聲明瞭一個變量 xhsSum
,並將它的值設置爲等於 sum
。注意,使用不帶括號的函數名會訪問函數指針名,而不會執行函數。此時,xhsSum
和 sum
都指向同一個函數。調用 xhsSum()
也能夠返回結果。把 sum
設置爲 null
以後,就切斷了它與函數之間的關聯。而 xhsSum()
仍是能夠照常調用,沒有問題。
ECMAScript
函數的參數跟大多數其餘語言不一樣。ECMAScript
函數既不關心傳入的參數個數,也不關心這些參數的數據類型。定義函數時要接收兩個參數,並不意味着調用時就傳兩個參數。你能夠傳一個、三個,也能夠一個也不傳,解釋器都不會報錯。
之因此會這樣,主要是由於 ECMAScript
函數的參數在內部表現爲一個數組。函數被調用時總會接收一個數組,但函數並不關心這個數組中包含什麼。若是數組中什麼也沒有,那沒問題;若是數組的元素超出了要求,那也沒問題。
事實上,在使用 function
關鍵字定義(非箭頭)函數時,能夠在函數內部訪問 arguments
對象,從中取得傳進來的每一個參數值。arguments
對象是一個類數組對象(但不是 Array
的實例),所以可使用中括號語法訪問其中的元素(第一個參數是 arguments[0]
,第二個參數是 arguments[1]
)。而要肯定傳進來多少個參數,能夠訪問 arguments.length
屬性。在下面的例子中,sayHi()
函數的第一個參數叫 name
:
function sayHi(name, message) { console.log('Hello ' + name + ', ' + message) }
能夠經過 arguments[0]
取得相同的參數值。所以,把函數重寫成不聲明參數也能夠:
function sayHi() { console.log('Hello ' + arguments[0] + ', ' + arguments[1]) }
在重寫後的代碼中,沒有命名參數。name
和 message
參數都不見了,但函數照樣能夠調用。這就代表,ECMAScript
函數的參數只是爲了方便才寫出來的,並非必須寫出來的。與其餘語言不一樣,在 ECMAScript
中的命名參數不會建立讓以後的調用必須匹配的函數簽名。這是由於根本不存在驗證命名參數的機制。也能夠經過 arguments
對象的 length
屬性檢查傳入的參數個數。下面的例子展現了在每調用一個函數時,都會打印出傳入的參數個數:
function getArgsNum() { console.log(arguments.length) } getArgsNum('string', 45) // 2 getArgsNum() // 0 getArgsNum(12) // 1
這個例子分別打印出 2
、 0
和 1
(按順序)。既然如此,那麼開發者能夠想傳多少參數就傳多少參數。
好比:
function getTotle() { if (arguments.length === 1) { console.log(arguments[0] + 10) } else if (arguments.length === 2) { console.log(arguments[0] + arguments[1]) } } getTotle(10) // 20 getTotle(30, 20) // 50
這個函數 getTotle()
在只傳一個參數時會加 10
,在傳兩個參數時會將它們相加,而後返回。所以 getTotle(10)
返回 20
,而 getTotle(30,20)
返回 50
。雖然不像真正的函數重載那麼明確,但這已經足以彌補 ECMAScript
在這方面的缺失了。
還有一個必須理解的重要方面,那就是 arguments
對象能夠跟命名參數一塊兒使用,好比:
function getTotle(num1, num2) { if (arguments.length === 1) { console.log(num1 + 10) } else if (arguments.length === 2) { console.log(arguments[0] + num2) } }
在這個 getTotle()
函數中,同時使用了兩個命名參數和 arguments
對象。命名參數 num1
保存着與 arugments[0]
同樣的值,所以使用誰都無所謂。(一樣,num2
也保存着跟 arguments[1]
同樣的值。)arguments
對象的另外一個有意思的地方就是,它的值始終會與對應的命名參數同步。來看下面的例子:
function getTotle(num1, num2) { arguments[1] = 10 console.log(arguments[0] + num2) }
這個 getTotle()
函數把第二個參數的值重寫爲 10
。由於 arguments
對象的值會自動同步到對應的命名參數,因此修改 arguments[1]
也會修改 num2
的值,所以二者的值都是 10
。但這並不意味着它們都訪問同一個內存地址,它們在內存中仍是分開的,只不過會保持同步而已。
另外還要記住一點:若是隻傳了一個參數,而後把 arguments[1]
設置爲某個值,那麼這個值並不會反映到第二個命名參數。這是由於 arguments
對象的長度是根據傳入的參數個數,而非定義函數時給出的命名參數個數肯定的。對於命名參數而言,若是調用函數時沒有傳這個參數,那麼它的值就是 undefined
。這就相似於定義了變量而沒有初始化。
好比,若是隻給 getTotle()
傳了一個參數,那麼 num2
的值就是 undefined
。嚴格模式下,arguments
會有一些變化。首先,像前面那樣給 arguments[1]
賦值不會再影響 num2
的值。就算把 arguments[1]
設置爲 10
,num2
的值仍然仍是傳入的值。其次,在函數中嘗試重寫 arguments
對象會致使語法錯誤。(代碼也不會執行。)
ECMAScript
函數不能像傳統編程那樣重載。在其餘語言好比 Java
中,一個函數能夠有兩個定義,只要簽名(接收參數的類型和數量)不一樣就行。如前所述,ECMAScript
函數沒有簽名,由於參數是由包含零個或多個值的數組表示的。沒有函數簽名,天然也就沒有重載。若是在ECMAScript
中定義了兩個同名函數,則後定義的會覆蓋先定義的。來看下面的例子:
function addSomeNumber(num) { return num + 100 } function addSomeNumber(num) { return num + 200 } let result = addSomeNumber(100) // 300
這裏,函數 addSomeNumber()
被定義了兩次。第一個版本給參數加 100
,第二個版本加 200
。最後一行調用這個函數時,返回了 300
,由於第二個定義覆蓋了第一個定義。
咱們能夠經過 function
構造一個函數,也能夠將一個變量賦值成一個函數,這兩種方法在大多數狀況下均可以正常調用執行。但是在 JavaScript
引擎在加載數據時對函數聲明和函數表達式是區別對待的。JavaScript
引擎在任何代碼執行以前,會先讀取函數聲明,並在執行上下文中生成函數定義。而函數表達式必須等到代碼執行到它那一行,纔會在執行上下文中生成函數定義。來看下面的例子:
// 沒問題 console.log(sum(10, 10)) function sum(num1, num2) { return num1 + num2 }
以上代碼能夠正常運行,由於函數聲明會在任何代碼執行以前先被讀取並添加到執行上下文。這個過程叫做函數聲明提高(function declaration hoisting
)。在執行代碼時,JavaScript
引擎會先執行一遍掃描,把發現的函數聲明提高到源代碼樹的頂部。所以即便函數定義出如今調用它們的代碼以後,引擎也會把函數聲明提高到頂部。若是把前面代碼中的函數聲明改成等價的函數表達式,那麼執行的時候就會出錯:
// 會出錯 console.log(sum(10, 10)) var sum = function (num1, num2) { return num1 + num2 }
由於函數名在 ECMAScript
中就是變量,因此函數能夠用在任何可使用變量的地方。這意味着不只能夠把函數做爲參數傳給另外一個函數,並且還能夠在一個函數中返回另外一個函數。
function add10(num) { return num + 10 } let result1 = callSomeFunction(add10, 10) console.log(result1) // 20 function getGreeting(name) { return 'Hello, ' + name } let result2 = callSomeFunction(getGreeting, 'Nicholas') console.log(result2) // "Hello, Nicholas"
function fun1(x) { return function (y) { return function (z) { console.log(x * y * z) } } } fun1(2)(3)(4) //24
this
是一個特殊的對象,它在標準函數和箭頭函數中有不一樣的行爲。
在標準函數中,this
引用的是把函數當成方法調用的上下文對象,這時候一般稱其爲 this
值(在網頁的全局上下文中調用函數時,this
指向 window
對象)。來看下面的例子:
window.color = 'red' let o = { color: 'blue', } function sayColor() { console.log(this.color) } sayColor() // 'red' o.sayColor = sayColor o.sayColor() // 'blue'
定義在全局上下文中的函數 sayColor()
引用了 this
對象。這個 this
到底引用哪一個對象必須到函數被調用時才能肯定。所以這個值在代碼執行的過程當中可能會變。若是在全局上下文中調用 sayColor()
,這結果會輸出 red
,由於 this
指向 window
,而 this.color
至關於 window.color
。而在把 sayColor()
賦值給 o
以後再調用 o.sayColor()
,this
會指向 o
,即 this.color
至關於 o.color
,因此會顯示 blue
。
在箭頭函數中,this
引用的是定義箭頭函數的上下文。箭頭函數的內容咱們會在第二部分的函數擴展中詳細說起。
遞歸函數一般的形式是一個函數經過名稱調用本身,以下面的例子所示:
function factorial(num) { if (num <= 1) { return 1 } else { return num * factorial(num - 1) } }
這是經典的遞歸階乘函數。雖然這樣寫是能夠的,但若是把這個函數賦值給其餘變量,就會出問題:
let anotherFactorial = factorial factorial = null console.log(anotherFactorial(4)) // 報錯
這裏把 factorial()
函數保存在了另外一個變量 anotherFactorial
中,而後將 factorial
設置爲 null
,因而只保留了一個對原始函數的引用。而在調用 anotherFactorial()
時,要遞歸調用 factorial()
,但由於它已經不是函數了,因此會出錯。在寫遞歸函數時使用 arguments.callee
能夠避免這個問題。arguments.callee
就是一個指向正在執行的函數的指針,所以能夠在函數內部遞歸調用,以下所示:
function factorial(num) { if (num <= 1) { return 1 } else { return num * arguments.callee(num - 1) } }
把函數名稱替換成 arguments.callee
,能夠確保不管經過什麼變量調用這個函數都不會出問題。所以在編寫遞歸函數時 arguments.callee
是引用當前函數的首選。不過,在嚴格模式下運行的代碼是不能訪問 arguments.callee
的,由於訪問會出錯。此時,可使用命名函數表達式(named function expression
)達到目的。好比:
const factorial = function f(num) { if (num <= 1) { return 1 } else { return num * f(num - 1) } }
一:如下代碼輸出什麼
function callFunc(func, argument) { return func(argument) } function func1(argument) { console.log(argument + '.') } callFunc(func1, 'hello,world')
二:如下代碼輸出什麼
function recursion(num) { if (num === 0) return 1 else if (num % 2 === 0) return recursion(num - 1) + 1 else return recursion(num - 1) } console.log(recursion(6))
三:兩段代碼分別執行各會輸出什麼
func1(1, 2) function func2(x, y) { console.log(x + y) } let func1 = func2
func2(1, 2) function func2(x, y) { console.log(x + y) }
1、
Answer: 'hello,world.'
callFunc
函數的做用其實就是執行了第一個參數,並把第二個參數傳給第一個參數當作參數。
2、
Answer: 4
recursion
是一個遞歸函數,若是 num
等於0
就返回1
,若是 num
是偶數,就在下一個結果前 +1
,若是是奇數就返回下一個結果。其做用就是統計從 0
到 num
一共有多少個偶數。因此 recursion(6)
爲 4
3、
func1(1,2)
輸出 ReferenceError
,由於在調用時 func1
還未被定義和賦值。 func2(1,2)
輸出 3
,由於 function
的聲明會被提早,能夠在以前使用。