JavaScript 閉包全方位解析

總結

概念: 有權訪問另外一個函數做用域中的變量的函數

優勢: 內存駐留、避免全局變量污染

缺點: 內存泄漏(?)、沒法預知變量被更改

相關知識點: 做用域、內存駐留、內存泄露、JS執行機制、內存機制、垃圾回收機制

1、什麼是閉包

一、做用域鏈

要理解什麼是閉包、首先咱們要對JS中的做用域做用域鏈有必定的理解:前端

做用域: 簡單來說就是一個變量可以被訪問的範圍,JS有三種做用域,分別是:全局做用域、函數做用域、塊級做用域

做用域鏈: 在JS中做用域是一層一層嵌套的, 子做用域中能夠訪問父做用域中的變量, 咱們把這種能夠一層一層向上訪問的鏈式結構叫作做用域鏈.

~~ 當JS建立一個函數時, 首先會建立一個預先包含全局變量對象的做用域鏈, 保存在內部的Scope屬性之中
當JS調用這個函數時, 會爲此函數建立一個執行環境, 也就是函數上下文
而後複製函數的Scope的屬性中的對象構建執行環境的做用域鏈~~瀏覽器

做用域與做用域鏈就比如是數學中的集合, 最大的即是全局做用域, 子集即是函數做用域, 子集中又能夠有子集,全部的本身均可以向外訪問,但全部的父級不能夠向子集訪問,相同的子集之間也不能夠互相訪問。安全

二、閉包的概念

有權訪問另外一個函數做用域中的變量的函數 《JavaScript高級程序設計》閉包

咱們來理解一下這句話異步

  • 首先: 閉包是...函數
  • 而後: 有權訪問另外一個函數做用域中的變量

那麼怎麼纔有權訪問另外一個函數做用域中的變量呢?
根據上文中子做用域中能夠訪問父做用域中的變量的特性,答案是: 成爲另外一個函數的子函數函數

補充一點:

在《JavaScript權威指南》, 強調了函數體內部變量能夠保存在函數做用域 函數對象能夠經過做用域鏈相互關聯起來,函數體內部變量能夠保存在函數做用域內,這就是閉包。性能

三、閉包的結構

從嚴格的角度來說, 閉包須要知足三個必要的條件:學習

  1. 函數嵌套
  2. 訪問父函數做用域中的變量
  3. 在函數聲明做用域外被調用( 有異議,歡迎討論 )

所以咱們猜測一個閉包的樣子, 大概應該是這樣的:線程

// 全局變量-全局做用域
var global = "global scope"; 
function partner() {
    // 局部變量-函數做用域
    var variable = 'Function scope';
    function children() {
        console.log(variable);
    }
}

// 此時子函數 children 訪問了父函數 partner, 咱們就稱子函數 children 爲閉包.

2、閉包存在的意義

意義: 內存駐留

當咱們要實現一個計數器時, 首先用常規的方法來寫:設計

// 計數器
var count = 0;
function counter() {
    console.log(count++);
}

counter(); // 1
counter(); // 2

上面的代碼已經實現了咱們所需的功能, 可是它並不完美, 一方面全局的count變量可能形成變量污染, 另外一方面代碼中的任何一個位置均可以輕鬆的修改這個count的值, 這是咱們所不能接受的!

所以, 咱們須要一個變量能夠在counter函數中訪問, 但它並不在全局做用域中, 且能夠長時間的停留在內存當中不被瀏覽器的垃圾回收機制清除, 因而咱們就想到了閉包, 接下來咱們用閉包再實現一下計數器:

// 計數器
var counter = (function() {
    var count = 0;
    return function() {
        console.log(count++);
    }
})()

counter(); // 1
counter(); // 2

閉包彷彿結合了全局做用域與局部做用域的優勢與一身,對於其餘函數做用域而言,父函數做用域中的變量就像是父函數和閉包的一個 「私有變量」 , 而對於父函數和閉包而言, 父函數做用域中的變量又好像身處 「全局做用域」 中.

3、閉包形成的影響

缺陷: 影響性能、變量修改
影響性能:

閉包的存在會致使函數中得變量一直存在於內存中,不能被垃圾回收機制清理, 致使內存消耗增長, 影響系統運行的性能,因此不能濫用閉包.

若是要使用閉包, 應該在使用結束時手動的清除閉包!

變量修改:

在閉包存在的時候,將父函數比作一個類,父函數中得局部變量就是類的私有屬性,而閉包訪問的變量就是類的公共屬性,在父函數做用域和閉包函數中均可以對 variable 變量進行修改, 這是件使人頭疼的事情, 由於你並不知道有多少閉包會在何時對你的變量進行修改, 這將形成你的程序極不穩定甚至執行異常.

4、閉包的經典案例

[操做內部變量]返回函數內部變量
function parent() {
    let name = 'sf'
    return {
        get() { return name },
        set(val) { name = val }
    }
}

const nameProxy = parent()
console.log(nameProxy.get()) // 'sf'
// 子函數children也能夠定義爲null在全局,而後在parent中賦值
[鎖定變量狀態]for循環中的定時器
// 由於setTimeout是異步的, 代碼執行先同步後異步, 因此當它執行的時候for循環已經結束了
for(var i=0,len=10; i<len; i++) {
    setTimeout(() => console.log(i), 1);
}
// 10個10

// 閉包寫法-IIFE
for(var i=0,len=10; i<len; i++) {
    ((i) => {
        setTimeout(() => console.log(i), 1);
    })(i)
}
[製造安全環境]徹底封閉的功能模塊-IIFE
// 內部的變量不會污染全局變量, 能夠放心的對多個模塊進行合併
(function(window) {
    let a = 1
    let b = '2'
    function add() {
        a++
    }
})(window)

5、閉包的底層原理

執行父函數時, JS線程會對內部的子函數進行預編譯, 看一看子函數中是否用到了父函數的內部變量
若是用到了, 爲了保證在將來調用子函數時不出錯, JS線程會在父函數執行完畢以後, 清空函數執行棧中的上下文以前, 將父函數中被用到的變量 copy 一份放在堆中, 供以後子函數引用.

6、其餘相關的擴展

內存泄露

內存泄露是指一塊被分配的內存既不能使用,又不能回收,直到瀏覽器進程結束。

在閉包形成的影響中,咱們常常會聽到一句話, 那即是:在IE中閉包的使用可能會致使內存泄漏。可是,在我學習V8引擎的過程當中發現,引起內存泄漏的緣由彷佛是循環引用,這讓我對閉包和內存泄漏的關係產生了疑惑.

後續我將圍繞內存泄漏從新整理一篇文章,這裏先引用一篇文章中的一句話和一個例子:

在IE瀏覽器中,因爲BOM和DOM中的對象是使用C++以COM對象的方式實現的,而COM對象的垃圾收集機制採用的是引用計數策略。在基於引用計數策略的垃圾回收機制中,若是兩個對象之間造成了循環引用,那麼這兩個對象都沒法被回收,但循環引用形成的內存泄露在本質上也不是閉包形成的。

做者:前端小學生\_f675  
連接:https://www.jianshu.com/p/66881ba3c8ba  
來源:簡書  
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
// 內存泄漏
var ele = document.getElementById("someElement");
ele.click = function() {
    console.log(ele.id);
}
// 解決方案:使用結束後釋放內存
var ele = document.getElementById("someElement");
var eleID = ele.id;
ele.click = function() {
    console.log(eleID);
}
ele = null;

垃圾回收

文章地址: 待更新

7、閉包的相關文章

相關文章
相關標籤/搜索