再談js中的函數(shù)

前言

博主進入前端領(lǐng)域工作到現(xiàn)在也已經(jīng)有兩年的時間了砸狞,回看了兩年多前剛開始學(xué)習(xí)js這門語言的時候?qū)懙?a href="http://www.reibang.com/p/460cd01cc555" target="_blank">關(guān)于函數(shù)的文章
捻勉,發(fā)現(xiàn)又有了新的理解,但在此博客中不對函數(shù)的創(chuàng)建和聲明提升等概念做更多的理解刀森,只說一些新的理解,概念性可能較強报账,也會有一些面試題以及專門針對react的優(yōu)化方式研底。

js中的函數(shù)

在之前,我看過很多種對函數(shù)的稱呼透罢,除了函數(shù)本身外榜晦,還有方法、過程等羽圃,但是他們之間是有一定區(qū)別的乾胶。

  • 關(guān)于子程序
    子程序是指那些由一個或者多個語句組成的用來處理特定任務(wù)的程序,相對獨立朽寞,聽起來很像是函數(shù)吧识窿,但是函數(shù)只是子程序的一種類型。
  • 子程序的分類
    子程序一般分為三種脑融,函數(shù)是其中一種喻频,剩下兩種分別是過程方法,那么他們有個字有什么區(qū)別呢?
  • 函數(shù)肘迎、過程和方法
    • 函數(shù): 最顯著的特點就是具有返回值甥温,那么可能會有人說了,我在js中寫一個函數(shù)我不return不就沒有返回值了嗎妓布,但實際上你不return姻蚓,js會自動幫你return undefined,也就是說在js中函數(shù)必定有返回值
    • 過程: 過程實際上就是沒有返回值的函數(shù)匣沼,又因為在js中不存在沒有返回值的函數(shù)狰挡,因此js中不存在過程
    • 方法: 與函數(shù)、過程的區(qū)別就是肛著,方法一般存在于類和對象中(可能也是因為這樣圆兵,Java朋友總是和我杠說我把方法讀成函數(shù)了)
      image.png

函數(shù)返回值的確定

前面說到在js中,函數(shù)永遠都有返回值枢贿,那么函數(shù)的返回值又是由什么去確定的呢?

  • 返回值的確定
    • 函數(shù)的返回值是由調(diào)用時輸入的參數(shù)定義時的環(huán)境確定的殉农,調(diào)用時輸入的參數(shù)影響結(jié)果大家都懂就不討論了,只討論函數(shù)定義時候的環(huán)境局荚,比如下面這個面試題

    • 面試題目:

      image.png

      答案是x1超凳,可能有人會說答案是'x2'愈污,因為f1調(diào)用時候內(nèi)部沒有變量a,應(yīng)該取外部的a轮傍,那么外部的a最近的就是let a = '2'了暂雹。
      取外部的a確實沒有錯,但是因為第一點已經(jīng)說了创夜,函數(shù)的返回值也由定義時的環(huán)境決定杭跪,所以取值的時候取的是1,而不是2驰吓。

關(guān)于閉包

閉包相關(guān)的題目是出現(xiàn)頻率巨高的面試題涧尿,也是老生常談的問題了。在這里就說說閉包的定義和特點吧檬贰。

  • 閉包的定義

    • 如果在函數(shù)的里面可以訪問到外面的變量姑廉,那么這個函數(shù) + 這些變量 = 閉包
    • 閉包問題本質(zhì)上是作用域問題
  • 閉包特點

    • 通過上面閉包的定義可以知道,閉包可以維持住一個變量翁涤,因此桥言,可以使得外部對函數(shù)內(nèi)部的變量進行訪問,比如下面這個例子:


      image.png
  • 優(yōu)缺點

    • 優(yōu)點: 可以在外部對函數(shù)內(nèi)部的變量進行訪問
    • 缺點: 返回出來的變量葵礼,例如上面例子中的{ a: 1 }号阿,是被保留在內(nèi)存中的,如果不及時清空的話會造成內(nèi)存泄漏和性能問題章咧。

關(guān)于this

this問題在js中也是老生常談的問題的了倦西,討論的最多的就是它的指向問題,在這里分為三種情況進行討論(嚴格模式下的this不考慮)赁严。

  • 在非函數(shù)體中扰柠,this指向全局對象(瀏覽器為window對象,node中為global對象)疼约。

  • 在非箭頭函數(shù)中

    • 在非箭頭函數(shù)中this的指向是最復(fù)雜的卤档,面試的時候考的也多是費箭頭函數(shù)中的this。在非箭頭函數(shù)中程剥,this和arguments一樣是函數(shù)的一個內(nèi)置參數(shù)劝枣,this指向一般為調(diào)用時候的外層環(huán)境,比如下面的例子:
      image.png

      又因為函數(shù)調(diào)用時候都會默認使用call的形式進行調(diào)用织鲸,所以上面的調(diào)用形式又可以改寫成下面的代碼舔腾,這樣非常簡單就能知道this究竟指向何物了:
      image.png
    • 但是上面的情況只是適合大部分情況下,有些情況還是不適合的搂擦,比如下面幾種情況:
      • 在構(gòu)造函數(shù)中稳诚,this會被強制綁定到構(gòu)造出來的新對象中
      • 在使用addEventListener作為事件監(jiān)聽器的情況下,處理dom事件的函數(shù)中的this會指向該dom元素
      • setTimeout/setInterval中的this默認指向window
  • 在箭頭函數(shù)中

    • 在箭頭函數(shù)中是沒有this的瀑踢,它直接繼承定義它時候所處的外部對象扳还,并且call/apply/bind均不能改變改變其this的指向(猜測因為不是內(nèi)置參數(shù)的原因)才避,例如下面的例子:
      image.png
  • 一道巨坑的this面試題

let length = 10

function fn() {
    console.log(this.length)
}

let obj = {
    length: 5,
    method(fn) {
        fn()
        arguments[0]()
    }
}

obj.method(fn, 1) // 請問輸出什么

可能有人會說答案是10和10,然而這時錯的氨距,正確的答案是window.length(當(dāng)前頁面的iframe數(shù)量)和2桑逝。
為什么呢?

  1. 首先我們考慮第一次執(zhí)行fn時候,也就是method中fn()的時候俏让,很明顯fn中的this是指向window的楞遏,然而let length = 10因為使用的是let,所以并不會將length變量的值掛到window.length上去舆驶,所以fn()時候輸出的是window.length的值
  2. arguments[0]()執(zhí)行的時候橱健,arguments是method的arguments,那么arguments[0]也就是執(zhí)行fn沙廉,但是這么時候需要注意,arguments是一個對象臼节,arguments[0]()相當(dāng)于arguments[0].call(arguments)撬陵,又因為obj.method(fn, 1)輸入了兩個參數(shù),所以這里輸出的是2

遞歸

說到函數(shù)就不能不說遞歸了网缝,遞歸是指函數(shù)自身調(diào)用自身巨税,它的使用范圍很廣,面試題出的也多粉臊,比如最常見的求階乘斐波那契的第n位數(shù):

  • 求階乘n位數(shù): j = n => n === 1 ? 1 : n * j(n - 1)

  • 求斐波那契n位數(shù): f = n => n === 0 ? 0 : n === 1 ? 1 : f(n - 1) + f(n - 2)

  • 遞歸的優(yōu)缺點
    從上面求階乘和斐波那契n位數(shù)的解法可以看出: 遞歸可以使函數(shù)變得更加簡潔草添,但同時也導(dǎo)致理解上更加困難,與此同時扼仲,更麻煩的是遞歸對于性能影響非常大远寸,甚至導(dǎo)致爆棧,原因在于它會不斷地將已經(jīng)求得的結(jié)果再求一次屠凶,也就是說會進行大量地重復(fù)堆棧行為驰后,例如在上述階乘解法中,如果求的是第3位數(shù)矗愧,首先求得第一位數(shù)灶芝,在求第二位數(shù)的時候,又會再一次求第一位數(shù)唉韭,到了求第3位數(shù)的時候夜涕,又會重復(fù)求第一和第二位數(shù),結(jié)果就導(dǎo)致重復(fù)的求值行為属愤,如下圖:


    image.png
  • 遞歸的優(yōu)化
    在js中女器,所有的遞歸都可以改成循環(huán)的形式,例如上述的階乘就可以該成為以下形式:

const j = n => {
    for(let i = n - 1; i >= 1; i--) {
        n = n * i
    }
    console.log(n)
}

j(3)  // 6

斐波那契則可以改寫成如下:

const f = n => {
    let arr = [0, 1]
    for(let i = 0; i <= n - 2; i++) {
        arr[i + 2] = arr[i + 1] + arr[i]
    }
    console.log(arr[arr.length - 1])
}

除此之外春塌,還可以將遞歸改寫成尾遞歸的形式進行優(yōu)化晓避,但在此不再贅述簇捍。

調(diào)用棧

在上面討論遞歸的時候有提到了調(diào)用棧這個東西,那么調(diào)用棧究竟是什么呢?

  • 首先什么是棧
    棧是一種線性的數(shù)據(jù)結(jié)構(gòu)俏拱,其特點是先進后出暑塑,可以將其想象成一種容器,入棧是指將數(shù)據(jù)元素放入棧中锅必,出棧是指從棧的頂部取出數(shù)據(jù)元素

  • 什么是調(diào)用棧以及js中的調(diào)用棧
    調(diào)用棧是指解釋器追蹤函數(shù)執(zhí)行流的一種機制事格,通過這種機制,當(dāng)執(zhí)行環(huán)境中調(diào)用了多個函數(shù)時搞隐,我們能夠追蹤到哪個函數(shù)正在執(zhí)行驹愚,執(zhí)行的函數(shù)體中又調(diào)用了哪個函數(shù)。
    在js中劣纲,由于其本身是單線程的逢捺,一次只能執(zhí)行一件事情,所以js中的調(diào)用棧只有一個癞季。結(jié)合前面棧的特點劫瞳,可以對如下代碼進行解釋:


    image.png

    首先解釋器將f1入棧并執(zhí)行,其中f1執(zhí)行的時候又執(zhí)行了f2绷柒,然后解釋器又把f2入棧并執(zhí)行志于,執(zhí)行完f2后,f2從調(diào)用棧中刪去(出棧)废睦,然后f1出棧伺绽。

從對react的優(yōu)化中看記憶化函數(shù)

該部分會涉及到react相關(guān)的知識,默認讀者已具備相應(yīng)的知識嗜湃。首先將會介紹在react中的兩種減少組件計算的優(yōu)化方式react.memouseCallback奈应,通過這兩個優(yōu)化再對記憶化函數(shù)進行理解。

  • react中的優(yōu)化

    • react.memo

      • 問題描述1
        試看下面的場景:

        image.png

        該段代碼中净蚤,App父組件包裹了Child子組件钥组,App組件中有一個狀態(tài)num, 當(dāng)使用setNumnum的值進行改變的時候今瀑,App組件會重新執(zhí)行程梦,但是這個時候你會發(fā)現(xiàn)Child雖然沒有用到num狀態(tài)慎玖,但是也被重新執(zhí)行了拼缝,這就造成了重復(fù)計算:
        image.png

      • 優(yōu)化方案
        這個問題我們可以使用react.memo來解決,這個api和class組件下的pureComponent功能類似壤玫,只需要用react.memo對Child組件進行包裹即可:

        image.png

        然后你就會神奇地發(fā)現(xiàn)哥童,當(dāng)我點擊按鈕增加num的時候挺份,那些重復(fù)執(zhí)行的步驟消失了:
        image.png

    • useCallback

      • 問題描述2
        基于問題1,我們將代碼改成如下:

        image.png

        這時候你點擊value的按鈕贮懈,會發(fā)現(xiàn)雖然Child組件雖然和value并沒有什么關(guān)系匀泊,但是卻導(dǎo)致了重新執(zhí)行优训,之前react.memo已經(jīng)解決的問題又出現(xiàn)了:
        image.png

        這是因為在更新App組件的時候,又重新聲明了print函數(shù)各聘,使得print函數(shù)的引用發(fā)生了改變揣非,也就是說傳入Child組件的函數(shù)發(fā)生了改變導(dǎo)致Child組件重新執(zhí)行,但是實際上print與value并沒有關(guān)系躲因,這么問題在class組件里很好解決早敬,但是在函數(shù)式組件里如何解決呢?

      • 優(yōu)化方案
        這個時候是否可以創(chuàng)建一種,在App內(nèi)部大脉,只有當(dāng)num的值發(fā)生變化時候才更新print函數(shù)的方案呢? 答案是使用useCallback搞监,我們將print函數(shù)用useCallback進行包裹,變成如下:

        image.png

        這里傳入的第二個參數(shù)就是當(dāng)該參數(shù)中的值發(fā)生變化時候镰矿,才返回一個新的函數(shù)琐驴。
        優(yōu)化結(jié)果,點擊value按鈕不再刷新Child組件了:
        image.png

        只有點擊num按鈕才會刷新:
        image.png

  • 記憶化函數(shù)
    從上面對react組件進行的優(yōu)化方式來看秤标,他們都使用了記憶化函數(shù)棍矛,也就是當(dāng)前輸入的參數(shù)如果之前已經(jīng)求過結(jié)果,那么便不再重新執(zhí)行抛杨,而是直接輸出之前的結(jié)果。那么應(yīng)該如何來實現(xiàn)這么一個函數(shù)呢?
    我們可以通過一道面試題得出一些結(jié)論:

const memo = (fn) => {
  請補全
}

const x2 = memo((x) => {
    console.log('執(zhí)行了一次')
    return x * 2
  })
  // 第一次調(diào)用 x2(1)
console.log(x2(1)) // 打印出執(zhí)行了荐类,并且返回2
  // 第二次調(diào)用 x2(1)
console.log(x2(1)) // 不打印執(zhí)行怖现,并且返回上次的結(jié)果2
  // 第三次調(diào)用 x2(1)
console.log(x2(1)) // 不打印執(zhí)行,并且返回上次的結(jié)果2

要實現(xiàn)記憶化函數(shù)實際上并不難玉罐,只需要找到一個容器對之前已經(jīng)計算過的結(jié)果進行存儲屈嗤,當(dāng)輸入的參數(shù)和之前的參數(shù)一樣時候,就直接從容器中取出結(jié)果吊输,如果不是就執(zhí)行函數(shù)饶号,并對結(jié)果進行存儲:

const memo = (fn) => {
    const memoed = key => {
        // 如果參數(shù)和之前不同則進行結(jié)果緩存
        if(!(key in memoed.cache)) {
            memoed.cache[key] = fn(key)
        }
        // 否則直接輸出結(jié)果
        return memoed.cache[key]
    }
    memoed.cache = {}
    return memoed
}

執(zhí)行結(jié)果:


image.png

函數(shù)柯里化(Currying)

函數(shù)柯里化也是現(xiàn)在前端面試的常客了季蚂,雖然我一直覺得嵌套太多層會導(dǎo)致代碼難看

  • 什么是柯里化

柯里化是編譯原理層面實現(xiàn)多參函數(shù)的一個技術(shù)

  • js中的函數(shù)柯里化和使用實例
    在js中茫船,柯里化技術(shù)可以將一個接收多個參數(shù)的函數(shù)改成每次只接收一個參數(shù), 這也是比較常見的扭屁,他可以使函數(shù)延遲執(zhí)行算谈,最常見的就是bind函數(shù),它可以將一個函數(shù)綁定this后料滥,再返回這個函數(shù)然眼,使得這個函數(shù)的this固定化(對箭頭函數(shù)無效),來看看一個簡易bind函數(shù)的實現(xiàn)吧:
function bind(context, ...args) {
    return (...rest) => this.call(context, ...args, ...rest);
}

另外葵腹,也可以利用柯里化函數(shù)延遲執(zhí)行的特性對代碼進行優(yōu)化高每,例子可以直接點這里查看

  • js柯里化面試題


    image.png

    該題中屿岂,最重要的是有兩點:

    1. 找到一個容器對每次接收的參數(shù)進行存儲
    2. 每次接收新的參數(shù)都需要對比接收到的參數(shù)的與傳入的fn的形參是否相同

答案:


image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市鲸匿,隨后出現(xiàn)的幾起案子爷怀,更是在濱河造成了極大的恐慌,老刑警劉巖晒骇,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件霉撵,死亡現(xiàn)場離奇詭異,居然都是意外死亡洪囤,警方通過查閱死者的電腦和手機徒坡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瘤缩,“玉大人喇完,你說我怎么就攤上這事“。” “怎么了锦溪?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長府怯。 經(jīng)常有香客問我刻诊,道長,這世上最難降的妖魔是什么牺丙? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任则涯,我火速辦了婚禮,結(jié)果婚禮上冲簿,老公的妹妹穿的比我還像新娘粟判。我一直安慰自己,他們只是感情好峦剔,可當(dāng)我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布档礁。 她就那樣靜靜地躺著,像睡著了一般吝沫。 火紅的嫁衣襯著肌膚如雪呻澜。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天野舶,我揣著相機與錄音易迹,去河邊找鬼。 笑死平道,一個胖子當(dāng)著我的面吹牛睹欲,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼窘疮,長吁一口氣:“原來是場噩夢啊……” “哼袋哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起闸衫,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤涛贯,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蔚出,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體弟翘,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年骄酗,在試婚紗的時候發(fā)現(xiàn)自己被綠了稀余。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡趋翻,死狀恐怖睛琳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情踏烙,我是刑警寧澤师骗,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站讨惩,受9級特大地震影響辟癌,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜荐捻,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一愿待、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧靴患,春花似錦、人聲如沸要出。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽患蹂。三九已至或颊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間传于,已是汗流浹背囱挑。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留沼溜,地道東北人平挑。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親通熄。 傳聞我的和親對象是個殘疾皇子唆涝,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容