前端百題斬——用“閉包”問(wèn)題征服面試官
13.1 定義
在JavaScript中,根據(jù)詞法作用域的規(guī)則,內(nèi)部函數(shù)總是可以訪問(wèn)其外部函數(shù)聲明的變量,當(dāng)通過(guò)調(diào)用一個(gè)外部函數(shù)返回一個(gè)內(nèi)部函數(shù)后,即使該外部函數(shù)已經(jīng)執(zhí)行結(jié)束了,但是內(nèi)部函數(shù)引用外部函數(shù)的變量依然保存在內(nèi)存中,就把這些變量的集合稱為閉包。
13.2 閉包實(shí)現(xiàn)
在一個(gè)函數(shù)中嵌套另一個(gè)函數(shù)或者將一個(gè)匿名函數(shù)作為值傳入另一個(gè)函數(shù)中。
- // 函數(shù)fun1中嵌套了fun2,fun2作為參數(shù)返回,外部調(diào)用時(shí)仍能打印val1,構(gòu)成閉包
- function fun1() {
- const val1 = 10;
- function fun2() {
- console.log(val1);
- }
- return fun2;
- }
- function fun3() {
- const val2 = 20;
- // 定時(shí)器中的為一個(gè)匿名函數(shù),其作為參數(shù)傳入了,函數(shù)fun3執(zhí)行完畢之后,1s鐘后才會(huì)執(zhí)行定時(shí)器函數(shù),但此時(shí)還能打印val2,構(gòu)成閉包
- setTimeout(function() {
- console.log(val2);
- }, 1000);
- }
13.3 流程
根據(jù)下面的函數(shù)來(lái)看看閉包的整個(gè)執(zhí)行流程
- function main() {
- const val1 = 20;
- var val2 = 2
- function valResult() {
- return val1 * val2;
- }
- return valResult;
- }
- var result = main();
- console.log(result()); // 40
上圖中展示了各個(gè)時(shí)期的調(diào)用棧,需要重點(diǎn)關(guān)注以下幾點(diǎn):
- 當(dāng)main函數(shù)執(zhí)行完畢后,main函數(shù)的執(zhí)行上下文從棧頂彈出;
- 返回的方法(valResult)中調(diào)用了main函數(shù)中的val1和val2變量,這兩個(gè)變量就會(huì)打包成closure閉包,加到[[scopes]];
- 調(diào)用返回的方法時(shí),作用域鏈為:result函數(shù)作用域——Closure(main)——全局作用域
13.4 優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
(1)可以重復(fù)使用變量,并且不會(huì)造成變量污染;
(2)可以用來(lái)定義私有屬性和私有方法
缺點(diǎn)
(1)會(huì)產(chǎn)生不銷毀的上下文,導(dǎo)致棧/堆內(nèi)存消耗過(guò)大
(2)會(huì)造成內(nèi)存泄露。
擴(kuò)展:閉包是怎么回收的?
- 如果閉包引入的函數(shù)是一個(gè)全局變量,那么閉包會(huì)一直存在直到頁(yè)面關(guān)閉;但如果這個(gè)閉包以后不再使用的話,就會(huì)造成內(nèi)存泄露;
- 如果引用閉包的函數(shù)是一個(gè)局部變量,等函數(shù)銷毀后,在下次JavaScript引擎執(zhí)行垃圾回收時(shí),判斷閉包內(nèi)容已經(jīng)不再被使用,則js引擎的垃圾回收器就會(huì)進(jìn)行回收。
13.5 用途
閉包用途主要有以下兩個(gè):
創(chuàng)建私有變量
- function MyName(name) {
- return {
- getName() {
- return name;
- }
- }
- }
- const myName = MyName('lili');
- // 只能通過(guò)getName訪問(wèn)對(duì)應(yīng)的名字,別的方式訪問(wèn)不到
- console.log(myName.getName()); // lili
作為回調(diào)函數(shù)。當(dāng)把函數(shù)作為值傳遞到某處,并在某個(gè)時(shí)刻進(jìn)行回調(diào)的時(shí)候就會(huì)創(chuàng)建一個(gè)閉包。例如定時(shí)器、DOM事件監(jiān)聽(tīng)器、Ajax請(qǐng)求。
- function fun(name) {
- setTimeout(() => {
- console.log(name);
- }, 1000);
- }
- fun('linlin');
13.6 經(jīng)典閉包問(wèn)題
多個(gè)子函數(shù)的[[scope]]都是同時(shí)指向父級(jí),是完全共享的。因此當(dāng)父級(jí)的變量對(duì)象被修改時(shí),所有子函數(shù)都受到影響。
- for (var i = 1; i < 5; i++) {
- setTimeout(() => console.log(i), 1000);
- }
上述代碼本意是輸出1、2、3、4,但結(jié)果卻是四個(gè)5,為了解決該問(wèn)題,主要有三種辦法。
變量可以通過(guò) 函數(shù)參數(shù)的形式 傳入,避免使用默認(rèn)的[[scope]]向上查找
- for (var i = 1; i < 5; i++) {
- (function(i) {
- setTimeout(() => console.log(i), 1000);
- })(i);
- }
使用setTimeout包裹,通過(guò)第三個(gè)參數(shù)傳入。(注:setTimeout后面可以有多個(gè)參數(shù),從第三個(gè)參數(shù)開(kāi)始其就作為回掉函數(shù)的附加參數(shù))
- for (var i = 1; i < 5; i++) {
- setTimeout(value => console.log(value), 1000, i);
- }
使用 塊級(jí)作用域,讓變量成為自己上下文的屬性,避免共享
- for (let i = 1; i < 5; i++) {
- setTimeout(() => console.log(i), 1000);
- }
本文轉(zhuǎn)載自微信公眾號(hào)「執(zhí)鳶者」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系執(zhí)鳶者公眾號(hào)。