首頁  >  文章  >  web前端  >  老司機帶你徹底搞懂JS閉包各種坑

老司機帶你徹底搞懂JS閉包各種坑

angryTom
angryTom轉載
2019-11-25 17:00:433248瀏覽

老司機帶你徹底搞懂JS閉包各種坑

老司機帶你徹底搞懂JS閉包各種坑

##閉包是js發展慣用的技巧,什麼是閉包?

閉包指的是:能夠存取另一個函數作用域的變數的函數。清晰的講:閉包就是一個函數,這個函數能夠存取其他函數的作用域中的變數。 eg:

function outer() {
     var  a = '变量1'
     var  inner = function () {
            console.info(a)
     }
    return inner    // inner 就是一个闭包函数,因为他能够访问到outer函数的作用域
}

很多人會搞不懂匿名函數與閉包的關係,實際上,閉包是站在作用域的角度上來定義的,因為inner訪問到outer作用域的變量,所以inner就是一個閉包函數。雖然定義很簡單,但有很多坑點,像是this指向、變數的作用域,稍微不注意可能就會造成記憶體外洩。我們先把問題拋一邊,思考一個問題:為什麼閉包函數能夠存取其他函數的作用域?

#從堆疊的角度看待js函數

基本變量的值一般都是存在棧記憶體中,而物件類型的變數的值儲存在堆疊記憶體中,棧記憶體儲存對應空間位址。基本的資料型態: Number 、Boolean、Undefined、String、Null。

var  a = 1   //a是一个基本类型
var  b = {m: 20 }   //b是一个对象

對應記憶體儲存:

老司機帶你徹底搞懂JS閉包各種坑

當我們執行b={m:30}時,堆記憶體就有新的物件{m:30} ,棧記憶體的b指向新的空間位址( 指向{m:30} ),而堆疊記憶體中原來的{m:20}就會被程式引擎垃圾回收掉,節省記憶體空間。我們知道js函數也是對象,它也是在堆與棧記憶體中儲存的,我們來看轉化:

var a = 1;
function fn(){
    var b = 2;
    function fn1(){
        console.log(b);
    }
    fn1();
}
fn();

老司機帶你徹底搞懂JS閉包各種坑

**

#棧是一種先進後出的資料結構:

1 在執行fn前,此時我們在全域執行環境(瀏覽器就是window作用域),全域作用域裡有個變數a;

2 進入fn,此時堆疊記憶體就會push一個fn的執行環境,這個環境裡有變數b和函數物件fn1,這裡可以存取自身執行環境和全域執行環境所定義的變數

3 進入fn1,此時棧內存就會push 一個fn1的執行環境,這裡面沒有定義其他變量,但是我們可以訪問到fn和全局執行環境裡面的變量,因為程序在訪問變量時,是向底層棧一個個找,如果找到全域執行環境裡都沒有對應變量,程式拋出underfined的錯誤。

4 隨著fn1()執行完畢,fn1的執行環境被杯銷毀,接著執行完fn(),fn的執行環境也會被銷毀,只剩下全局的執行環境下,現在沒有b變量,和fn1函數物件了,只有a 和fn(函數宣告作用域是window下)

**

在函數內存取某個變數是根據函數作用域鏈來判斷變數是否存在的,而函數作用域鍊是程式根據函數所在的執行環境棧來初始化的,所以上面的例子,我們在fn1裡面印出變數b,根據fn1的作用域鏈的找到對應fn執行環境下的變數b。所以當程式在呼叫某個函數時,做了一下的工作:準備執行環境,初始函數作用域鍊和arguments參數物件

我們現在看回最初的範例outer與inner

function outer() {
     var  a = '变量1'
     var  inner = function () {
            console.info(a)
     }
    return inner    // inner 就是一个闭包函数,因为他能够访问到outer函数的作用域
}
var  inner = outer()   // 获得inner闭包函数
inner()   //"变量1"

當程式執行完var inner = outer(),其實outer的執行環境並沒有被銷毀,因為他裡面的變數a仍然被inner的函數作用域鏈所引用,當程式執行完inner(), 這時候,inner和outer的執行環境才會被銷毀調;《JavaScript高級程式設計》書中建議:由於閉包會攜帶包含它的函數的作用域,因為會比其他函數佔用更多內容,過度使用閉包,會導致記憶體佔用過多。

現在我們明白了閉包,已經對應的作用域與作用域鏈,回歸主題:

坑點1: 引用的變數可能改變

function outer() {
      var result = [];
      for (var i = 0; i<10; i++){
        result.[i] = function () {
            console.info(i)
        }
     }
     return result
}

看起來result每個閉包函數對列印對應數字,1,2,3,4,...,10, 實際上不是,因為每個閉包函數存取變數i是outer執行環境下的變數i,隨著迴圈的結束,i已經變成10了,所以執行每個閉包函數,結果列印10, 10, ..., 10

怎麼解決這個問題呢?

function outer() {
      var result = [];
      for (var i = 0; i<10; i++){
        result.[i] = function (num) {
             return function() {
                   console.info(num);    // 此时访问的num,是上层函数执行环境的num,数组有10个函数对象,每个对象的执行环境下的number都不一样
             }
        }(i)
     }
     return result
}

坑點2: this指向問題

var object = {
     name: &#39;&#39;object",
     getName: function() {
        return function() {
             console.info(this.name)
        }
    }
}
object.getName()()    // underfined
// 因为里面的闭包函数是在window作用域下执行的,也就是说,this指向window

坑點3:記憶體外洩問題

function  showId() {
    var el = document.getElementById("app")
    el.onclick = function(){
      aler(el.id)   // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
    }
}
// 改成下面
function  showId() {
    var el = document.getElementById("app")
    var id  = el.id
    el.onclick = function(){
      aler(id)   // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
    }
    el = null    // 主动释放el
}

#技巧1: 用閉包解決遞歸呼叫問題

function  factorial(num) {
   if(num<= 1) {
       return 1;
   } else {
      return num * factorial(num-1)
   }
}
var anotherFactorial = factorial
factorial = null
anotherFactorial(4)   // 报错 ,因为最好是return num* arguments.callee(num-1),arguments.callee指向当前执行函数,但是在严格模式下不能使用该属性也会报错,所以借助闭包来实现
// 使用闭包实现递归
function newFactorial = (function f(num){
    if(num<1) {return 1}
    else {
       return num* f(num-1)
    }
}) //这样就没有问题了,实际上起作用的是闭包函数f,而不是外面的函数newFactorial

** 技巧2:用閉包模仿區塊級作用域**

es6沒出來之前,用var定義變數存在變數提升問題,eg:

for(var i=0; i<10; i++){
    console.info(i)
}
alert(i)  // 变量提升,弹出10

//为了避免i的提升可以这样做
(function () {
    for(var i=0; i<10; i++){
         console.info(i)
    }
})()
alert(i)   // underfined   因为i随着闭包函数的退出,执行环境销毁,变量回收

當然現在大多用es6的let 和const 定義。


這篇文章到這裡就已經全部結束了,更多其他精彩內容可以關注PHP中文網的

JavaScript影片教學專欄!  

以上是老司機帶你徹底搞懂JS閉包各種坑的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:jianshu.com。如有侵權,請聯絡admin@php.cn刪除