前言
博主進入前端領(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ù)返回值的確定
前面說到在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ù)中this的指向是最復(fù)雜的卤档,面試的時候考的也多是費箭頭函數(shù)中的this。在非箭頭函數(shù)中程剥,this和
-
在箭頭函數(shù)中
- 在箭頭函數(shù)中是沒有this的瀑踢,它直接繼承定義它時候所處的外部對象扳还,并且
call/apply/bind
均不能改變改變其this的指向(猜測因為不是內(nèi)置參數(shù)的原因)才避,例如下面的例子:
image.png
- 在箭頭函數(shù)中是沒有this的瀑踢,它直接繼承定義它時候所處的外部對象扳还,并且
一道巨坑的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桑逝。
為什么呢?
- 首先我們考慮第一次執(zhí)行fn時候,也就是method中
fn()
的時候俏让,很明顯fn中的this是指向window的楞遏,然而let length = 10
因為使用的是let,所以并不會將length變量的值掛到window.length
上去舆驶,所以fn()
時候輸出的是window.length
的值 -
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.memo
和useCallback
奈应,通過這兩個優(yōu)化再對記憶化函數(shù)進行理解。
-
react中的優(yōu)化
-
react.memo
-
問題描述1
試看下面的場景:
image.png
該段代碼中净蚤,App父組件包裹了Child子組件钥组,App組件中有一個狀態(tài)num
, 當(dāng)使用setNum
對num
的值進行改變的時候今瀑,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é)果:
函數(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
該題中屿岂,最重要的是有兩點:
- 找到一個容器對每次接收的參數(shù)進行存儲
- 每次接收新的參數(shù)都需要對比接收到的參數(shù)的與傳入的fn的形參是否相同
答案: