在面試過程當中,各位童鞋常常會被問道這樣的問題:"請描述下你對閉包的理解",或者在面試烤卷中會有關於閉包的選擇、填空題。若是是前者,大可一句帶過:"閉包就是一個函數有權訪問另外一個函數做用域中的變量"。若是是後者,那咱們拿起筆的那隻小爪爪可能會有一絲顫抖~~~(我對閉包真的熟悉嗎?)。html
爲了能讓你們在再次遇到有關閉包的問題時,能作到"心不虛,手不抖,LZ跟着感受走"。因此接下來,我要爲你們表演一個"我吹閉包,如吹大烏蘇"。走起!!!前端
開場白:請各位大聲告訴我下面的代碼打印了什麼?爲何?web
function daRio() {
let name = "劍大瑞" let callMe = function(bilibili) { return bilibili + name } return callMe } daRio()("帥哥") 複製代碼
先看代碼,咱們在上面的代碼中作了哪些事情?面試
daRio
中分別
「建立」了一個變量name,一個匿名函數callMe
當JavaScript引擎在執行代碼以前會經歷三個步驟:windows
分詞/詞法分析——》解析/語法分析——》代碼生成瀏覽器
最終結果代碼會轉化爲一組機器指令,緊接着開始執行。在上面這個過程當中其實有三個不通的角色相互配合,分別是Javascript引擎大哥、編譯器老2、做用域保姆。緩存
Javascript負責整個Javascript代碼的編譯及執行閉包
編譯器負責進行語法分析及代碼生成異步
做用域根據一套很是嚴格的家規(「規則」)收集並維護變量一系列的查詢,肯定誰有權限訪問誰。編輯器
做用域的查詢規則主要有兩種工做模型,一種是「詞法做用域」,遵循詞法做用域的查詢模型關注的是標識符定義的位置,好比Javascript.另外一種是「動態做用域」,遵循動態做用域的查詢模型關注的是函數從何處調用,其做用域鏈是基於運行時的調用棧的,好比Base、Perl。
當咱們看到 let name = "劍大瑞" 時,JS引擎會分爲兩步執行,一步由編譯器進行編譯時處理,一步由引擎在運行時處理,
在上面的代碼中
都是在進行下面這個操做:
❝首先編譯器會在當前做用域中聲明一個變量(若是以前沒有聲明過),而後在運行在時引擎會在做用域中查找該變量,若是能找到就對變量進行賦值。
❞
前面說到當引擎在詢問當前做用域中是否存在變量時,若是沒有找到當前變量則會繼續查找,若是找到就中止,沒有就拋錯。其實這一過程這就是咱們所說的做用域鏈查找。
每當咱們建立一個函數時,都會生成一個「函數做用域」,而這個函數做用域中就會保存當前函數參數和局部變量,若是存在做用域嵌套的話還會有一個做用域鏈指針,指向包裹該函數的包含環境。
這個過程是經過層層嵌套的做用域鏈最終找到咱們的目標變量,直至咱們的全局環境(在瀏覽器中即windows)。而且做用域鏈直接保證了執行環境有權訪問的全部變量、函數的有序訪問。
經過這張圖片咱們能夠看到在callMe函數中存在一個[[Scopes]]屬性,固然咱們不能經過callMe.Scopes訪問到他,可是咱們實實在在使用了他,這裏面保存了兩個對象指針一個是Closure,一個Global。當咱們的Js引擎在執行過程當中發如今callMe做用域中沒有找到變量name,就會沿着Scopes去查找,若是經過Closure找到則中止(即便Global中還存在同一個變量name)。這就是做用域鏈爲咱們callMe函數所提供的變量訪問權限!至此咱們也就明白了爲何咱們能夠在callMe中訪問到daRio中的name。
文章寫到這裏,我已經感受個人臺詞已經用完了,閉包已經沒得解釋了~~~,經過上面的內容咱們已經把閉包最爲本質的東西扒完了。不過仍是要一句話總結下閉包的原理:
❝其實閉包就是基於JavaScript的「詞法做用域」,當嵌套函數在外部環境執行的過程當中經過「做用域鏈」訪問到包含它的函數做用域中變量所造成的一種現象。而且因爲嵌套函數存在對包含函數變量引用的緣由,致使外部做用域中的變量沒法及時銷燬,會佔用必定的內。若是閉包過多,則會影響程序性能。
❞
let daRio = (function() {
let myAttr = { name: "劍大瑞" gender: "man", age: 18, height: 180, weChat: 185****0350 } let callMe = function(bilibili) { return bilibili + myAttr.name } let introduceMe = function() { return `Hi sweetie, my name is ${myAttr.name}, I\`m ${myAttr.age} years old and ${myAttr.height} , this\`s my weChat ${myAttr.weChat} ` } return { callMe, introduceMe } })() daRio.callMe("帥哥") daRio.introduceMe() 複製代碼
經過建立函數做用域 + 利用閉包的特色,咱們能夠實現簡單的模塊化
柯里化 Currying
經過Js函數柯里化,能夠實現函數參數的緩存效果。在平常的開發任務中咱們回常用到這項技術,好比bind的實現,React中的高階組件等等。
function youInfo(gender) {
let style if(gender) { style = "小姐姐" } else { style = "小鍋鍋" } return function(name) { return style + name } } let me = youInfo(1) console.log(me("劍大瑞")) // 打印了什麼? 複製代碼
for (var i = 0; i< 10; i++){
setTimeout(() => { console.log(i); }, 1000) } 複製代碼
PS:這道題涉及到JS的異步事件,若是吃透,對於理解JS的「事件循環機制」及異步很是有幫助哦~
var name = 'Tom';
(function() { if (typeof name == 'undefined') { var name = 'Jack'; console.log('Goodbye ' + name); } else { console.log('Hello ' + name); } })(); 複製代碼
PS:這道題涉及到Javascript的變量提高及函數做用域,請先分辨出它有沒有產生閉包呢?爲何?能夠在評論區留下答案哈
var data = [];
for (var i = 0; i < 3; i++) { data[i] = function () { console.log(i); }; } data[0](); data[1](); data[2](); 複製代碼
節目的最後,給你們留個問題:函數能夠經過做用域鏈訪問到上層甚至上上層的變量,可是爲何當閉包存在時,使用this會出錯呢?(要知詳情如何,且聽下回分解)
關於"閉包"的這杯酒我是吹完了,各位呢?
參考文獻: