函數的定義和參數
結構
- 函數式的不同點到底是什么?
- 函數作為對象的樂趣
- 函數定義
- 函數的實參和形參
- 小結
- 練習
函數式的不同點到底是什么?
函數式概念之所以如此重要, 原因之一在于函數是執行過程中的主要模塊單元.
既然大多數代碼都以函數作為模塊單元, 那么就應該盡可能少的限制函數的使用方式, 從而引出函數是一等公民的概念. 函數擁有對象的所有能力. 從一等公民的概念上看, 函數應該具有如下能力:
// 使用字面量創建一個函數
function f() {}
// 將函數賦值給變量
var v = f
// 將函數插入到數組中
var arr = []
arr.push(f)
// 將函數作為對象的屬性
var obj = {}
obj.f = f
// 將函數作為參數傳遞給另外一個函數
function call(func) {
func()
}
call(f)
// 將函數作為一個值從另一個函數返回
function creator() {
return function() {}
}
// 奇特的是, 函數其實是對象, 給一個函數添加屬性
// 函數和其他非函數的對象的主要區別在于函數是可調用的, 而其它對象不是
f.status = "done"
函數式編程是一種編程風格, 或稱為范式. 函數式編程不是唯一的編程范式, 對于同一個問題, 可以使用函數式風格解決, 也可以通過其他編程風格解決. 其他編程風格還有命令式(C語言), 面向對象式(Java). 函數式編程本身是一個較大的問題, 一般來說, 使用函數式編程的主要優勢在于其代碼易于測試, 擴展和模塊化.
回調函數
高效使用Javascript編程的關鍵在于使用回調函數. 基于Javascript中函數作為一等對象且函數可以定義在任何表達式出現的位置, 也就可以方便的將函數做為回調函數傳遞給另一個函數完成調用. 這樣的調用通常是異步的.
let arr = []
for(let i = 0; i < 20; i++) {
arr.push(Number.parseInt(Math.random()*100))
}
// 將比較函數作為回調函數傳遞給排序函數, 該函數定義在參數列表中
arr.sort(function(v1, v2) {
return v1 - v2
})
函數作為對象的樂趣
在Javascript中一個讓人感覺驚訝的地方是函數是對象. 和其他所有對象一樣可以賦予屬性, 屬性可以是任意的值. 如此可以產生一些特別的應用, 如賦值給函數一個id來管理函數的集合, 或者將函數每次計算得到的值緩存在其自身的屬性中, 如此可以在后續調用時首先根據入參查找是否已經緩存過結果, 從而提高運算性能.
管理函數集合
var store = {
nextId: 1,
cache: {},
add: function(fn) {
if(!fn.id) {
fn.id = this.nextId++
this.cache[fn.id] = fn
return true
}
}
}
store對象的作用是存儲一個函數的集合用于如某個事件觸發之后調用其中存儲的所有回調函數. 也可以簡單的使用數組來存儲這樣一個集合, 這樣就需要一個方式確認向集合中添加函數時不會添加了重復的函數. 使用數組來存儲這個集合也就意味著每次添加函數時都需要遍歷一遍集合以確保集合中不會保存重復的函數. 而使用store對象則是更簡潔的方法, 添加函數進入集合時不需要每次都遍歷一遍集合, 只需簡單的在函數上設置一個標志位即可.
自記憶函數
function isPrime(value) {
// 首先創建緩存區
if(!isPrime.cache) {
isPrime.cache = {}
}
// 在緩存區查找緩存值
if(isPrime.cache[value] !== undefined) {
return isPrime.cache[value]
}
// 沒有緩存值的情況下才執行運算
let prime = value !== 0 && value !== 1
for(let i = 2; i < value; i++) {
if(value % i === 0) {
prime = false
break
}
}
// 將執行運算的結果存入緩存區
return isPrime.cache[value] = prime
}
以上代碼利用了函數是對象的特性創建了一個可以緩存計算結果的函數, 此函數將這個緩存操作封裝在其內部, 外部調用者無需以任何特殊方式取使用它就可以獲得性能的提升.
函數定義
函數通常使用函數字面量來創建函數值, 然而作為第一類對象, 函數可以使用程序中的值定義的, 如字符串或變量中的值. 一共有4類函數定義方式:
- 函數定義和函數表達式
function func() { return 1 }
var func = function() { return 1 }
- 箭頭函數
param => param * 2
- 函數構造函數
new Function('a', 'b', 'return a + b')
- 生成器函數
function* gen() { yield 1 }
函數創建的方式影響了函數可被調用的時間, 函數的行為及函數可以在哪個對象上被調用.
函數聲明和函數表達式
首先是函數聲明, 其定義語法是: function func(a, b) { return a + b }
, 它和函數表達式看起來類似, 其特點是函數聲明是獨立的Javascript代碼塊, 作為一個單獨的Javascript語句.
接著是函數表達式, 其定義語法是: function(a, b) { return a + b }
, 可以看到它就像一個沒有名字的函數聲明. 實際上函數表達式幾乎總是其他表達式的一部分, 例如可以出現在賦值表達式的右值, 或出現在函數調用的參數列表中. 簡單來說它就是一個表達式.
最后是立即函數, 對于基本的函數調用而言首先求值得到函數的標識符作為左值, 接著使用函數調用運算符(即一對括號)調用這個函數. 函數表達式的定義本身就可以作為函數標識符的左值, 因此可以使用一對括號立即調用這個新定義的函數, 這就是立即調用函數表達式(IIFE).
// 從語法層面依然需要一對括號括起函數表達式的定義
// 如果使用函數表達式定義函數, 而該定義獨立的出現在程序代碼中
// 而非作為其他表達式的一部分, 解釋器將會報錯, 因為其認為這是一個
// 忘記寫函數名的函數聲明, 因此需要讓函數定義表達式出現在另一個表達式中,
// 如使用運算符通知解釋器這里是一個表達式
;(function(){
console.log('immediately')
})()
;(function() {
console.log('expression')
}())
// 使用4個一元運算符依然可以通知解釋器接下來的函數表達式是一個表達式而非語句
;+function(){
console.log('+++')
}()
;-function(){
console.log('---')
}()
;!function(){
console.log('!!!')
}()
;~function(){
console.log('~~~')
}()
箭頭函數
Javascript中會大量使用函數, 因此ES6標準中出于簡化創建函數方式的目的新增了箭頭函數, 一般來說箭頭函數就是函數表達式的簡化版. 箭頭函數有兩種可選方式:
// 函數體就是一個表達式時省略了return, 該表達式的求值結果就是函數的值
var greet = name => 'Greetings ' + name
// 和函數表達式一樣, 使用大括號包含了整個函數體
var greet = name => {
var helloString = 'Greetings '
return helloString + name
}
函數的實參和形參
- 形參是定義函數時所列舉的變量
- 實參是調用函數時所傳遞給函數的值
當調用一個函數時所列舉的實參會按照順序賦值給函數內的形參. 當實參多余形參或形參多余實參時都不會拋出錯誤. 多出的實參被簡單的丟棄, 而多出的形參則被賦值為undefined.
剩余參數
在形參列表中的最后一個形參可以使用剩余參數, 使用...
標識這個形參是剩余參數, 它將接收所有還未被之前形參所接收的實參的值, 并將這些值放到一個數組中. 如果沒有任何可以接收的實參那么剩余參數將被賦值為空數組而非undefined
.
默認參數
默認參數是ES6的新特性, 其目的依然是書寫簡潔的代碼, 看下面的例子中的ES6之前的默認參數處理方式和ES6之后的默認參數處理方式就可以看出其區別:
function sum(a, b) {
// ES6之前處理默認參數需要寫出冗長的代碼
a = typeof a === 'undefined' ? 0 : a
b = typeof b === 'undefined' ? 0 : b
return a + b
}
// ES6之后簡單的在參數列表中指定其默認值
function sum(a=0, b=0) {
return a + b
}
// 默認參數可以引用參數列表中之前的參數的值
// 現在如果只向sum傳遞一個參數那么它的功能就變成了double
function sum(a=0, b=a) {
return a + b
}