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