第七色在线视频,2021少妇久久久久久久久久,亚洲欧洲精品成人久久av18,亚洲国产精品特色大片观看完整版,孙宇晨将参加特朗普的晚宴

為了賬號(hào)安全,請(qǐng)及時(shí)綁定郵箱和手機(jī)立即綁定
慕課專欄

目錄

索引目錄

JavaScript 設(shè)計(jì)模式精講

原價(jià) ¥ 48.00

立即訂閱
03 閉包與高階函數(shù)
更新時(shí)間:2019-09-24 11:24:00
我要扼住命運(yùn)的咽喉,它妄想使我屈服,這絕對(duì)辦不到。生活是這樣美好,活他一千輩子吧!——貝多芬

「JavaScript 中,函數(shù)是一等公民」,在各種書籍和文章中我們總能看到這句話。

既然有一等,那么當(dāng)然也有次等了。

如果公民分等級(jí),一等公民什么都可以做,次等公民這不能做那不能做。
JavaScript 的函數(shù)也是對(duì)象,可以有屬性,可以賦值給一個(gè)變量,可以放在數(shù)組里作為元素,可以作為其他對(duì)象的屬性,什么都可以做,別的對(duì)象能做的它能做,別的對(duì)象不能做的它也能做。這不就是一等公民的地位嘛。 — 程墨 Morgan

所以它的含義是:函數(shù)和其他普通對(duì)象一樣,其上有屬性也有方法,普通對(duì)象能做的,函數(shù)也可以做。

正因?yàn)樵?JavaScript 中的極大自由,函數(shù)被賦予了卓越的表達(dá)力和靈活性,但是也產(chǎn)生了很多讓人抓耳撓腮的問題。本文我們就一起討論一下最常遇見的兩個(gè)與函數(shù)密切相關(guān)的概念:閉包和高階函數(shù)。這兩個(gè)概念在之后設(shè)計(jì)模式的文章中也會(huì)經(jīng)常碰見。

注意: 本文屬于基礎(chǔ)篇,如果你已經(jīng)對(duì)本文相關(guān)知識(shí)點(diǎn)已經(jīng)很了解了,那么可以跳過本文。如果你不夠了解,或者了解的還不完整,那么可以通過本文來復(fù)習(xí)一下~

1. 閉包

1.1 什么是閉包

當(dāng)函數(shù)可以記住并訪問所在的詞法作用域時(shí),就產(chǎn)生了閉包,即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行。

我們首先來看一個(gè)閉包的例子:

function foo() {
    var a = 2
    
    function bar() {
        console.log(a)
    }
    
    return bar
}

var baz = foo()

baz()            // 輸出: 2

foo 函數(shù)傳遞出了一個(gè)函數(shù) bar,傳遞出來的 bar 被賦值給 baz 并調(diào)用,雖然這時(shí) baz 是在 foo 作用域外執(zhí)行的,但 baz 在調(diào)用的時(shí)候可以訪問到前面的 bar 函數(shù)所在的 foo 的內(nèi)部作用域。

由于 bar 聲明在 foo 函數(shù)內(nèi)部,bar 擁有涵蓋 foo 內(nèi)部作用域的閉包,使得 foo 的內(nèi)部作用域一直存活不被回收。一般來說,函數(shù)在執(zhí)行完后其整個(gè)內(nèi)部作用域都會(huì)被銷毀,因?yàn)?JavaScript 的 GC(Garbage Collection)垃圾回收機(jī)制會(huì)自動(dòng)回收不再使用的內(nèi)存空間。但是閉包會(huì)阻止某些 GC,比如本例中 foo() 執(zhí)行完,因?yàn)榉祷氐?bar 函數(shù)依然持有其所在作用域的引用,所以其內(nèi)部作用域不會(huì)被回收。

注意: 如果不是必須使用閉包,那么盡量避免創(chuàng)建它,因?yàn)殚]包在處理速度和內(nèi)存消耗方面對(duì)性能具有負(fù)面影響。

1.2 利用閉包實(shí)現(xiàn)結(jié)果緩存(備忘模式)

備忘模式就是應(yīng)用閉包的特點(diǎn)的一個(gè)典型應(yīng)用。比如有個(gè)函數(shù):

function add(a) {
    return a + 1;
}

多次運(yùn)行 add() 時(shí),每次得到的結(jié)果都是重新計(jì)算得到的,如果是開銷很大的計(jì)算操作的話就比較消耗性能了,這里可以對(duì)已經(jīng)計(jì)算過的輸入做一個(gè)緩存。

所以這里可以利用閉包的特點(diǎn)來實(shí)現(xiàn)一個(gè)簡(jiǎn)單的緩存,在函數(shù)內(nèi)部用一個(gè)對(duì)象存儲(chǔ)輸入的參數(shù),如果下次再輸入相同的參數(shù),那就比較一下對(duì)象的屬性,如果有緩存,就直接把值從這個(gè)對(duì)象里面取出來。

/* 備忘函數(shù) */
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))
    }
}

/* 復(fù)雜計(jì)算函數(shù) */
function add(a) {
    return a + 1
}

var adder = memorize(add)

adder(1)            // 輸出: 2    當(dāng)前: cache: { '[1]': 2 }
adder(1)            // 輸出: 2    當(dāng)前: cache: { '[1]': 2 }
adder(2)            // 輸出: 3    當(dāng)前: cache: { '[1]': 2, '[2]': 3 }

使用 ES6 的方式會(huì)更優(yōu)雅一些:

/* 備忘函數(shù) */
function memorize(fn) {
    const cache = {}
    return function(...args) {
        const key = JSON.stringify(args)
        return cache[key] || (cache[key] = fn.apply(fn, args))
    }
}

/* 復(fù)雜計(jì)算函數(shù) */
function add(a) {
    return a + 1
}

const adder = memorize(add)

adder(1)            // 輸出: 2    當(dāng)前: cache: { '[1]': 2 }
adder(1)            // 輸出: 2    當(dāng)前: cache: { '[1]': 2 }
adder(2)            // 輸出: 3    當(dāng)前: cache: { '[1]': 2, '[2]': 3 }

稍微解釋一下:

備忘函數(shù)中用 JSON.stringify 把傳給 adder 函數(shù)的參數(shù)序列化成字符串,把它當(dāng)做 cache 的索引,將 add 函數(shù)運(yùn)行的結(jié)果當(dāng)做索引的值傳遞給 cache,這樣 adder 運(yùn)行的時(shí)候如果傳遞的參數(shù)之前傳遞過,那么就返回緩存好的計(jì)算結(jié)果,不用再計(jì)算了,如果傳遞的參數(shù)沒計(jì)算過,則計(jì)算并緩存 fn.apply(fn, args),再返回計(jì)算的結(jié)果。

當(dāng)然這里的實(shí)現(xiàn)如果要實(shí)際應(yīng)用的話,還需要繼續(xù)改進(jìn)一下,比如:

  1. 緩存不可以永遠(yuǎn)擴(kuò)張下去,這樣太耗費(fèi)內(nèi)存資源,我們可以只緩存最新傳入的 n 個(gè);
  2. 在瀏覽器中使用的時(shí)候,我們可以借助瀏覽器的持久化手段,來進(jìn)行緩存的持久化,比如 cookie、localStorage 等;

這里的復(fù)雜計(jì)算函數(shù)可以是過去的某個(gè)狀態(tài),比如對(duì)某個(gè)目標(biāo)的操作,這樣把過去的狀態(tài)緩存起來,方便地進(jìn)行狀態(tài)回退。

復(fù)雜計(jì)算函數(shù)也可以是一個(gè)返回時(shí)間比較慢的異步操作,這樣如果把結(jié)果緩存起來,下次就可以直接從本地獲取,而不是重新進(jìn)行異步請(qǐng)求。

注意: cache 不可以是 Map,因?yàn)?Map 的鍵是使用 === 比較的,因此當(dāng)傳入引用類型值作為鍵時(shí),雖然它們看上去是相等的,但實(shí)際并不是,比如 [1]!==[1],所以還是被存為不同的鍵。

//  X 錯(cuò)誤示范
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. 高階函數(shù)

高階函數(shù)就是輸入?yún)?shù)里有函數(shù),或者輸出是函數(shù)的函數(shù)。

2.1 函數(shù)作為參數(shù)

如果你用過 setTimeout、setInterval、ajax 請(qǐng)求,那么你已經(jīng)用過高階函數(shù)了,這是我們最常看到的場(chǎng)景:回調(diào)函數(shù),因?yàn)樗鼘⒑瘮?shù)作為參數(shù)傳遞給另一個(gè)函數(shù)。

比如 ajax 請(qǐng)求中,我們通常使用回調(diào)函數(shù)來定義請(qǐng)求成功或者失敗時(shí)的操作邏輯:

$.ajax("/request/url", function(result){
    console.log("請(qǐng)求成功!")
})

在 Array、Object、String 等等基本對(duì)象的原型上有很多操作方法,可以接受回調(diào)函數(shù)來方便地進(jìn)行對(duì)象操作。這里舉一個(gè)很常用的 Array.prototype.filter() 方法,這個(gè)方法返回一個(gè)新創(chuàng)建的數(shù)組,包含所有回調(diào)函數(shù)執(zhí)行后返回 true 或真值的數(shù)組元素。

var words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present'];

var result = words.filter(function(word) {
    return word.length > 6
})       // 輸出: ["exuberant", "destruction", "present"]

回調(diào)函數(shù)還有一個(gè)應(yīng)用就是鉤子,如果你用過 Vue 或者 React 等框架,那么你應(yīng)該對(duì)鉤子很熟悉了,它的形式是這樣的:

function foo(callback) {
    // ... 一些操作
    callback()
}

2.2 函數(shù)作為返回值

另一個(gè)經(jīng)??吹降母唠A函數(shù)的場(chǎng)景是在一個(gè)函數(shù)內(nèi)部輸出另一個(gè)函數(shù),比如:

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),是把接受多個(gè)參數(shù)的原函數(shù)變換成接受一個(gè)單一參數(shù)(原函數(shù)的第一個(gè)參數(shù))的函數(shù),并且返回一個(gè)新函數(shù),新函數(shù)能夠接受余下的參數(shù),最后返回同原函數(shù)一樣的結(jié)果。

核心思想是把多參數(shù)傳入的函數(shù)拆成單(或部分)參數(shù)函數(shù),內(nèi)部再返回調(diào)用下一個(gè)單(或部分)參數(shù)函數(shù),依次處理剩余的參數(shù)。

柯里化有 3 個(gè)常見作用:

  1. 參數(shù)復(fù)用
  2. 提前返回
  3. 延遲計(jì)算 / 運(yùn)行

先來看看柯里化的通用實(shí)現(xiàn):

// 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))
  }
}

用它將一個(gè) sayHello 函數(shù)柯里化試試:

// 接上面
function sayHello(name, age, fruit) {
   console.log(`我叫 ${name},我 ${age} 歲了, 我喜歡吃 ${fruit}`)
}

var curryingShowMsg1 = currying(sayHello, '小明')
curryingShowMsg1(22, '蘋果')           // 輸出: 我叫 小明,我 22 歲了, 我喜歡吃 蘋果

var curryingShowMsg2 = currying(sayHello, '小衰', 20)
curryingShowMsg2('西瓜')               // 輸出: 我叫 小衰,我 20 歲了, 我喜歡吃 西瓜

2. 反柯里化

柯里化是固定部分參數(shù),返回一個(gè)接受剩余參數(shù)的函數(shù),也稱為部分計(jì)算函數(shù),目的是為了縮小適用范圍,創(chuàng)建一個(gè)針對(duì)性更強(qiáng)的函數(shù)。核心思想是把多參數(shù)傳入的函數(shù)拆成單參數(shù)(或部分)函數(shù),內(nèi)部再返回調(diào)用下一個(gè)單參數(shù)(或部分)函數(shù),依次處理剩余的參數(shù)。

反柯里化,從字面講,意義和用法跟函數(shù)柯里化相比正好相反,擴(kuò)大適用范圍,創(chuàng)建一個(gè)應(yīng)用范圍更廣的函數(shù)。使本來只有特定對(duì)象才適用的方法,擴(kuò)展到更多的對(duì)象。

先來看看反柯里化的通用實(shí)現(xiàn)吧~

// 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)
  }
}

如果你覺得把函數(shù)放在 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)
  }
}

下面簡(jiǎn)單試用一下反柯里化通用實(shí)現(xiàn),我們將 Array 上的 push 方法借出來給 arguments 這樣的類數(shù)組增加一個(gè)元素:

// 接上面
var push = unCurrying(Array.prototype.push)

function execPush() {
  push(arguments, 4)
  console.log(arguments)
}

execPush(1, 2, 3)    // 輸出: [1, 2, 3, 4]

簡(jiǎn)單說,函數(shù)柯里化就是對(duì)高階函數(shù)的降階處理,縮小適用范圍,創(chuàng)建一個(gè)針對(duì)性更強(qiáng)的函數(shù)。

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)

而反柯里化就是反過來,增加適用范圍,讓方法使用場(chǎng)景更大。使用反柯里化,可以把原生方法借出來,讓任何對(duì)象擁有原生對(duì)象的方法。

obj.func(arg1, arg2)        // => func(obj, arg1, arg2)

可以這樣理解柯里化和反柯里化的區(qū)別:

  1. 柯里化是在運(yùn)算前提前傳參,可以傳遞多個(gè)參數(shù);
  2. 反柯里化是延遲傳參,在運(yùn)算時(shí)把原來已經(jīng)固定的參數(shù)或者 this 上下文等當(dāng)作參數(shù)延遲到未來傳遞。

3. 偏函數(shù)

偏函數(shù)是創(chuàng)建一個(gè)調(diào)用另外一個(gè)部分(參數(shù)或變量已預(yù)制的函數(shù))的函數(shù),函數(shù)可以根據(jù)傳入的參數(shù)來生成一個(gè)真正執(zhí)行的函數(shù)。其本身不包括我們真正需要的邏輯代碼,只是根據(jù)傳入的參數(shù)返回其他的函數(shù),返回的函數(shù)中才有真正的處理邏輯比如:

var isType = function(type) {
  return function(obj) {
    return Object.prototype.toString.call(obj) === `[object ${type}]`
  }
}

var isString = isType('String')
var isFunction = isType('Function')

這樣就用偏函數(shù)快速創(chuàng)建了一組判斷對(duì)象類型的方法~

偏函數(shù)和柯里化的區(qū)別:

  1. 柯里化是把一個(gè)接受 n 個(gè)參數(shù)的函數(shù),由原本的一次性傳遞所有參數(shù)并執(zhí)行變成了可以分多次接受參數(shù)再執(zhí)行,例如:add = (x, y, z) => x + y + zcurryAdd = x => y => z => x + y + z
  2. 偏函數(shù)固定了函數(shù)的某個(gè)部分,通過傳入的參數(shù)或者方法返回一個(gè)新的函數(shù)來接受剩余的參數(shù),數(shù)量可能是一個(gè)也可能是多個(gè);

當(dāng)一個(gè)柯里化函數(shù)只接受兩次參數(shù)時(shí),比如 curry()(),這時(shí)的柯里化函數(shù)和偏函數(shù)概念類似,可以認(rèn)為偏函數(shù)是柯里化函數(shù)的退化版。

}
立即訂閱 ¥ 48.00

你正在閱讀課程試讀內(nèi)容,訂閱后解鎖課程全部?jī)?nèi)容

千學(xué)不如一看,千看不如一練

手機(jī)
閱讀

掃一掃 手機(jī)閱讀

JavaScript 設(shè)計(jì)模式精講
立即訂閱 ¥ 48.00

舉報(bào)

0/150
提交
取消