進擊的巨人第三篇腿短,本篇就作用域屏箍、作用域鏈、閉包等知識點答姥,一一擊破铣除。
作用域
作用域:負責收集并維護由所有聲明的標識符(變量)組成的一系列查詢谚咬,并實施一套非常嚴格的規(guī)則和敬,確定當前執(zhí)行的代碼對這些標識符(變量)的訪問權限
——《你不知道的JavaScript上卷》
作用域有點像圈地盤讹开,大家劃好區(qū)域,然后各自經營管理,井水不犯河水资锰。
var globaValue = '我是全局作用域';
function foo() {
var fooValue = '我是foo作用域';
function bar() {
var barValue = '我是bar作用域';
}
}
function other() {
var otherValue = '我是other作用域';
}
作用域的變量聲明
不同作用域下命名相同的變量不會發(fā)生沖突,"就近原則"選取文狱。
var name = '任何名字';
function getName() {
var name = '以樂之名';
console.log(name); // '以樂之名'
}
console.log(name); // '任何名字'
作用域的類型
執(zhí)行上下文環(huán)境有:全局盗棵、函數、eval尚辑。那么作用域也有三種辑鲤,ES6新增了塊級作用域。
- 全局作用域
- 函數作用域
- eval作用域(不推薦使用eval杠茬,暫時忽略)
- 塊級作用域(ES6新增)
全局作用域
JavaScript中全局環(huán)境只有一個月褥,對應的全局作用域也只有一個。沒有用var/let/const
聲明的變量默認都會成為全局變量瓢喉。
function foo() {
a = 10;
};
foo();
console.log(a); // 10 變全局變量(意外由此發(fā)生)
函數作用域
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的環(huán)境下構建塊級作用域越败,一般都是是通過立即執(zhí)行函數來實現的。
var name = '任何名字';
(function(window) {
var name = '以樂之名';
console.log(name); // '以樂之名'
}(window));
console.log(name); // '任何名字'
ES5借助函數作用域來實現塊級作用域的方式硼瓣,會讓我們的代碼充斥大量的立即執(zhí)行函數(IIFE)究飞,不便于代碼的閱讀。好的代碼的就跟好的文章一樣堂鲤,讓閱讀的人讀來舒暢明了亿傅。
為此,ES6新增塊級作用域的概念瘟栖,使用let/const
聲明變量的方式葵擎,即可將其作用域指定在代碼塊中,跟函數作用域一樣支持嵌套半哟。
let i = 0;
for (let i = 0; i < 10; i++){
console.log(i);
}
i; // 0
let/const
不允許變量提升酬滤,必須"先聲明再使用"。這種限制寓涨,稱為"暫時性死區(qū)"盯串。這也能讓我們在代碼編寫階段變得更加規(guī)范化,執(zhí)行跟書寫順序保持一致戒良。
作用域鏈(變量查詢規(guī)則)
變量被作用域所管理体捏,那么變量在作用域中的查找規(guī)則,就是所謂的作用域鏈糯崎。
作用域鏈的用途几缭,是保證對執(zhí)行環(huán)境有權訪問的所有變量和函數的有序訪問
——《JavaScript高級程序涉及》
"在當前執(zhí)行環(huán)境開始查找使用到的變量,如果找到拇颅,則返回其值奏司。如果找不到,會逐層往上級(父作用域)查找樟插,直到全局作用域"韵洋。
var money = 100;
function foo() {
function bar() {
console.log(money);
}
bar();
}
foo();
自由變量
變量我們見的不少,但"自由變量"聽著是不是挺唬人的黄锤。其實對它搪缨,我們并不陌生。
"自由變量:當前執(zhí)行環(huán)境使用到鸵熟,但并未在當前執(zhí)行環(huán)境聲明的變量(函數參數arguments排除)"
函數調用時副编,進入執(zhí)行上下文創(chuàng)建階段,會對argument
進行隱式的變量聲明流强。
var outer = '我是外面變量';
function foo() {
var inner = '我是里面變量痹届,不是自由變量';
console.log(outer);
// 這里用到了outer呻待,但outer并不在函數foo中聲明,所以outer就是foo中的自由變量
}
"自由變量的作用域由詞法環(huán)境決定队腐,也就是它的作用域在代碼書寫階段就已經確定了蚕捉,而不是在代碼編譯執(zhí)行階段確定。"
"自由變量的值是在代碼執(zhí)行時確定的柴淘,變量變量變量迫淹,值肯定要變,所以自由變量的值只有在程序運行階段才能確定为严。"
閉包
開篇第一文我們就執(zhí)行環(huán)境敛熬,執(zhí)行棧做出了詳解,有所遺忘的可再溫習第股。執(zhí)行棧是我們理解閉包原理基礎中的基礎应民。
函數調用棧過程的圖再曬出來,順便溫習下炸茧。
function foo () {
function bar () {
return 'I am bar';
}
return bar();
}
foo();
函數調用時入棧瑞妇,調用結束出棧稿静。執(zhí)行函數時梭冠,會創(chuàng)建一個變量對象去存儲函數中的變量,方法改备,參數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
是閉包炫隶。
因為作用域和作用域鏈規(guī)則的限定淋叶,子環(huán)境的自由變量只能逐層向上到父環(huán)境查找。
但是通過閉包伪阶,我們在外部環(huán)境也可以獲取到變量fooVal
煞檩,雖然foo()
函數執(zhí)行完成了处嫌,但它并沒從函數調用棧中銷毀,其變量對象存儲仍然能被訪問到斟湃。
實際執(zhí)行過程請看圖:
把上述代碼改以下锰霜,接著看:
function foo() {
var fooVal = '2019';
var bar = function() {
console.log(fooVal); // bar中使用到了自由變量fooVal
}
return bar; // 函數作為參數返回
}
var getValue = foo();
var fooVal = '2018'; // 這里的fooVal是全局作用域的變量
getValue(); // 2019
答案與結果不符的小伙伴要回頭理解下自由變量了。"自由變量的作用域在代碼書寫時(函數創(chuàng)建時)就確定了"桐早,所以函數中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
創(chuàng)建的時候就確定了,就是函數fn
中的max
陶衅,所以它的作用域鏈查找到fn
中已經結束并返回了屡立,不會再向上找到全局作用域。
注意:棧中存儲的不只是閉包中使用到的自由變量搀军,而是父級函數的整個變量對象(父級函數作用域中聲明的方法膨俐,變量,參數等)
閉包的應用場景
上文中已經闡述了閉包的特點罩句,就是能夠讓我們跨作用域取值(不局限于父子作用域)焚刺。列舉兩個實際開放中常用的栗子:
- 封裝回調保存作用域
for(var i = 1; i < 5; i++) {
setTimeout((function(i){
return function() {
console.log(i);
}
})(i), i * 1000)
}
// 原理:通過自執(zhí)行函數傳參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沒有對外訪問權限乳愉,但通過閉包使其一直保留在內存中,可以被外部調用
閉包的應用場景還有很多屯远,具體實際情況還需具體分析蔓姚。
閉包造成的內存泄露
閉包的使用,破壞了函數的出棧過程慨丐。解釋執(zhí)行棧的時候坡脐,講到同個函數即使調用自身,創(chuàng)建的變量對象也并非同一個房揭,其內存存儲是各自獨立的备闲。
棧中只入不出,函數的變量對象沒有被有效回收崩溪,就會造成瀏覽器內存占用逐步增加浅役,內存占用過高的情況下,就會導致頁面卡頓伶唯,甚至瀏覽器崩潰觉既。這就是我們常說的閉包造成的"內存泄露"。
所以,一名合格的前端瞪讼,除了會用閉包钧椰,還要正確的解除閉包引用。
垃圾回收機制講解時符欠,通過設置變量值為null
時可已解除變量的引用嫡霞,以便下一次垃圾回收銷毀它。
function foo() {
var fooVal = '2019';
var bar = function() {
console.log(fooVal);
}
return bar;
}
var getValue = foo();
var fooVal = '2018';
getValue();
getValue = null; // 解除引用希柿,下一次垃圾回收就會回收了
寫在結尾
閉包算是前端初學者的一個難點诊沪,能解釋清楚并不容易,涉及到作用域曾撤,執(zhí)行上下文環(huán)境端姚、變量對象等等。
零散知識的內聚匯總挤悉,正是是系列更文的初衷所在渐裸。
知識不是小段子,聽完笑過就忘装悲,唯有形成體系昏鹃,達成閉環(huán),才能深植入記憶中诀诊。
參考文檔:
本文首發(fā)Github洞渤,期待Star!
https://github.com/ZengLingYong/blog
作者:以樂之名
本文原創(chuàng)畏梆,有不當的地方歡迎指出您宪。轉載請指明出處奈懒。