Web前端:征服 JavaScript 面試之什么是閉包?
前言
在我面試時(shí)問出的一系列問題里,閉包通常是我問的***個(gè)或***一個(gè)問題。坦白地說,如果你連閉包也弄不明白,你是不會(huì)在 JavaScript 的道路上走多遠(yuǎn)的。
你別東張西望,說的就是你。你真的理解如何構(gòu)建一個(gè)嚴(yán)謹(jǐn)?shù)?JavaScript 應(yīng)用?你真的理解代碼背后發(fā)生的事情或者說一個(gè)應(yīng)用程序是如何工作的?我表示懷疑。如果連個(gè)閉包問題都搞不清的話,真是有點(diǎn)夠嗆。
你不僅僅應(yīng)該了解閉包的機(jī)制,更應(yīng)該了解閉包為什么很重要,以及能夠很容易地回答出閉包的幾種可能的應(yīng)用場景。
閉包在 JavaScript 中常用來實(shí)現(xiàn)對象數(shù)據(jù)的私有,在事件處理和回調(diào)函數(shù)中也常常會(huì)用到它,此外還有偏函數(shù)應(yīng)用(partial applications)和柯里化(currying),以及其他函數(shù)式編程模式。
我不在乎面試者是否知道“closure”這個(gè)單詞或者它的專業(yè)定義。我只想弄清他們是否理解基本原理。如果他們沒有,那么通常意味著這些面試者在構(gòu)建實(shí)際 JavaScript 應(yīng)用方面并沒有很多經(jīng)驗(yàn)。
如果你不能回答這個(gè)問題,你只是個(gè)初級(jí)開發(fā)者。不管你實(shí)際上已經(jīng)干這個(gè)多久了。
為了快速理解下面的內(nèi)容:你想一下能否舉出兩個(gè)閉包的通用場景?
什么是閉包?
簡言之,閉包是由函數(shù)引用其周邊狀態(tài)(詞法環(huán)境)綁在一起形成的(封裝)組合結(jié)構(gòu)。在 JavaScript 中,閉包在每個(gè)函數(shù)被創(chuàng)建時(shí)形成。
這是基本原理,但為什么我們關(guān)心這些?實(shí)際上,由于閉包與它的詞法環(huán)境綁在一起,因此閉包讓我們能夠從一個(gè)函數(shù)內(nèi)部訪問其外部函數(shù)的作用域。
要使用閉包,只需要簡單地將一個(gè)函數(shù)定義在另一個(gè)函數(shù)內(nèi)部,并將它暴露出來。要暴露一個(gè)函數(shù),可以將它返回或者傳給其他函數(shù)。
內(nèi)部函數(shù)將能夠訪問到外部函數(shù)作用域中的變量,即使外部函數(shù)已經(jīng)執(zhí)行完畢。
閉包使用的例子
閉包的用途之一是實(shí)現(xiàn)對象的私有數(shù)據(jù)。數(shù)據(jù)私有是讓我們能夠面向接口編程而不是面向?qū)崿F(xiàn)編程的基礎(chǔ)。而面向接口編程是一個(gè)重要的概念,有助于我們創(chuàng)建更加健壯的軟件,因?yàn)閷?shí)現(xiàn)細(xì)節(jié)比接口約定相對來說更加容易被改變。
“面向接口編程,別面向?qū)崿F(xiàn)編程。” 設(shè)計(jì)模式:可復(fù)用面向?qū)ο筌浖囊?/p>
在 JavaScript 中,閉包是用來實(shí)現(xiàn)數(shù)據(jù)私有的原生機(jī)制。當(dāng)你使用閉包來實(shí)現(xiàn)數(shù)據(jù)私有時(shí),被封裝的變量只能在閉包容器函數(shù)作用域中使用。你無法繞過對象被授權(quán)的方法在外部訪問這些數(shù)據(jù)。在 JavaScript 中,任何定義在閉包作用域下的公開方法才可以訪問這些數(shù)據(jù)。例如:
- const getSecret = (secret) => {
- return {
- get: () => secret
- };
- };
- test('Closure for object privacy.', assert => {
- const msg = '.get() should have access to the closure.';
- const expected = 1;
- const obj = getSecret(1);
- const actual = obj.get();
- try {
- assert.ok(secret, 'This throws an error.');
- } catch (e) {
- assert.ok(true, `The secret var is only available
- to privileged methods.`);
- }
- assert.equal(actual, expected, msg);
- assert.end();
- });
在上面的例子里,get() 方法定義在 getSecret() 作用域下,這讓它可以訪問任何 getSecret()中的變量,于是它就是一個(gè)被授權(quán)的方法。在這個(gè)例子里,它可以訪問參數(shù) secret。
對象不是唯一的產(chǎn)生私有數(shù)據(jù)的方式。閉包還可以被用來創(chuàng)建有狀態(tài)的函數(shù),這些函數(shù)的執(zhí)行過程可能由它們自身的內(nèi)部狀態(tài)所決定。例如:
- const secret = (msg) => () => msg;
- // Secret - creates closures with secret messages.
- // https://gist.github.com/ericelliott/f6a87bc41de31562d0f9
- // https://jsbin.com/hitusu/edit?html,js,output
- // secret(msg: String) => getSecret() => msg: String
- const secret = (msg) => () => msg;
- test('secret', assert => {
- const msg = 'secret() should return a function that returns the passed secret.';
- const theSecret = 'Closures are easy.';
- const mySecret = secret(theSecret);
- const actual = mySecret();
- const expected = theSecret;
- assert.equal(actual, expected, msg);
- assert.end();
- });
在函數(shù)式編程中,閉包經(jīng)常用于偏函數(shù)應(yīng)用和柯里化。為了說明這個(gè),我們先定義一些概念:
函數(shù)應(yīng)用:一個(gè)過程,指將參數(shù)傳給一個(gè)函數(shù),并獲得它的返回值。
偏函數(shù)應(yīng)用:一個(gè)過程,它傳給某個(gè)函數(shù)其中一部分參數(shù),然后返回一個(gè)新的函數(shù),該函數(shù)等待接受后續(xù)參數(shù)。換句話說,偏函數(shù)應(yīng)用是一個(gè)函數(shù),它接受另一個(gè)函數(shù)為參數(shù),這個(gè)作為參數(shù)的函數(shù)本身接受多個(gè)參數(shù),它返回一個(gè)函數(shù),這個(gè)函數(shù)與它的參數(shù)函數(shù)相比,接受更少的參數(shù)。偏函數(shù)應(yīng)用提前賦予一部分參數(shù),而返回的函數(shù)則等待調(diào)用時(shí)傳入剩余的參數(shù)。
偏函數(shù)應(yīng)用通過閉包作用域來提前賦予參數(shù)。你可以實(shí)現(xiàn)一個(gè)通用的函數(shù)來賦予指定的函數(shù)部分參數(shù),它看起來如下:
- partialApply(targetFunction: Function, ...fixedArgs: Any[]) =>
- functionWithFewerParams(...remainingArgs: Any[])
如果你要更進(jìn)一步理解上面的形式,你可以看這里。
partialApply 接受一個(gè)多參數(shù)的函數(shù),以及一串我們想要提前賦給這個(gè)函數(shù)的參數(shù),它返回一個(gè)新的函數(shù),這個(gè)函數(shù)將接受剩余的參數(shù)。
下面給一個(gè)例子來說明,假設(shè)你有一個(gè)函數(shù),求兩個(gè)數(shù)的和:
- const add = (a, b) => a + b;
現(xiàn)在你想要得到一個(gè)函數(shù),它能夠?qū)θ魏蝹鹘o它的參數(shù)都加 10,我們可以將它命名為 add10()。add10(5) 的結(jié)果應(yīng)該是 15。我們的 partialApply() 函數(shù)可以做到這個(gè):
- const add10 = partialApply(add, 10);
- add10(5);
在這個(gè)例子里,參數(shù) 10 通過閉包作用域被提前賦予 add(),從而讓我們獲得 add10()。
現(xiàn)在讓我們看一下如何實(shí)現(xiàn) partialApply():
- // Generic Partial Application Function
- // https://jsbin.com/biyupu/edit?html,js,output
- // https://gist.github.com/ericelliott/f0a8fd662111ea2f569e
- // partialApply(targetFunction: Function, ...fixedArgs: Any[]) =>
- // functionWithFewerParams(...remainingArgs: Any[])
- const partialApply = (fn, ...fixedArgs) => {
- return function (...remainingArgs) {
- return fn.apply(this, fixedArgs.concat(remainingArgs));
- };
- };
- test('add10', assert => {
- const msg = 'partialApply() should partially apply functions'
- const add = (a, b) => a + b;
- const add10 = partialApply(add, 10);
- const actual = add10(5);
- const expected = 15;
- assert.equal(actual, expected, msg);
- });
如你所見,它只是簡單地返回一個(gè)函數(shù),這個(gè)函數(shù)通過閉包訪問了傳給 partialApply() 函數(shù)的 fixedArgs 參數(shù)。
輪到你來試試了
你用閉包來做什么?如果你有最喜歡的應(yīng)用場景,舉一些例子,在評論中告訴我。