摘要:進擊的巨人第三篇,本篇就作用域作用域鏈閉包等知識點,一一擊破。在此我們遵照的方式,暫且稱是閉包。所以,一名合格的前端,除了會用閉包,還要正確的解除閉包引用。
進擊的巨人第三篇,本篇就作用域、作用域鏈、閉包等知識點,一一擊破。
作用域作用域:負責收集并維護由所有聲明的標識符(變量)組成的一系列查詢,并實施一套非常嚴格的規則,確定當前執行的代碼對這些標識符(變量)的訪問權限——《你不知道的JavaScript上卷》
作用域有點像圈地盤,大家劃好區域,然后各自經營管理,井水不犯河水。
var globaValue = "我是全局作用域"; function foo() { var fooValue = "我是foo作用域"; function bar() { var barValue = "我是bar作用域"; } } function other() { var otherValue = "我是other作用域"; }作用域的變量聲明
不同作用域下命名相同的變量不會發生沖突,"就近原則"選取。
var name = "任何名字"; function getName() { var name = "以樂之名"; console.log(name); // "以樂之名" } console.log(name); // "任何名字"作用域的類型
執行上下文環境有:全局、函數、eval。那么作用域也有三種,ES6新增了塊級作用域。
全局作用域
函數作用域
eval作用域(不推薦使用eval,暫時忽略)
塊級作用域(ES6新增)
全局作用域JavaScript中全局環境只有一個,對應的全局作用域也只有一個。局部環境沒有使用var/let/const聲明的變量默認都會成為全局變量。
function foo() { a = 10; }; foo(); console.log(a); // 10 變全局變量(意外由此發生)函數作用域
ES6之前,想要實現局部作用域的方式,都是是通過在函數中聲明變量來實現的,所以也稱函數作用域,支持嵌套多個。
var a = 20; function foo() { var a = 10; console.log(a); // 10; } foo();
函數中聲明變量時,建議在函數起始部分聲明所有變量,方便查看,切記要用var/let/const聲明,防止手抖將局部變量變成全局變量。
function getClient() { var name; var phone; var sex; }塊級作用域
我們先來理解什么是塊?所謂塊,其實就是被大括號{}包裹的代碼部分。
if (true) { // 這里就是塊了,也可稱代碼塊 }
ES6前沒有塊級作用域的概念,所以{}中并沒有自己的作用域。如果我們想在ES5的環境下構建塊級作用域,一般都是是通過立即執行函數來實現的。
var name = "任何名字"; (function(window) { var name = "以樂之名"; console.log(name); // "以樂之名" }(window)); console.log(name); // "任何名字"
ES5借助函數作用域來實現塊級作用域的方式,會讓我們的代碼充斥大量的立即執行函數(IIFE),不便于代碼的閱讀。好的代碼的就跟好的文章一樣,讓閱讀者讀起來舒暢明了。
為此,ES6新增塊級作用域的概念,使用let/const聲明變量的方式,即可將其作用域指定在代碼塊中,跟函數作用域一樣支持嵌套。
let i = 0; for (let i = 0; i < 10; i++){ ????console.log(i); } i; // 0
let/const不允許變量提升,必須"先聲明再使用"。這種限制,稱為"暫時性死區"。這也能讓我們在代碼編寫階段變得更加規范化,執行跟書寫順序保持一致。
作用域鏈(變量查詢規則)變量被作用域所管理,那么變量在作用域中的查找規則,就是所謂的作用域鏈。
作用域鏈的用途,是保證對執行環境有權訪問的所有變量和函數的有序訪問——《JavaScript高級程序設計》
"在當前執行環境開始查找使用到的變量,如果找到,則返回其值。如果找不到,會逐層往上級(父作用域)查找,直到全局作用域"。
var money = 100; function foo() { function bar() { console.log(money); } bar(); } foo();自由變量
變量我們見的不少,但"自由變量"聽著是不是挺唬人的。其實對它,我們并不陌生。
"自由變量:當前執行環境使用到,但并未在當前執行環境聲明的變量(函數參數arguments排除)"
函數調用時,進入執行上下文創建階段,會對arguments進行隱式的變量聲明。
var outer = "我是外面變量"; function foo() { var inner = "我是里面變量,不是自由變量"; console.log(outer); // 這里用到了outer,但outer并不在函數foo中聲明,所以outer就是foo中的自由變量 }
"自由變量的作用域由詞法環境決定,也就是它的作用域在代碼書寫階段就已經確定了,而不是在代碼執行階段確定。"
"自由變量的值是在代碼執行時確定的,變量變量變量,值肯定要變,所以自由變量的值只有在程序運行階段才能確定。"
閉包開篇第一文我們就執行環境,執行棧做出了詳解,有所遺忘的可再溫習。執行棧是我們理解閉包原理基礎中的基礎。
函數調用棧過程的圖再曬出來,順便溫習下。
function foo () { function bar () { return "I am bar"; } return bar(); } foo();
函數調用時入棧,調用結束出棧。執行函數時,會創建一個變量對象去存儲函數中的變量,方法,參數arguments等,結束調用時,該變量對象就會被銷毀。(理想的情況下,不理想的情況就是出現"閉包"調用了)。
什么是閉包?閉包是指有權訪問另外一個函數作用域的變量的函數。——《JavaScript高級程序設計》
閉包是指那些能夠訪問自由變量的函數。
——MDN
閉包的特點首先是函數,其次是它可以訪問到父級作用域的變量對象,即使父級函數完成調用后"理應出棧銷毀"。
判定閉包出現函數作為參數傳遞
函數作為返回值傳遞
function foo() { var fooVal = "2019"; var bar = function() { console.log(fooVal); // bar中使用到了自由變量fooVal } return bar; // 函數作為參數返回 } var getValue = foo(); getValue(); // 2019
對函數中誰是閉包,各文檔解釋不一。在此我們遵照Chrome的方式,暫且稱foo是閉包。
因為作用域和作用域鏈規則的限定,子環境的自由變量只能逐層向上到父環境查找。
但是通過閉包,我們在外部環境也可以獲取到變量fooVal,雖然foo()函數執行完成了,但它并沒從函數調用棧中銷毀,其變量對象存儲仍然能被訪問到。
實際執行過程請看圖:
把上述代碼改以下,接著看:
function foo() { var fooVal = "2019"; var bar = function() { console.log(fooVal); // bar中使用到了自由變量fooVal } return bar; // 函數作為參數返回 } var getValue = foo(); var fooVal = "2018"; // 這里的fooVal是全局作用域的變量 getValue(); // 2019
答案與結果不符的小伙伴要回頭理解下自由變量了。"自由變量的作用域在代碼書寫時(函數創建時)就確定了",所以函數中getValue()使用的fooVal在foo的作用域下,而不是在全局作用域下。
答對的小伙伴們再來一道題,加深你的記憶
function fn() { var max = 10; function bar(x) { if (x > max) { console.log(x) } } return bar; } var f1 = fn(); var max = 100; f1(20); // 輸出20
題目解析:max作為函數bar中的自由變量,它的作用域在函數bar創建的時候就確定了,就是函數fn中的max,所以它的作用域鏈查找到fn中已經結束并返回了,不會再向上找到全局作用域。
注意:棧中存儲的不只是閉包中使用到的自由變量,而是父級函數的整個變量對象(父級函數作用域中聲明的方法,變量,參數等)
閉包的應用場景上文中已經闡述了閉包的特點,就是能夠讓我們跨作用域取值(不局限于父子作用域)。列舉兩個實際開發中常用的栗子:
封裝回調保存作用域
for(var i = 1; i < 5; i++) { setTimeout((function(i){ return function() { console.log(i); } })(i), i * 1000) } // 原理:通過自執行函數傳參i,然后返回一個函數(閉包)中使用i,使父函數的變量對象一直存在
私有變量和方法實現模塊化
var makePeople = function () { var _name = "以樂之名"; return { getName: function () { console.log(_name); }, setName: function (name) { if (name != "Hello world") { _name = name; } } } } var me = makePeople(); me.getName(); // "以樂之名" me.setName("KenTsang"); me.getName(); // "KenTsang" // 原理:私有變量_name沒有對外訪問權限,但通過閉包使其一直保留在內存中,可以被外部調用
閉包的應用場景還有很多,具體實際情況還需具體分析。
閉包造成的內存泄露閉包的使用,破壞了函數的出棧過程。解釋執行棧的時候,講到同個函數即使調用自身,創建的變量對象也并非同一個,其內存存儲是各自獨立的。
棧中只入不出,函數的變量對象沒有被有效回收,就會造成瀏覽器內存占用逐步增加,內存占用過高的情況下,就會導致頁面卡頓,甚至瀏覽器崩潰。這就是我們常說閉包過度使用造成的"內存泄露"。
所以,一名合格的前端,除了會用閉包,還要正確的解除閉包引用。
垃圾回收機制講解時,通過設置變量值為null時可以解除變量的引用,以便下一次垃圾回收銷毀它。
function foo() { var fooVal = "2019"; var bar = function() { console.log(fooVal); } return bar; } var getValue = foo(); var fooVal = "2018"; getValue(); getValue = null; // 解除引用,下一次垃圾回收就會回收了寫在結尾
閉包算是前端初學者的一個難點,能解釋清楚并不容易,涉及到作用域,執行上下文環境、變量對象等等。
零散知識的內聚匯總,正是是系列更文的初衷所在。
知識不是小段子,聽完笑過就忘,唯有形成體系,達成閉環,才能深植入記憶中。
參考文檔:
深入理解javascript原型和閉包
本文首發Github,期待Star!
https://github.com/ZengLingYong/blog
作者:以樂之名
本文原創,有不當的地方歡迎指出。轉載請指明出處。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/101184.html
摘要:自執行函數閉包實現模塊化以樂之名程序員產品經理對作用域,以及閉包知識還沒掌握的小伙伴,可回閱前端進擊的巨人三從作用域走進閉包。參考文檔利用閉包實現模塊化翻譯淺談中的高階函數系列更文請關注專欄前端進擊的巨人,不斷更新中。。。 系列更文前三篇文章,圍繞了一個重要的知識點:函數。函數調用棧、函數執行上下文、函數作用域到閉包。可見不理解函數式編程,代碼都擼不好。 showImg(https:/...
摘要:函數柯里化是把支持多個參數的函數變成接收單一參數的函數,并返回一個函數能接收處理剩余參數,而反柯里化就是把參數全部釋放出來。但在一些復雜的業務邏輯封裝中,函數柯里化能夠為我們提供更好的應對方案,讓我們的函數更具自由度和靈活性。 showImg(https://segmentfault.com/img/bVburN1?w=800&h=600); 柯里化(Curring, 以邏輯學家Has...
摘要:在中,通過棧的存取方式來管理執行上下文,我們可稱其為執行棧,或函數調用棧。而處于棧頂的是當前正在執行函數的執行上下文,當函數調用完成后,它就會從棧頂被推出理想的情況下,閉包會阻止該操作,閉包后續文章深入詳解。 寫在開篇 已經不敢自稱前端小白,曾經吹過的牛逼總要一點點去實現。 正如前領導說的,自己喝酒吹過的牛皮,跪著都得含著淚去實現。 那么沒有年終完美總結,來個新年莽撞開始可好。 進擊巨...
摘要:有關函數柯里化的詳解,請回閱前端進擊的巨人五學會函數柯里化。構造函數中的通過操作符可以實現對函數的構造調用。在了解構造函數中的前,有必要先了解下實例化對象的過程。 showImg(https://segmentfault.com/img/bVburMp?w=800&h=600); 常見this的誤解 指向函數自身(源于this英文意思的誤解) 指向函數的詞法作用域(部分情況) th...
摘要:除了以上介紹的幾種對象創建方式,此外還有寄生構造函數模式穩妥構造函數模式。 showImg(https://segmentfault.com/img/remote/1460000018196128); 面向對象 是以 對象 為中心的編程思想,它的思維方式是構造。 面向對象 編程的三大特點:封裝、繼承、多態: 封裝:屬性方法的抽象 繼承:一個類繼承(復制)另一個類的屬性/方法 多態:方...
閱讀 2605·2023-04-25 15:07
閱讀 710·2021-11-24 10:21
閱讀 2312·2021-09-22 10:02
閱讀 3523·2019-08-30 15:43
閱讀 3230·2019-08-30 13:03
閱讀 2292·2019-08-29 17:18
閱讀 3591·2019-08-29 17:07
閱讀 1880·2019-08-29 12:27