死磕JS:閉包到底是個(gè)什么鬼?
本文轉(zhuǎn)載自微信公眾號(hào)「前端思維框架」,作者ViktorHub。轉(zhuǎn)載本文請(qǐng)聯(lián)系前端思維框架公眾號(hào)。
在JavaScript這門語言中,閉包是它的核心基礎(chǔ)之一,可以說是一個(gè)特色了,但是很多從事前端工作的程序員并沒有真正的理解它!
閉包有多重要?如果你是初入前端的朋友,我可以肯定得告訴你,前端面試,必問閉包。面試官們常常用對(duì)閉包的了解程度來判定面試者的基礎(chǔ)水平,保守估計(jì),10個(gè)前端面試者,至少5個(gè)都死在閉包上。
通過本文講解,希望你可以重新認(rèn)識(shí)一下閉包!
函數(shù)調(diào)用時(shí)發(fā)生了什么?
為了理解閉包,首先我們需要完全理解 JavaScript 到底是如何工作的!
那么函數(shù)調(diào)用是會(huì)發(fā)生什么呢?
當(dāng)瀏覽器在解析 JS代碼的時(shí)候,會(huì)進(jìn)行一個(gè)預(yù)解析的操作,會(huì)有一個(gè)js解析器,里面會(huì)執(zhí)行其中的兩步操作:
1、預(yù)解析,找一些東西(var function 參數(shù));
2、逐行去解讀代碼。
當(dāng)解析器解讀函數(shù)調(diào)用時(shí),會(huì)將整個(gè)函數(shù)執(zhí)行一個(gè)入棧操作,并為函數(shù)創(chuàng)建一個(gè)新的執(zhí)行上下文。函數(shù)內(nèi)部可以看作是一個(gè)小的區(qū)域,它有它自己的作用域和執(zhí)行線程,也要逐行解讀。當(dāng)函數(shù)顯式返回(到達(dá)return語句)或隱式返回(默認(rèn)情況下函數(shù)返回undefined)時(shí),函數(shù)將出棧,其執(zhí)行上下文也將被銷毀。
閉包是什么鬼?
我們先來看下這段代碼:
- let name = "John"
- function greet() {
- const greeting = "Hi"
- function printHi() {
- console.log(greeting + ' ' + name)
- }
- printHi()
- }
- name = "Jane"
- greet() // "Hi Jane"
我們發(fā)現(xiàn),子函數(shù) printHi 可以訪問全局作用域和其父函數(shù) greet 的局部作用域。
注意,我們實(shí)際上可以訪問函數(shù)執(zhí)行期間可用的“新”數(shù)據(jù),而不是聲明。這就是詞法作用域在 JavaScript 中的工作方式。
但是如果我們返回一個(gè)函數(shù),而不是僅僅在外部函數(shù)體中調(diào)用它,會(huì)發(fā)生什么呢?
看好了,奇跡出現(xiàn)了!
從一個(gè)函數(shù)中返回的函數(shù)不僅僅是一個(gè)簡(jiǎn)單的函數(shù)定義,它是這個(gè)定義加上它可以訪問并需要執(zhí)行的變量,這些變量存儲(chǔ)在它附帶的詞法作用域中。
我們剛剛描述的就是閉包。從形式上講,閉包是一個(gè)「即使在詞法范圍之外調(diào)用,仍可以記住它的詞法范圍」的函數(shù)。
- function creator(num) {
- return function() {
- num = num * 2
- console.log(num)
- }
- }
- const double = creator(5)
- double() //10
- double() //20
- const double2 = creator(7)
- double2() // 14
- double2() //28
- double() // 40
正如我們?cè)谏厦娴拇a片段中看到的,每當(dāng)我們調(diào)用 double 時(shí),它都會(huì)更新存儲(chǔ)在其詞法作用域中的同一個(gè)變量(來自其父函數(shù)的num),從技術(shù)上講,這是函數(shù)所具有的隱藏 [[scope]] 屬性。
如果你想知道閉包到底有什么用,請(qǐng)繼續(xù)看下面的示例。
模塊封裝
閉包允許我們保護(hù)或隱藏某些信息。[[scope]] 是一個(gè)隱藏的屬性,所以我們不能像使用標(biāo)準(zhǔn)對(duì)象那樣訪問和更新它。還有一點(diǎn)很重要,我們可以返回一組存儲(chǔ)在對(duì)象上的函數(shù),它們都是閉包。
在下面的代碼片段中,我們利用了所謂的IIFE(立即執(zhí)行函數(shù)),它允許我們消除調(diào)用外部函數(shù)的中間步驟,就像我們?cè)谫x值時(shí)直接調(diào)用它一樣。
- const myModule = (function(){
- const apiKey = "123456789"
- return {
- displayKey() {
- console.log(apiKey)
- }
- }
- })()
- myModule.displayKey() // "123456789"
如果我們將這個(gè)模塊 export 出去, 提供給其他人使用,我們?yōu)樗麥?zhǔn)備的 API 不允許他更改 apiKey,這就做到了只讀屬性,除了在源代碼中重寫它之外,調(diào)用方不可能更改它。
緩存和記憶化
假設(shè)您想創(chuàng)建一個(gè)簡(jiǎn)單的ID生成器。為了確??偸欠祷乇壬弦粋€(gè)高的數(shù)字,也可以使用閉包。我們將緩存當(dāng)前變量中最高的 ID 值。
- const newID = (function() {
- let current = 0
- return function() {
- return ++current
- }
- })()
- newID() // 1
- newID() // 2
當(dāng)我們的算法時(shí)間復(fù)雜度很高時(shí),這種緩存方式就非常有用,我們可以將部分結(jié)果存儲(chǔ)在緩存中,當(dāng)我們使用更高的數(shù)字進(jìn)行計(jì)算時(shí),我們可以使用緩存中的數(shù)據(jù)作為基礎(chǔ)。這個(gè)過程叫做記憶化。一個(gè)最好的例子就是處理處理遞歸問題,比如斐波那契序列。
- const factorialMemo = (function() {
- const cache = {}
- return function factorial(n) {
- if(n === 1 || n === 0) {
- return 1
- } else if (cache[n]) {
- return cache[n]
- } else {
- cache[n] = n * factorial(n-1)
- return cache[n]
- }
- }
- })()
- factorialMemo(5) //120
- // cache object looks like {'2': 2, '3' : 6, '4' : 24, '5' : 120}
- factorialMemo(6) // 6 * cached 120
好了,今天的內(nèi)容到此就結(jié)束了,你有 get 到閉包到底是個(gè)什么鬼了嗎?當(dāng)然要熟練掌握,還需要你在不斷的練習(xí)與總結(jié)中自我體會(huì)!
在《面向?qū)ο蠓治雠c設(shè)計(jì)》這本書里有一句話對(duì)閉包的概括我很喜歡,他是這樣說的:
“閉包是懶人的對(duì)象,對(duì)象是天然的閉包!”
慢慢悟吧,歡迎評(píng)論交流,周末愉快~