您好,登錄后才能下訂單哦!
JavaScript 閉包與高階函數是怎樣的,相信很多沒有經驗的人對此束手無策,為此本文總結了問題出現的原因和解決方法,通過這篇文章希望你能解決這個問題。
「JavaScript 中,函數是一等公民」,在各種書籍和文章中我們總能看到這句話。
既然有一等,那么當然也有次等了。
如果公民分等級,一等公民什么都可以做,次等公民這不能做那不能做。JavaScript的函數也是對象,可以有屬性,可以賦值給一個變量,可以放在數組里作為元素,可以作為其他對象的屬性,什么都可以做,別的對象能做的它能做,別的對象不能做的它也能做。這不就是一等公民的地位嘛。
— 程墨Morgan
所以它的含義是:函數和其他普通對象一樣,其上有屬性也有方法,普通對象能做的,函數也可以做。
正因為在 JavaScript 中的極大自由,函數被賦予了卓越的表達力和靈活性,但是也產生了很多讓人抓耳撓腮的問題。本文我們就一起討論一下最常遇見的兩個與函數密切相關的概念:閉包和高階函數。這兩個概念在之后設計模式的文章中也會經常碰見。
1.1 什么是閉包
當函數可以記住并訪問所在的詞法作用域時,就產生了閉包,即使函數是在當前詞法作用域之外執行。
我們首先來看一個閉包的例子:
function foo() { var a = 2 function bar() { console.log(a) } return bar } var baz = foo() baz() // 輸出: 2
foo 函數傳遞出了一個函數 bar,傳遞出來的 bar 被賦值給 baz 并調用,雖然這時 baz 是在 foo 作用域外執行的,但 baz 在調用的時候可以訪問到前面的 bar 函數所在的 foo 的內部作用域。
由于 bar 聲明在 foo 函數內部,bar 擁有涵蓋 foo 內部作用域的閉包,使得 foo 的內部作用域一直存活不被回收。一般來說,函數在執行完后其整個內部作用域都會被銷毀,因為 JavaScript 的 GC(Garbage Collection)垃圾回收機制會自動回收不再使用的內存空間。但是閉包會阻止某些 GC,比如本例中 foo() 執行完,因為返回的 bar 函數依然持有其所在作用域的引用,所以其內部作用域不會被回收。
注意: 如果不是必須使用閉包,那么盡量避免創建它,因為閉包在處理速度和內存消耗方面對性能具有負面影響。
1.2 利用閉包實現結果緩存(備忘模式)
備忘模式就是應用閉包的特點的一個典型應用。比如有個函數:
function add(a) { return a + 1; }
多次運行 add() 時,每次得到的結果都是重新計算得到的,如果是開銷很大的計算操作的話就比較消耗性能了,這里可以對已經計算過的輸入做一個緩存。
所以這里可以利用閉包的特點來實現一個簡單的緩存,在函數內部用一個對象存儲輸入的參數,如果下次再輸入相同的參數,那就比較一下對象的屬性,如果有緩存,就直接把值從這個對象里面取出來。
/* 備忘函數 */ function memorize(fn) { var cache = {} return function() { var args = Array.prototype.slice.call(arguments) var key = JSON.stringify(args) return cache[key] || (cache[key] = fn.apply(fn, args)) } } /* 復雜計算函數 */ function add(a) { return a + 1 } var adder = memorize(add) adder(1) // 輸出: 2 當前: cache: { '[1]': 2 } adder(1) // 輸出: 2 當前: cache: { '[1]': 2 } adder(2) // 輸出: 3 當前: cache: { '[1]': 2, '[2]': 3 }
使用 ES6 的方式會更優雅一些:
/* 備忘函數 */ function memorize(fn) { const cache = {} return function(...args) { const key = JSON.stringify(args) return cache[key] || (cache[key] = fn.apply(fn, args)) } } /* 復雜計算函數 */ function add(a) { return a + 1 } const adder = memorize(add) adder(1) // 輸出: 2 當前: cache: { '[1]': 2 } adder(1) // 輸出: 2 當前: cache: { '[1]': 2 } adder(2) // 輸出: 3 當前: cache: { '[1]': 2, '[2]': 3 }
稍微解釋一下:
備忘函數中用 JSON.stringify 把傳給 adder 函數的參數序列化成字符串,把它當做 cache 的索引,將 add 函數運行的結果當做索引的值傳遞給 cache,這樣 adder 運行的時候如果傳遞的參數之前傳遞過,那么就返回緩存好的計算結果,不用再計算了,如果傳遞的參數沒計算過,則計算并緩存 fn.apply(fn, args),再返回計算的結果。
當然這里的實現如果要實際應用的話,還需要繼續改進一下,比如:
緩存不可以永遠擴張下去,這樣太耗費內存資源,我們可以只緩存最新傳入的 n 個;
在瀏覽器中使用的時候,我們可以借助瀏覽器的持久化手段,來進行緩存的持久化,比如 cookie、localStorage 等;
這里的復雜計算函數可以是過去的某個狀態,比如對某個目標的操作,這樣把過去的狀態緩存起來,方便地進行狀態回退。
復雜計算函數也可以是一個返回時間比較慢的異步操作,這樣如果把結果緩存起來,下次就可以直接從本地獲取,而不是重新進行異步請求。
注意: cache 不可以是 Map,因為 Map 的鍵是使用 === 比較的,因此當傳入引用類型值作為鍵時,雖然它們看上去是相等的,但實際并不是,比如 [1]!==[1],所以還是被存為不同的鍵。
// X 錯誤示范 function memorize(fn) { const cache = new Map() return function(...args) { return cache.get(args) || cache.set(args, fn.apply(fn, args)).get(args) } } function add(a) { return a + 1 } const adder = memorize(add) adder(1) // 2 cache: { [ 1 ] => 2 } adder(1) // 2 cache: { [ 1 ] => 2, [ 1 ] => 2 } adder(2) // 3 cache: { [ 1 ] => 2, [ 1 ] => 2, [ 2 ] => 3 }
2. 高階函數
高階函數就是輸入參數里有函數,或者輸出是函數的函數。
2.1 函數作為參數
如果你用過 setTimeout、setInterval、ajax 請求,那么你已經用過高階函數了,這是我們最常看到的場景:回調函數,因為它將函數作為參數傳遞給另一個函數。
比如 ajax 請求中,我們通常使用回調函數來定義請求成功或者失敗時的操作邏輯:
$.ajax("/request/url", function(result){ console.log("請求成功!") })
在 Array、Object、String 等等基本對象的原型上有很多操作方法,可以接受回調函數來方便地進行對象操作。這里舉一個很常用的 Array.prototype.filter() 方法,這個方法返回一個新創建的數組,包含所有回調函數執行后返回 true 或真值的數組元素。
var words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present']; var result = words.filter(function(word) { return word.length > 6 }) // 輸出: ["exuberant", "destruction", "present"]
回調函數還有一個應用就是鉤子,如果你用過 Vue 或者 React 等框架,那么你應該對鉤子很熟悉了,它的形式是這樣的:
function foo(callback) { // ... 一些操作 callback() }
2.2 函數作為返回值
另一個經常看到的高階函數的場景是在一個函數內部輸出另一個函數,比如:
function foo() { return function bar() {} }
主要是利用閉包來保持著作用域:
function add() { var num = 0 return function(a) { return num = num + a } } var adder = add() adder(1) // 輸出: 1 adder(2) // 輸出: 3
1. 柯里化
柯里化(Currying),又稱部分求值(Partial Evaluation),是把接受多個參數的原函數變換成接受一個單一參數(原函數的第一個參數)的函數,并且返回一個新函數,新函數能夠接受余下的參數,最后返回同原函數一樣的結果。
核心思想是把多參數傳入的函數拆成單(或部分)參數函數,內部再返回調用下一個單(或部分)參數函數,依次處理剩余的參數。
柯里化有3個常見作用:
參數復用
提前返回
延遲計算/運行
先來看看柯里化的通用實現:
// ES5 方式 function currying(fn) { var rest1 = Array.prototype.slice.call(arguments) rest1.shift() return function() { var rest2 = Array.prototype.slice.call(arguments) return fn.apply(null, rest1.concat(rest2)) } } // ES6 方式 function currying(fn, ...rest1) { return function(...rest2) { return fn.apply(null, rest1.concat(rest2)) } }
用它將一個 sayHello 函數柯里化試試:
// 接上面 function sayHello(name, age, fruit) { console.log(console.log(`我叫 ${name},我 ${age} 歲了, 我喜歡吃 ${fruit}`)) } var curryingShowMsg1 = currying(sayHello, '小明') curryingShowMsg1(22, '蘋果') // 輸出: 我叫 小明,我 22 歲了, 我喜歡吃 蘋果 var curryingShowMsg2 = currying(sayHello, '小衰', 20) curryingShowMsg2('西瓜') // 輸出: 我叫 小衰,我 20 歲了, 我喜歡吃 西瓜
更高階的用法參見:JavaScript 函數式編程技巧 - 柯里化
2. 反柯里化
柯里化是固定部分參數,返回一個接受剩余參數的函數,也稱為部分計算函數,目的是為了縮小適用范圍,創建一個針對性更強的函數。核心思想是把多參數傳入的函數拆成單參數(或部分)函數,內部再返回調用下一個單參數(或部分)函數,依次處理剩余的參數。
而反柯里化,從字面講,意義和用法跟函數柯里化相比正好相反,擴大適用范圍,創建一個應用范圍更廣的函數。使本來只有特定對象才適用的方法,擴展到更多的對象。
先來看看反柯里化的通用實現吧~
// ES5 方式 Function.prototype.unCurrying = function() { var self = this return function() { var rest = Array.prototype.slice.call(arguments) return Function.prototype.call.apply(self, rest) } } // ES6 方式 Function.prototype.unCurrying = function() { const self = this return function(...rest) { return Function.prototype.call.apply(self, rest) } }
如果你覺得把函數放在 Function 的原型上不太好,也可以這樣:
// ES5 方式 function unCurrying(fn) { return function (tar) { var rest = Array.prototype.slice.call(arguments) rest.shift() return fn.apply(tar, rest) } } // ES6 方式 function unCurrying(fn) { return function(tar, ...argu) { return fn.apply(tar, argu) } }
下面簡單試用一下反柯里化通用實現,我們將 Array 上的 push 方法借出來給 arguments 這樣的類數組增加一個元素:
// 接上面 var push = unCurrying(Array.prototype.push) function execPush() { push(arguments, 4) console.log(arguments) } execPush(1, 2, 3) // 輸出: [1, 2, 3, 4]
簡單說,函數柯里化就是對高階函數的降階處理,縮小適用范圍,創建一個針對性更強的函數。
function(arg1, arg2) // => function(arg1)(arg2) function(arg1, arg2, arg3) // => function(arg1)(arg2)(arg3) function(arg1, arg2, arg3, arg4) // => function(arg1)(arg2)(arg3)(arg4) function(arg1, arg2, ..., argn) // => function(arg1)(arg2)…(argn)
而反柯里化就是反過來,增加適用范圍,讓方法使用場景更大。使用反柯里化, 可以把原生方法借出來,讓任何對象擁有原生對象的方法。
obj.func(arg1, arg2) // => func(obj, arg1, arg2)
可以這樣理解柯里化和反柯里化的區別:
柯里化是在運算前提前傳參,可以傳遞多個參數;
反柯里化是延遲傳參,在運算時把原來已經固定的參數或者 this 上下文等當作參數延遲到未來傳遞。
更高階的用法參見:JavaScript 函數式編程技巧 - 反柯里化
3. 偏函數
偏函數是創建一個調用另外一個部分(參數或變量已預制的函數)的函數,函數可以根據傳入的參數來生成一個真正執行的函數。其本身不包括我們真正需要的邏輯代碼,只是根據傳入的參數返回其他的函數,返回的函數中才有真正的處理邏輯比如:
var isType = function(type) { return function(obj) { return Object.prototype.toString.call(obj) === `[object ${type}]` } } var isString = isType('String') var isFunction = isType('Function')
這樣就用偏函數快速創建了一組判斷對象類型的方法~
偏函數和柯里化的區別:
鴻蒙官方戰略合作共建——HarmonyOS技術社區
柯里化是把一個接受 n 個參數的函數,由原本的一次性傳遞所有參數并執行變成了可以分多次接受參數再執行,例如:add = (x, y, z) => x + y + z→curryAdd = x => y => z => x + y + z;
偏函數固定了函數的某個部分,通過傳入的參數或者方法返回一個新的函數來接受剩余的參數,數量可能是一個也可能是多個;
當一個柯里化函數只接受兩次參數時,比如 curry()(),這時的柯里化函數和偏函數概念類似,可以認為偏函數是柯里化函數的退化版。
看完上述內容,你們掌握JavaScript 閉包與高階函數是怎樣的的方法了嗎?如果還想學到更多技能或想了解更多相關內容,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。