摘要:當(dāng)面試中讓我解釋一下閉包時(shí)我懵逼了。這個(gè)解釋開始可能有點(diǎn)晦澀,讓我們抽絲剝繭摘下閉包的真面目。此文不詳述作用域有專門的主題闡述,不過作用域是理解閉包原理的基礎(chǔ)。這才是閉包的真正便利之處。閉包使用不當(dāng)就會(huì)很坑。
原文鏈接
為什么深度學(xué)習(xí)JavaScript?JavaScript如今是最流行的編程語(yǔ)言之一。它運(yùn)行在瀏覽器、服務(wù)器、移動(dòng)設(shè)備、桌面應(yīng)用,也可能包括冰箱。無(wú)需我舉其他再多不相干的例子,只要你正從事web開發(fā),你就不可避免地要寫JavaScript。
很多web開發(fā)者僅僅因?yàn)槟軐懣梢赃\(yùn)行的代碼就聲稱了解JavaScript。對(duì)于JavaScript,你可以用一個(gè)月就能寫代碼,掌握它之后終生收益。(If there are no errors and nobody’s complaining why should you need to learn more?)(譯者注:不知所云)
好吧,我就是曾經(jīng)聲稱很了解此語(yǔ)言的一員。幾年前我用AngularJS和Node寫應(yīng)用,當(dāng)時(shí)對(duì)自己的能力非常自信。拋開功能,我堅(jiān)信我已經(jīng)征服了JavaScript。
當(dāng)面試中讓我解釋一下閉包時(shí)我懵逼了。我感覺自己知道一點(diǎn),和回調(diào)有關(guān),我當(dāng)時(shí)一直用回調(diào)(當(dāng)時(shí)還不知道Promise),但就是不知道怎么描述其原理。
在我的開發(fā)職業(yè)生涯中那次失敗的JavaScript面試是最恥辱和最具教育意義的經(jīng)歷。從那時(shí)起我歷時(shí)一年半致力于JavaScript的高價(jià)段位,并決定分享于世人。先從一個(gè)最常見的JavaScript面試題開始:
什么是閉包?毫無(wú)疑問你已經(jīng)在各種應(yīng)用中使用過閉包。你每次為事件處理器添加回調(diào)時(shí)你都在用閉包的神奇屬性。
我遇到過很多關(guān)于此概念的解釋,但我最信服是Kyle Simpson下的定義:
當(dāng)一個(gè)方法執(zhí)行完脫離了自己的詞法作用域,但仍然能夠記住并訪問其詞法作用域,這就是閉包。
這個(gè)解釋開始可能有點(diǎn)晦澀,讓我們抽絲剝繭摘下閉包的真面目。
此文不詳述作用域(有專門的主題闡述),不過作用域是理解閉包原理的基礎(chǔ)。作用域就是包含某些屬性和方法的區(qū)域。每個(gè)JavaScript方法都會(huì)創(chuàng)建一個(gè)新的作用域,它內(nèi)部的變量和入?yún)⒍贾荒茉谄鋬?nèi)部訪問。
如果你在函數(shù)內(nèi)聲明一個(gè)變量,函數(shù)外是訪問不到的。不過,我們可以在函數(shù)內(nèi)部定義擁有作用域的內(nèi)部函數(shù)。這些內(nèi)嵌函數(shù)的特別之處在于它們可以訪問父作用域的變量。
坦白說這也算不上什么特別之處,因?yàn)槊恳粋€(gè)在全局作用域中定義的函數(shù)都能訪問全局變量。雖然我們提到的這些內(nèi)嵌函數(shù)可以訪問父函數(shù)的作用域,但它們不能在父函數(shù)之外被調(diào)用。除非我們將其暴露出來。
我們將內(nèi)部函數(shù)暴露出來就可以在全局作用域中使用。牛逼!現(xiàn)在我們就可以隨心所欲了。不過,暴露出來的內(nèi)部函數(shù)實(shí)際上引用了它父作用域的變量,會(huì)不會(huì)有問題?不會(huì)!絕對(duì)不會(huì),這就是閉包!
閉包是暴露出來的內(nèi)嵌方法我不確定這是否是給閉包下的最好的定義,但這確實(shí)能夠很好地抓住此術(shù)語(yǔ)的本質(zhì)。閉包就是我們?cè)诤瘮?shù)外部就能訪問其父作用域的內(nèi)部函數(shù)。你能否通過我們之前提到的詞法作用域理解此解釋呢?
function person(name) { return { greet: function() { console.log("hello from " + name) } } } let alex = person("alex"); alex.greet(); // hello from alex console.log(alex.name); // undefined console.log(name); // will throw ReferenceError
我們?cè)诖硕x了只有一個(gè)參數(shù)name的person函數(shù)。它返回一個(gè)以greet為屬性的對(duì)象?,F(xiàn)在我們知道,暴露出的greet函數(shù)可以訪問父函數(shù)參數(shù)。盡管name變量并沒有定義在greet的作用域中,因?yàn)樗情]包,所以greet可以從其父作用域中獲取。
并不是特別難理解,你可能都用了很多次了。我學(xué)閉包前從沒把它想象的多難,理解了其背后的原理,我就明白了封裝并使用模塊。
哇唔,哇唔...模塊?封裝?出乎意料。
模塊和用閉包封裝我深陷JavaScript漩渦之前首先了解到其中很多高深詞匯都有實(shí)踐解釋。模塊和封裝就是這類術(shù)語(yǔ)很完美的例子。我先從封裝開始,用相同的策略各個(gè)擊破去理解它們。
封裝是基本的編程原則之一。學(xué)過OOP(面向?qū)ο缶幊蹋┑娜藢?duì)此概念非常熟悉,但對(duì)于沒學(xué)過的人來說---封裝就是允許我們保持?jǐn)?shù)據(jù)私有的基本隱藏機(jī)制。我們不想把方法的所有內(nèi)容暴露給全局作用域,我們想讓大多數(shù)內(nèi)容保持私有且不可訪問。
這才是閉包的真正便利之處。我們可以利用閉包訪問父作用域,甚至在外部訪問的時(shí)候獲得適當(dāng)?shù)胤庋b。在父函數(shù)中可能有很多方法和變量,通過利用閉包我們可以將其暴露給我們需要的函數(shù)。
我們可以用閉包為我們的方法定義一個(gè)公共API,并保持方法中所有東西私有。
我們現(xiàn)在已經(jīng)掌握了封裝,只需實(shí)踐即可。在JavaScript中對(duì)此概念的實(shí)踐就是使用模塊。
模塊在ES6中可以使用import和export關(guān)鍵字產(chǎn)生以文件為基礎(chǔ)的模塊,但要注意這些只是語(yǔ)法糖而已。
function Person(firstName, lastName, age) { var private = "this is a private member"; return { getName: function() { console.log("My name is " + firstName + " " + lastName); }, getAge: function() { console.log("I am " + age + " years old") } } } let person = new Person("Alex", "Kondov", 22); person.getName(); person.getAge(); console.log(person.private); //undefined
這是一個(gè)我們可以保持一些數(shù)據(jù)私有的簡(jiǎn)單例子。我們可以有其他內(nèi)嵌方法,盡管導(dǎo)出后可以使用,但并沒有都暴露出來。
function Order (items) { const total = items => { return items.reduce((acc, curr) => { return acc + curr.price }, 0) } const addTaxToPrice = price => price + (price * 0.2) return { calculateTotal: () => { return addTaxToPrice(total(items)).toFixed(2) } } } const items = [ { name: "Toy", price: 14.99 }, { name: "Candy", price: 7.99 } ] const order = Order(items) console.log(order.total) // undefined console.log(order.addTaxToPrice) // undefined console.log(order.calculateTotal()) // 27.58
在這個(gè)更接近真實(shí)的例子中方法返回了一個(gè)order對(duì)象,唯一暴露出來的方法是calculateTotal。Order函數(shù)有一個(gè)閉包,允許此閉包使用它的變量和入?yún)?。在你?jì)算訂單總價(jià)時(shí)隱藏了內(nèi)部邏輯,也方便以后擴(kuò)展。
怪異之處JavaScript也有其怪異之處。實(shí)際上有些怪異之處讓人非常蛋疼。閉包使用不當(dāng)就會(huì)很坑。
下面的代碼經(jīng)常出現(xiàn)在JavaScript面試中讓猜它的輸出。
for (var i = 1; i <= 5; i++) { setTimeout(function timer () { console.log(i); }, i * 1000); }
從1循環(huán)到5并在一段時(shí)間后打印出當(dāng)前的數(shù)字。正常感覺會(huì)輸出1,2,3,4,5,對(duì)嗎?
讓我驚奇的是上面的代碼會(huì)在輸出臺(tái)上連續(xù)5次打印出6。如果循環(huán)之中沒有setTimeout不會(huì)有任何問題,因?yàn)槿罩据敵鰰?huì)被立即執(zhí)行。很明顯,排隊(duì)操作引發(fā)了這個(gè)問題。
我們期望每次調(diào)用setTimeout都會(huì)獲取i變量自身的拷貝,但實(shí)際情況卻是它訪問的是它的父作用域。又因?yàn)槎荚谂抨?duì),第一個(gè)日志會(huì)在它排隊(duì)1秒后發(fā)生。當(dāng)1000毫秒過去的時(shí)候,循環(huán)早已結(jié)束,i變量也早已被賦值為6。
我明白了這個(gè)問題但如何修復(fù)呢?setTimeout會(huì)在全局作用域?qū)ふ?b>i變量,無(wú)法打印出我們想要的數(shù)字。我們可以把setTimeout包裹到一個(gè)方法中并將我們想要輸出的變量傳進(jìn)去。這樣setTimeout會(huì)從它的父作用域而不是全局作用域進(jìn)行訪問。
for (var i = 1; i <= 5; i++) { (function(index) { setTimeout(function timer () { console.log(index); }, index * 1000); })(i) }
我們使用IIFE(立即執(zhí)行函數(shù),Immediately Invoked Function Expression)并把想輸出的數(shù)字傳進(jìn)去。IIFE是一種定義后立即調(diào)用的函數(shù),它常用于這種情況---我們想要?jiǎng)?chuàng)建作用域。這種方式每次函數(shù)調(diào)用都用它們自己的變量拷貝,這也意味著setTimeout運(yùn)行時(shí)會(huì)訪問對(duì)應(yīng)的數(shù)字。所以上面的例子我們會(huì)達(dá)到期待的結(jié)果:1,2,3,4,5
結(jié)束語(yǔ)此文介紹了閉包的本質(zhì),但還有很多需要學(xué)習(xí)和更多的邊際情況需要考慮。如果你想更進(jìn)一步了解閉包,我強(qiáng)烈推薦Kyle Simpson的書中Scope & Closures的部分。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/95033.html
摘要:原文鏈接原文作者你想知道的關(guān)于作用域的一切譯中有許多章節(jié)是關(guān)于的但是對(duì)于初學(xué)者來說甚至是一些有經(jīng)驗(yàn)的開發(fā)者這些有關(guān)作用域的章節(jié)既不直接也不容易理解這篇文章的目的就是為了幫助那些想更深一步學(xué)習(xí)了解作用域的開發(fā)者尤其是當(dāng)他們聽到一些關(guān)于作用域的 原文鏈接: Everything you wanted to know about JavaScript scope原文作者: Todd Mott...
摘要:忍者級(jí)別的函數(shù)操作對(duì)于什么是匿名函數(shù),這里就不做過多介紹了。我們需要知道的是,對(duì)于而言,匿名函數(shù)是一個(gè)很重要且具有邏輯性的特性。通常,匿名函數(shù)的使用情況是創(chuàng)建一個(gè)供以后使用的函數(shù)。 JS 中的遞歸 遞歸, 遞歸基礎(chǔ), 斐波那契數(shù)列, 使用遞歸方式深拷貝, 自定義事件添加 這一次,徹底弄懂 JavaScript 執(zhí)行機(jī)制 本文的目的就是要保證你徹底弄懂javascript的執(zhí)行機(jī)制,如果...
摘要:上面的例子應(yīng)用了匿名函數(shù)這個(gè)特性,還可以使用構(gòu)造函數(shù)或者閉包來添加事件監(jiān)聽器另一個(gè)重要特性,則是上面這段代碼中最后一行的最后一個(gè)參數(shù),用來控制監(jiān)聽器對(duì)于冒泡事件的響應(yīng)。在這里你不能使用閉包或者匿名函數(shù),并且控制域也是有限的。 原文出處:addEventListener vs onclick 之所以會(huì)想到這個(gè)話題,是因?yàn)樵诨仡欁约褐皩懙臑?button 動(dòng)態(tài)綁定事件的函數(shù)時(shí),腦海里忽...
摘要:前端日?qǐng)?bào)精選免費(fèi)的計(jì)算機(jī)編程類中文書籍英文技術(shù)文檔看不懂看印記中文就夠了的內(nèi)部工作原理美團(tuán)點(diǎn)評(píng)點(diǎn)餐前后端分離實(shí)踐讓你的動(dòng)畫坐上時(shí)光機(jī)中文譯有多棒簡(jiǎn)書譯別再使用圖片輪播了掘金譯如何在中使用掘金個(gè)讓增長(zhǎng)成億美元公司的獨(dú)特方法眾成翻 2017-08-23 前端日?qǐng)?bào) 精選 FPB 2.0:免費(fèi)的計(jì)算機(jī)編程類中文書籍 2.0英文技術(shù)文檔看不懂?看印記中文就夠了!Virtual DOM 的內(nèi)部工作...
摘要:例如,考慮比較由字符串構(gòu)造函數(shù)創(chuàng)建的字符串對(duì)象和字符串字面量這里的操作符正在檢查這兩個(gè)對(duì)象的值并返回,但是鑒于它們不是相同類型并且返回。我的建議是完全繞過這個(gè)問題,只是不使用字符串構(gòu)造函數(shù)創(chuàng)建字符串對(duì)象。 Q1:javascript的閉包是如何工作的? 正如愛因斯坦所說的: 如果你不能把它解釋給一個(gè)六歲的小孩,說明你對(duì)它還不夠了解。 我曾嘗試向一個(gè)27歲的朋友解釋js閉包并且完全失敗了...
閱讀 3024·2023-04-26 02:14
閱讀 3841·2019-08-30 15:55
閱讀 1928·2019-08-29 16:42
閱讀 2834·2019-08-26 11:55
閱讀 2929·2019-08-23 13:38
閱讀 558·2019-08-23 12:10
閱讀 1372·2019-08-23 11:44
閱讀 2980·2019-08-23 11:43