一文剖析JavaScript中閉包的難點(diǎn)
一、作用域基本介紹
ES6之前只有全局作用域與函數(shù)作用域兩種,ES6出現(xiàn)之后,新增了塊級(jí)作用域。
1. 全局作用域
在JavaScript中,全局變量是掛載在window對(duì)象下的變量,所以在網(wǎng)頁(yè)中的任何位置你都可以使用并且訪問到這個(gè)全局變量。
當(dāng)我們定義很多全局變量的時(shí)候,會(huì)容易引起變量命名的沖突,所以在定義變量的時(shí)候應(yīng)該注意作用域的問題:
var globalName = 'global' function getName() { console.log(globalName) // global var name = 'inner' console.log(name) // inner } getName() console.log(name) // 報(bào)錯(cuò) console.log(globalName) // global function setName() { vName = 'setName' } setName() console.log(vName) // setName console.log(windwo.vName) // setName
2. 函數(shù)作用域
在JavaScript中,函數(shù)定義的變量叫作函數(shù)變量,這個(gè)時(shí)候只能在函數(shù)內(nèi)部才能訪問到它,所以它的作用域也就是函數(shù)的內(nèi)存,稱為函數(shù)作用域。
當(dāng)這個(gè)函數(shù)被執(zhí)行完之后,這個(gè)局部變量也相應(yīng)會(huì)被銷毀。所以你會(huì)看到在getName函數(shù)外面的name是訪問不到的:
function getName() { var name = 'inner' console.log(name) // inner } getName() console.log(name) // 報(bào)錯(cuò)
3. 塊級(jí)作用域
ES6新增了塊級(jí)作用域,最直接的表現(xiàn)就是新增的let關(guān)鍵詞,使用let關(guān)鍵詞定義的變量只能在塊級(jí)作用域中被訪問,有"暫時(shí)性死區(qū)"的特定,也就是說這個(gè)變量在定義之前是不能被使用的。
if語句及for語句后面的{...}這里面所包括的,就是塊級(jí)作用域:
console.log(a) // a is not defined if (true) { let a = '123' console.log(a) // 123 } console.log(a) // a is not defined
二、什么是閉包
紅寶書:閉包是指有權(quán)訪問另外一個(gè)函數(shù)作用域中的變量的函數(shù) MDN:一個(gè)函數(shù)和對(duì)其周圍狀態(tài)的引用捆綁在一起(或者說函數(shù)被引用包圍),這樣的組合就是閉包。也就是說,閉包讓你可以在一個(gè)內(nèi)層函數(shù)中訪問到其外層函數(shù)的作用域。
1. 閉包的基本概念
閉包其實(shí)就是一個(gè)可以訪問其他函數(shù)內(nèi)部變量的函數(shù)。即一個(gè)定義在函數(shù)內(nèi)部的函數(shù),或者直接說閉包是個(gè)內(nèi)嵌函數(shù)也可以。
因?yàn)橥ǔG闆r下,函數(shù)內(nèi)部變量是無法在外部訪問的(即全局變量和局部變量的區(qū)別),因此使用閉包的作用,就具備實(shí)現(xiàn)了能在外部訪問某個(gè)函數(shù)內(nèi)部變量的功能,讓這些內(nèi)部變量的值始終可以保存在內(nèi)存中。
function fun1() { var a = 1 return function () { console.log(a) } } fun1() var result = fun1() result() // 1
2. 閉包產(chǎn)生的原因
當(dāng)訪問一個(gè)變量時(shí),代碼解釋器會(huì)首先在當(dāng)前的作用域查找,如果沒找到,就去父級(jí)作用域去查找,直到找到該變量或者不存在父級(jí)作用域中,這樣的鏈路就是作用域鏈。
var a = 1 function fun1() { var a = 2 function fun2() { var a = 3 console.log(a) // 3 } } // fun1 函數(shù)的作用域指向全局作用域(window)和它自己本身;fun2 函數(shù)的作用域指向全局作用域(window)、fun1 和它本身;而作用域是從最底層向上找,直到找到全局作用域 window 為止,如果全局還沒有的話就會(huì)報(bào)錯(cuò) function fun1() { var a = 2 function fun2() { console.log(a) // 2 } return fun2 } var result = fun1() result() // 那是不是只有返回函數(shù)才算是產(chǎn)生了閉包呢?其實(shí)也不是,回到閉包的本質(zhì),**我們只需要讓父級(jí)作用域的引用存在即可** var fun3 function fun1() { var a = 2 fun3 = function () { console.log(a) } } fun1() fun3()
閉包產(chǎn)生的本質(zhì):當(dāng)前環(huán)境中存在指向父級(jí)作用域的引用。
3. 閉包的表現(xiàn)形式
返回一個(gè)函數(shù),上面將原因的時(shí)候已經(jīng)說過,這里就不在贅述了。
在定時(shí)器、事件監(jiān)聽、Ajax請(qǐng)求、Web Workers 或者任何異步中,只要使用了回調(diào)函數(shù),實(shí)際上就是在使用閉包。
// 2.1定時(shí)器 setTimeout(function handler() { console.log('1') }, 1000) // 2.2事件監(jiān)聽 $('app').click(function () { console.log('Event Listener') })
作為函數(shù)參數(shù)傳遞的形式,比如下面的例子:
// 3.作為函數(shù)參數(shù)傳遞的形式 var a = 1 function foo() { var a = 2 function baz() { console.log(a) } bar(baz) } function bar(fn) { // 這就是閉包 fn() } foo() // 輸出2,而不是1
IIFE(立即執(zhí)行函數(shù)),創(chuàng)建了閉包,保存了全局作用域(window)和當(dāng)前函數(shù)的作用域,因此可以輸出全局的變量,如下所示:
// 4.IIFE(立即執(zhí)行函數(shù)) var a = 2 (function IIFE() { console.log(a) // 輸出2 })()
IIFE 這個(gè)函數(shù)會(huì)稍微有些特殊,算是一種自執(zhí)行匿名函數(shù),這個(gè)匿名函數(shù)擁有獨(dú)立的作用域。這不僅可以避免了外界訪問此 IIFE 中的變量,而且又不會(huì)污染全局作用域,我們經(jīng)常能在高級(jí)的 JavaScript 編程中看見此類函數(shù)。
三、如何解決循環(huán)輸出問題
for (var i = 1; i <= 5; i++) { setTimeout(function () { console.log(i) }, 0) } // 依次輸出 5個(gè)6
setTimeout 為宏任務(wù),由于 JS 中單線程 eventLoop 機(jī)制,在主線程同步任務(wù)執(zhí)行完后才去執(zhí)行宏任務(wù),因此循環(huán)結(jié)束后 setTimeout 中的回調(diào)才依次執(zhí)行。
因?yàn)?setTimeout 函數(shù)也是一種閉包,往上找它的父級(jí)作用域就是 window,變量 i 為 window 上的全局變量,開始執(zhí)行 setTimeout 之前變量 i 已經(jīng)是 6 了,因此最后輸出的連續(xù)都是 6。
1. 利用 IIFE
利用 IIFE,當(dāng)每次 for 循環(huán)時(shí),把此時(shí)的變量 i 傳遞到定時(shí)器中,然后執(zhí)行:
for (var i = 1; i <= 5; i++) { (function (j) { setTimeout(function timer() { console.log(j) }, 0) })(i) }
2. 使用 ES6 中的 let
let 讓 JS 有了塊級(jí)作用域,代碼的作用域以塊級(jí)為單位進(jìn)行執(zhí)行:
for(let i = 1; i <= 5; i++) { setTimeout(function() { console.log() },0) }
3. 定時(shí)器傳入第三個(gè)參數(shù)
setTimeout 作為經(jīng)常使用的定時(shí)器,它是存在第三個(gè)參數(shù)的,日常工作中我們經(jīng)常使用的一般是前兩個(gè),一個(gè)是回調(diào)函數(shù),另外一個(gè)是時(shí)間,而第三個(gè)參數(shù)用得比較少:
for(var i=1;i<=5;i++) { setTimeout(function(j) { console.log(j) },0,i) }
第三個(gè)參數(shù)的傳遞,改變了 setTimeout 的執(zhí)行邏輯,從而實(shí)現(xiàn)我們想要的結(jié)果,這也是一種解決循環(huán)輸出問題的途徑。
到此這篇關(guān)于一文剖析JavaScript中閉包的難點(diǎn)的文章就介紹到這了,更多相關(guān)JavaScript閉包內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
js防止DIV布局滾動(dòng)時(shí)閃動(dòng)的解決方法
這篇文章主要介紹了js防止DIV布局滾動(dòng)時(shí)閃動(dòng)的解決方法,通過js的window.requestAnimationFrame來解決這一問題,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2014-10-10FireFox與IE 下js兼容觸發(fā)click事件的代碼
FireFox與IE 下js兼容觸發(fā)click事件 ,對(duì)于需要兼容這兩者的朋友,就需要參考下下面的代碼了2008-11-11微信小程序?qū)崿F(xiàn)同一頁(yè)面取值的方法分析
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)同一頁(yè)面取值的方法,結(jié)合實(shí)例形式分析了微信小程序在同一頁(yè)面取值的常見情況與具體操作技巧,需要的朋友可以參考下2019-04-04淺析ES6的八進(jìn)制與二進(jìn)制整數(shù)字面量
這篇文章給大家介紹了ES6特性中的八進(jìn)制和二進(jìn)制整數(shù)字面量,介紹的挺不錯(cuò)的現(xiàn)在分享給大家,有需要的可以參考借鑒。2016-08-08JS數(shù)組方法reverse()用法實(shí)例分析
這篇文章主要介紹了JS數(shù)組方法reverse()用法,結(jié)合實(shí)例形式分析了JS數(shù)組reverse()方法基本功能、用法與操作注意事項(xiàng),需要的朋友可以參考下2020-01-01JavaScript 動(dòng)態(tài)生成方法的例子
動(dòng)態(tài)生成方法的例子,這些方法在新對(duì)象實(shí)例化的時(shí)候創(chuàng)建2009-07-07Javascript結(jié)合css實(shí)現(xiàn)網(wǎng)頁(yè)換膚功能
現(xiàn)在網(wǎng)站換皮膚是比較常見的功能,大多數(shù)論壇都有的,要想實(shí)現(xiàn)這樣效果可以看如下代碼.2009-11-11