第四章:變量瘤袖、作用域和內(nèi)存問題
本章內(nèi)容:
- 理解基本類型和引用類型
- 理解執(zhí)行環(huán)境
- 理解垃圾回收機(jī)制
4.1 基本類型和引用類型
ECMAScript中的變量包含兩種不同類型的值: 基本類型值和引用類型值
基本類型有:Undefined测柠、Null、Boolean鹅士、Number券躁、String。
這五種數(shù)據(jù)類型是按值訪問的掉盅。
引用類型的值是保存在內(nèi)存中的對(duì)象也拜。 javascript不允許直接訪問內(nèi)存位置。
引用類型的值按引用訪問的趾痘。
4.1.1 動(dòng)態(tài)屬性
// 創(chuàng)建一個(gè)引用類型
var person = new Object();
person.name = 'zhangzhuo';
alert(person.name); //zhangzhuo
// 創(chuàng)建一個(gè)基本類型
var name = 'zhangzhuo';
alert(name.toUpperCase()); //ZHANGZHUO
name.age = 18;
alert(name.age); //error
這里雖然name能調(diào)用String.toUpperCase是因?yàn)榛绢愋妥詣?dòng)創(chuàng)建了基本包裝類型String的實(shí)例慢哈。但在該行運(yùn)行后便清空了。
基本類型不能添加屬性永票。
不可變的基本類型與可變的引用類型
4.1.2 復(fù)制變量值
從一個(gè)變量從另外一個(gè)變量復(fù)制基本類型值和引用類型時(shí)卵贱,也存在不同。
復(fù)制基本類型:
var num1 = 20;
var num2 = num1;
num2 = 30;
在變量對(duì)象中的數(shù)據(jù)發(fā)生復(fù)制行為時(shí)瓦侮,系統(tǒng)會(huì)自動(dòng)為新的變量分配一個(gè)新值艰赞。var num2 = num1
執(zhí)行之后,num1與num2雖然值都等于20肚吏,但是他們其實(shí)已經(jīng)是相互獨(dú)立互不影響的值了方妖。具體如圖。所以我們修改了num2的值以后罚攀,num1的值并不會(huì)發(fā)生變化党觅。
復(fù)制引用類型:
var obj1 = {a:10,b:15};
var obj2 = obj1;
obj1.a = 20;
alert(obj2.a); // 20
我們通過var obj1 = obj2
執(zhí)行一次復(fù)制引用類型的操作。引用類型的復(fù)制同樣也會(huì)為新的變量自動(dòng)分配一個(gè)新的值保存在變量對(duì)象中斋泄,但不同的是杯瞻,這個(gè)新的值,僅僅只是引用類型的一個(gè)地址指針炫掐。當(dāng)?shù)刂分羔樝嗤瑫r(shí)魁莉,盡管他們相互獨(dú)立,但是在變量對(duì)象中訪問到的具體對(duì)象實(shí)際上是同一個(gè)。如圖所示旗唁。
因此當(dāng)我改變obj1時(shí)畦浓,obj2也發(fā)生了變化。這就是引用類型的特性检疫。
4.1.3 傳遞參數(shù)
ECMAScript中所有函數(shù)的參數(shù)均是按值傳遞讶请。也就是說,會(huì)把函數(shù)外部的值復(fù)制給函數(shù)內(nèi)部的參數(shù)屎媳,就把值從一個(gè)變量復(fù)制給另一個(gè)變量相同夺溢。
在向參數(shù)傳遞引用類型的時(shí)候,其實(shí)會(huì)把這個(gè)值在內(nèi)存的地址復(fù)制給局部變量烛谊。
// demo1 傳遞基本類型
function addTen(num){
num += 10;
return num;
}
var count = 20;
var result = addTen(count);
alert(count); // 20
alert(result); // 30
從demo1可知道风响,傳遞的count變量,數(shù)字20被復(fù)制給了變量num晒来。num的數(shù)值增加了10钞诡,并不會(huì)影響外層的count郑现。
// demo2 傳遞引用類型
function setName(obj){
obj.name = 'zhangzhuo';
}
var person = new Object();
setName(person);
alert(person.name); // zhangzhuo
person變量的內(nèi)存值復(fù)制給了obj湃崩。obj和person指向同一個(gè)對(duì)象,所以當(dāng)函數(shù)內(nèi)部改變obj的屬性的時(shí)候接箫,person也會(huì)發(fā)生了變化攒读。
證明:對(duì)象是傳值而不是傳引用
//demo3 證明對(duì)象是傳值
function setName(obj){
obj.name = 'zhangzhuo';
obj = new Object();
obj.name = 'dudu';
}
var person = new Object();
setName(person);
alert(person.name); // zhangzhuo
如果person是傳遞引用,那么person就會(huì)指向name為'dudu'的新對(duì)象辛友。但是薄扁,訪問person.name的時(shí)候顯示仍然是zhangzhuo。說明對(duì)象是傳值而非傳遞引用废累。
4.1.4 檢測類型
檢測一個(gè)基本類型可以用typeof
:
檢測引用類型(判斷變量是什么類型的對(duì)象)邓梅,ECMAScript提供了instanceof
(原理:根據(jù)原型鏈來識(shí)別)。用法:
result = variable instanceof constructor; // 返回值 true or false
// eg:
alert(person instanceof Object); //變量person是Object嗎
alert(colors instanceof Array); //變量colors是Array嗎
如果使用instanceof檢測基本類型邑滨,會(huì)返回false日缨。因?yàn)榛绢愋筒皇菍?duì)象。
延伸閱讀1: 理解內(nèi)存分配
堆與棧
棧是一種FIFO(Last-In-First-Out)后進(jìn)先出的數(shù)據(jù)結(jié)構(gòu)掖看,在javascript中我們可以用Array模擬匣距。
var arr = []; // 創(chuàng)建一個(gè)棧
array.push('apple'); // 壓入一個(gè)元素apple ['apple']
array.push('orange'); // 壓入一個(gè)元素orange ['apple','orange']
array.pop(); // 彈出orange ['apple']
array.push('banana'); // 壓入一個(gè)元素banana ['apple','banana']
基本類型值是存儲(chǔ)在棧中的簡單數(shù)據(jù)段,也就是說哎壳,他們的值直接存儲(chǔ)在變量訪問的位置毅待。
堆是存放數(shù)據(jù)的一種離散數(shù)據(jù)結(jié)構(gòu),在javascript中归榕,引用值是存放在堆中的尸红。
那為什么引用值要放在堆中窗看,而原始值要放在棧中,不都是在內(nèi)存中嗎浦夷,為什么不放在一起呢?那接下來金刁,讓我們來探索問題的答案!
function Person(id,name,age){
this.id = id;
this.name = name;
this.age = age;
}
var num = 10;
var bol = true;
var str = "abc";
var obj = new Object();
var arr = ['a','b','c'];
var person = new Person(100,"zhangzhuo",25);
然后我們來看一下內(nèi)存分析圖:
變量num,bol,str為基本數(shù)據(jù)類型级乐,它們的值疙咸,直接存放在棧中,obj,person,arr為復(fù)合數(shù)據(jù)類型风科,他們的引用變量存儲(chǔ)在棧中撒轮,指向于存儲(chǔ)在堆中的實(shí)際對(duì)象。
由上圖可知贼穆,我們無法直接操縱堆中的數(shù)據(jù)题山,也就是說我們無法直接操縱對(duì)象,但我們可以通過棧中對(duì)對(duì)象的引用來操作對(duì)象故痊,就像我們通過遙控機(jī)操作電視機(jī)一樣顶瞳,區(qū)別在于這個(gè)電視機(jī)本身并沒有控制按鈕。
現(xiàn)在讓我們來回答為什么引用值要放在堆中愕秫,而原始值要放在棧中的問題:
記住一句話:能量是守衡的慨菱,無非是時(shí)間換空間,空間換時(shí)間的問題
堆比棧大戴甩,棧比堆的運(yùn)算速度快,對(duì)象是一個(gè)復(fù)雜的結(jié)構(gòu)符喝,并且可以自由擴(kuò)展,如:數(shù)組可以無限擴(kuò)充甜孤,對(duì)象可以自由添加屬性协饲。將他們放在堆中是為了不影響棧的效率。而是通過引用的方式查找到堆中的實(shí)際對(duì)象再進(jìn)行操作缴川。相對(duì)于簡單數(shù)據(jù)類型而言茉稠,簡單數(shù)據(jù)類型就比較穩(wěn)定,并且它只占據(jù)很小的內(nèi)存把夸。不將簡單數(shù)據(jù)類型放在堆是因?yàn)橥ㄟ^引用到堆中查找實(shí)際對(duì)象是要花費(fèi)時(shí)間的而线,而這個(gè)綜合成本遠(yuǎn)大于直接從棧中取得實(shí)際值的成本。所以簡單數(shù)據(jù)類型的值直接存放在棧中扎即。
4.2 執(zhí)行環(huán)境和作用域
執(zhí)行函數(shù)
執(zhí)行環(huán)境(execution context吞获, 有的地方也翻譯為執(zhí)行上下文)是javascript中最重要的一個(gè)概念。執(zhí)行環(huán)境定義了變量或者函數(shù)有權(quán)訪問的其他數(shù)據(jù)谚鄙。每個(gè)執(zhí)行環(huán)境都有一個(gè)與之關(guān)聯(lián)的變量對(duì)象(variable object),環(huán)境中所有定義的變量和函數(shù)都保存在這個(gè)對(duì)象中各拷。
全局執(zhí)行環(huán)境是最外圍的一個(gè)執(zhí)行環(huán)境。
每個(gè)函數(shù)都有自己的執(zhí)行環(huán)境闷营,當(dāng)執(zhí)行流進(jìn)入一個(gè)函數(shù)的時(shí)候烤黍,函數(shù)的環(huán)境就會(huì)被推入一個(gè)環(huán)境棧中知市,而這個(gè)函數(shù)執(zhí)行完畢后,棧將其環(huán)境彈出速蕊,把控制權(quán)返回之前的執(zhí)行環(huán)境嫂丙。ECMAScript程序中的執(zhí)行流就是由這個(gè)方便的機(jī)制控制著。
延伸閱讀2: 理解執(zhí)行環(huán)境
每次當(dāng)控制器轉(zhuǎn)到可執(zhí)行代碼的時(shí)候规哲,就會(huì)進(jìn)入當(dāng)前代碼的執(zhí)行環(huán)境跟啤,它會(huì)形成一個(gè)作用域。JavaScript中的運(yùn)行環(huán)境大概包括三種情況唉锌。
- 全局環(huán)境:JavaScript代碼運(yùn)行起來會(huì)首先進(jìn)入該環(huán)境隅肥;
- 函數(shù)環(huán)境:當(dāng)函數(shù)被調(diào)用執(zhí)行時(shí),會(huì)進(jìn)入當(dāng)前函數(shù)中執(zhí)行代碼 袄简;
- evel: (不建議使用腥放,忽略);
因此在一個(gè)JavaScript程序中绿语,必定會(huì)產(chǎn)生多個(gè)執(zhí)行環(huán)境秃症,在我的上一篇文章中也有提到,JavaScript引擎會(huì)以棧的方式來處理它們吕粹,這個(gè)棧种柑,我們稱其為函數(shù)調(diào)用棧(call stack)。棧底永遠(yuǎn)都是全局環(huán)境昂芜,而棧頂就是當(dāng)前正在執(zhí)行的環(huán)境莹规。
當(dāng)代碼在執(zhí)行過程中赔蒲,遇到以上三種情況泌神,都會(huì)生成一個(gè)執(zhí)行環(huán)境,放入棧中舞虱,而處于棧頂?shù)沫h(huán)境執(zhí)行完畢之后欢际,就會(huì)自動(dòng)出棧。為了更加清晰的理解這個(gè)過程矾兜,根據(jù)下面的例子损趋,結(jié)合圖示給大家展示。
執(zhí)行上下文可以理解為函數(shù)執(zhí)行的環(huán)境椅寺,每一個(gè)函數(shù)執(zhí)行時(shí)浑槽,都會(huì)給對(duì)應(yīng)的函數(shù)創(chuàng)建這樣一個(gè)執(zhí)行環(huán)境。
var color = 'blue';
function changeColor() {
var anotherColor = 'red';
function swapColors() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
}
swapColors();
}
changeColor();
我們用ECStack來表示處理執(zhí)行環(huán)境的的堆棧返帕。我們很容易知道桐玻,第一步,首先是全局環(huán)境入棧荆萤。
全局環(huán)境入棧之后镊靴,其中的可執(zhí)行代碼開始執(zhí)行铣卡,直到遇到了changeColor()
,這一句激活函數(shù)changeColor
創(chuàng)建它自己的執(zhí)行環(huán)境偏竟,因此第二步就是changeColor的執(zhí)行環(huán)境入棧煮落。
changeColor的環(huán)境入棧之后,控制器開始執(zhí)行其中的可執(zhí)行代碼踊谋,遇到swapColors()
之后又激活了一個(gè)執(zhí)行環(huán)境蝉仇。因此第三步是swapColors的執(zhí)行上下文入棧。
在swapColors的可執(zhí)行代碼中殖蚕,再?zèng)]有遇到其他能生成執(zhí)行環(huán)境的情況量淌,因此這段代碼順利執(zhí)行完畢,swapColors的環(huán)境從棧中彈出嫌褪。
swapColors的執(zhí)行環(huán)境彈出之后呀枢,繼續(xù)執(zhí)行changeColor的可執(zhí)行代碼,也沒有再遇到其他執(zhí)行環(huán)境笼痛,順利執(zhí)行完畢之后彈出裙秋。這樣,ECStack中就只身下全局環(huán)境了缨伊。
全局上下文在瀏覽器窗口關(guān)閉后出棧摘刑。
詳細(xì)了解了這個(gè)過程之后,我們就可以對(duì)執(zhí)行上下文總結(jié)一些結(jié)論了刻坊。
- js是單線程的枷恕;
- 同步執(zhí)行,只有棧頂?shù)沫h(huán)境處于執(zhí)行中谭胚,其他上下文需要等待
- 全局環(huán)境只有唯一的一個(gè)徐块,它在瀏覽器關(guān)閉時(shí)出棧
- 函數(shù)的執(zhí)行環(huán)境的個(gè)數(shù)沒有限制
- 每次某個(gè)函數(shù)被調(diào)用,就會(huì)有個(gè)新的執(zhí)行環(huán)境為其創(chuàng)建灾而,即使是調(diào)用的自身函數(shù)胡控,也是如此。
為了鞏固一下執(zhí)行環(huán)境的理解旁趟,我們?cè)賮砝L制一個(gè)例子的演變過程昼激,這是一個(gè)簡單的閉包例子。
function f1(){
var n=999;
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
因?yàn)閒1中的函數(shù)f2在f1的可執(zhí)行代碼中锡搜,并沒有被調(diào)用執(zhí)行橙困,因此執(zhí)行f1時(shí),f2不會(huì)創(chuàng)建新的上下文耕餐,而直到result執(zhí)行時(shí)凡傅,才創(chuàng)建了一個(gè)新的。具體演變過程如下蛾方。 (入棧相當(dāng)于要執(zhí)行代碼)
作用域和作用域鏈
作用域:
- 在JavaScript中像捶,我們可以將作用域定義為一套規(guī)則,這套規(guī)則用來管理引擎如何在當(dāng)前作用域以及嵌套的子作用域中根據(jù)標(biāo)識(shí)符名稱進(jìn)行變量查找上陕。
- 作用域與執(zhí)行環(huán)境是完全不同的兩個(gè)概念。我知道很多人會(huì)混淆他們拓春,但是一定要仔細(xì)區(qū)分释簿。
- JavaScript中只有全局作用域與函數(shù)作用域(因?yàn)閑val我們平時(shí)開發(fā)中幾乎不會(huì)用到它,這里不討論)硼莽。
JavaScript代碼的整個(gè)執(zhí)行過程庶溶,分為兩個(gè)階段,代碼編譯階段與代碼執(zhí)行階段懂鸵。編譯階段由編譯器完成偏螺,將代碼翻譯成可執(zhí)行代碼,這個(gè)階段作用域規(guī)則會(huì)確定匆光。執(zhí)行階段由引擎完成套像,主要任務(wù)是執(zhí)行可執(zhí)行代碼,執(zhí)行上下文在這個(gè)階段創(chuàng)建终息。
作用域鏈:
作用域鏈夺巩,是由當(dāng)前環(huán)境與上層環(huán)境的一系列變量對(duì)象組成,它保證了當(dāng)前執(zhí)行環(huán)境對(duì)符合訪問權(quán)限的變量和函數(shù)的有序訪問周崭。
當(dāng)代碼在一個(gè)環(huán)境中執(zhí)行的時(shí)候柳譬,會(huì)創(chuàng)建變量對(duì)象和一個(gè)作用域鏈(scope chain)。是保證對(duì)執(zhí)行環(huán)境有權(quán)訪問所有變量和函數(shù)的有序訪問续镇。作用域鏈的前端美澳,始終是當(dāng)前的執(zhí)行環(huán)境的變量對(duì)象。如果這個(gè)環(huán)境是函數(shù)摸航,則將其變量對(duì)象(activation object)作為活動(dòng)對(duì)象制跟。變量對(duì)象最開始只包含一個(gè)變量,即arguments對(duì)象(這個(gè)對(duì)象在全局環(huán)境中是不存在的)忙厌。
標(biāo)識(shí)符的解析是沿著作用域鏈一級(jí)一級(jí)地搜索標(biāo)識(shí)符的過程凫岖。搜索過程始終是從作用域鏈的前端開始。
var color = 'blue';
function changeColor(){
if(color === 'blue'){
color = 'red';
} else {
color = 'blue';
}
}
changeColor();
alert(color); //red
在這個(gè)例子中逢净,函數(shù)changeColor的作用域鏈包含兩個(gè)對(duì)象,它自己的變量對(duì)象arguments和全局環(huán)境的變量對(duì)象歼指〉粒可以在函數(shù)內(nèi)部訪問變量color,就是因?yàn)榭梢栽谧饔糜蜴溦业剿?/p>
延伸閱讀3: 作用域與作用域鏈
在訪問一個(gè)變量的時(shí)候踩身,就必須存在一個(gè)可見性的問題胀茵,這就是作用域。更深入的說挟阻,當(dāng)訪問一個(gè)變量或者調(diào)用一個(gè)函數(shù)的時(shí)候琼娘,javaScript引擎將不同執(zhí)行位置上的變量對(duì)象按照規(guī)則構(gòu)建一個(gè)鏈表峭弟。在訪問一個(gè)變量的時(shí)候,先從鏈表的第一個(gè)變量對(duì)象中查找脱拼,如果沒有則在第二個(gè)變量對(duì)象中查找瞒瘸,直到搜索結(jié)束。這也就形成了作用域鏈的概念熄浓。
延伸閱讀4: 變量對(duì)象詳解
當(dāng)調(diào)用一個(gè)函數(shù)時(shí)(激活)情臭,一個(gè)新的執(zhí)行環(huán)境就會(huì)被創(chuàng)建。而一個(gè)執(zhí)行環(huán)境的生命周期可以分為兩個(gè)階段赌蔑。
- 創(chuàng)建階段
在這個(gè)階段中俯在,執(zhí)行上下文會(huì)分別創(chuàng)建變量對(duì)象,建立作用域鏈娃惯,以及確定this的指向跷乐。
- 代碼執(zhí)行階段
創(chuàng)建完成之后,就會(huì)開始執(zhí)行代碼趾浅,這個(gè)時(shí)候劈猿,會(huì)完成變量賦值,函數(shù)引用潮孽,以及執(zhí)行其他代碼揪荣。
變量對(duì)象(Variable Object)
變量對(duì)象的創(chuàng)建,依次經(jīng)歷了以下幾個(gè)過程往史。
- 建立arguments對(duì)象仗颈。檢查當(dāng)前執(zhí)行環(huán)境中的參數(shù),建立該對(duì)象下的屬性與屬性值椎例。
- 檢查當(dāng)前執(zhí)行環(huán)境的函數(shù)聲明挨决,也就是使用function關(guān)鍵字聲明的函數(shù)。在變量對(duì)象中以函數(shù)名建立一個(gè)屬性订歪,屬性值為指向該函數(shù)所在內(nèi)存地址的引用脖祈。如果函數(shù)名的屬性已經(jīng)存在,那么該屬性將會(huì)被新的引用所覆蓋刷晋。
- 檢查當(dāng)前執(zhí)行環(huán)境中的變量聲明盖高,每找到一個(gè)變量聲明,就在變量對(duì)象中以變量名建立一個(gè)屬性眼虱,屬性值為undefined喻奥。如果該變量名的屬性已經(jīng)存在,為了防止同名的函數(shù)被修改為undefined捏悬,則會(huì)直接跳過撞蚕,原屬性值不會(huì)被修改。
許多讀者在閱讀到這的時(shí)候會(huì)因?yàn)橄旅娴倪@樣場景對(duì)于“跳過”一詞產(chǎn)生疑問过牙。既然變量聲明的foo遇到函數(shù)聲明的foo會(huì)跳過甥厦,可是為什么最后foo的輸出結(jié)果仍然是被覆蓋了纺铭?
function foo() { console.log('function foo') }
var foo = 20;
console.log(foo); // 20
其實(shí)只是大家在閱讀的時(shí)候不夠仔細(xì),因?yàn)樯厦娴娜龡l規(guī)則僅僅適用于變量對(duì)象的創(chuàng)建過程刀疙。也就是執(zhí)行環(huán)境的創(chuàng)建過程舶赔。而foo = 20
是在執(zhí)行環(huán)境的執(zhí)行過程中運(yùn)行的,輸出結(jié)果自然會(huì)是20庙洼。對(duì)比下例顿痪。
console.log(foo); // function foo
function foo() { console.log('function foo') }
var foo = 20;
// 上例的執(zhí)行順序?yàn)?
// 首先將所有函數(shù)聲明放入變量對(duì)象中
function foo() { console.log('function foo') }
// 其次將所有變量聲明放入變量對(duì)象中,但是因?yàn)閒oo已經(jīng)存在同名函數(shù)油够,因此此時(shí)會(huì)跳過undefined的賦值
// var foo = undefined;
// 然后開始執(zhí)行階段代碼的執(zhí)行
console.log(foo); // function foo
foo = 20;
根據(jù)這個(gè)規(guī)則蚁袭,理解變量提升就變得十分簡單了。
在上面的規(guī)則中我們看出石咬,function聲明會(huì)比var聲明優(yōu)先級(jí)更高一點(diǎn)揩悄。為了幫助大家更好的理解變量對(duì)象,我們結(jié)合一些簡單的例子來進(jìn)行探討鬼悠。
// demo01
function test() {
console.log(a);
console.log(foo());
var a = 1;
function foo() {
return 2;
}
}
test();
在上例中删性,我們直接從test()的執(zhí)行環(huán)境開始理解。全局作用域中運(yùn)行test()
時(shí)焕窝,test()的執(zhí)行上下文開始創(chuàng)建蹬挺。為了便于理解,我們用如下的形式來表示
// 創(chuàng)建過程
testEC = {
// 變量對(duì)象
VO: {},
scopeChain: {}
}
// 因?yàn)楸疚臅簳r(shí)不詳細(xì)解釋作用域鏈它掂,所以把變量對(duì)象專門提出來說明
// VO 為 Variable Object的縮寫巴帮,即變量對(duì)象
VO = {
arguments: {...}, //注:在瀏覽器的展示中,函數(shù)的參數(shù)可能并不是放在arguments對(duì)象中虐秋,這里為了方便理解榕茧,我做了這樣的處理
foo: <foo reference> // 表示foo的地址引用
a: undefined
}
未進(jìn)入執(zhí)行階段之前,變量對(duì)象中的屬性都不能訪問客给!但是進(jìn)入執(zhí)行階段之后用押,變量對(duì)象轉(zhuǎn)變?yōu)榱嘶顒?dòng)對(duì)象,里面的屬性都能被訪問了靶剑,然后開始進(jìn)行執(zhí)行階段的操作蜻拨。
這樣,如果再面試的時(shí)候被問到變量對(duì)象和活動(dòng)對(duì)象有什么區(qū)別抬虽,就又可以自如的應(yīng)答了官觅,他們其實(shí)都是同一個(gè)對(duì)象,只是處于執(zhí)行環(huán)境的不同生命周期阐污。不過只有處于函數(shù)調(diào)用棧棧頂?shù)膱?zhí)行環(huán)境中的變量對(duì)象,才會(huì)變成活動(dòng)對(duì)象咱圆。
// 執(zhí)行階段
VO -> AO // Active Object
AO = {
arguments: {...},
foo: <foo reference>,
a: 1,
this: Window
}
因此笛辟,上面的例子demo1功氨,執(zhí)行順序就變成了這樣
function test() {
function foo() {
return 2;
}
var a;
console.log(a);
console.log(foo());
a = 1;
}
test();
再來一個(gè)例子,鞏固一下我們的理解手幢。
// demo2
function test() {
console.log(foo);
console.log(bar);
var foo = 'Hello';
console.log(foo);
var bar = function () {
return 'world';
}
function foo() {
return 'hello';
}
}
test();
// 創(chuàng)建階段
VO = {
arguments: {...},
foo: <foo reference>,
bar: undefined
}
// 這里有一個(gè)需要注意的地方捷凄,因?yàn)関ar聲明的變量當(dāng)遇到同名的屬性時(shí),會(huì)跳過而不會(huì)覆蓋
// 執(zhí)行階段
VO -> AO
VO = {
arguments: {...},
foo: 'Hello',
bar: <bar reference>,
this: Window
}
延伸閱讀5: 詳細(xì)圖解作用域鏈與閉包
作用域鏈围来,是由當(dāng)前環(huán)境與上層環(huán)境的一系列變量對(duì)象組成跺涤,它保證了當(dāng)前執(zhí)行環(huán)境對(duì)符合訪問權(quán)限的變量和函數(shù)的有序訪問。
為了幫助大家理解作用域鏈监透,我我們先結(jié)合一個(gè)例子桶错,以及相應(yīng)的圖示來說明。
var a = 20;
function test(){
var b = a + 10;
function innerTest(){
var c = 10;
return b + c;
}
return innerTest();
}
console.log(test());
在上面的例子中胀蛮,全局院刁,函數(shù)test,函數(shù)innerTest的執(zhí)行上下文先后創(chuàng)建粪狼。我們?cè)O(shè)定他們的變量對(duì)象分別為VO(global)退腥,VO(test), VO(innerTest)。而innerTest的作用域鏈再榄,則同時(shí)包含了這三個(gè)變量對(duì)象狡刘,所以innerTest的執(zhí)行上下文可如下表示。
innerTestEC = {
VO: {...}, // 變量對(duì)象
scopeChain: [VO(innerTest), VO(test), VO(global)], // 作用域鏈
}
我們可以直接用一個(gè)數(shù)組來表示作用域鏈困鸥,數(shù)組的第一項(xiàng)scopeChain[0]為作用域鏈的最前端嗅蔬,而數(shù)組的最后一項(xiàng),為作用域鏈的最末端窝革,所有的最末端都為全局變量對(duì)象购城。
很多人會(huì)誤解為當(dāng)前作用域與上層作用域?yàn)榘P(guān)系,但其實(shí)并不是虐译。以最前端為起點(diǎn)瘪板,最末端為終點(diǎn)的單方向通道我認(rèn)為是更加貼切的形容。如圖漆诽。
注意侮攀,因?yàn)樽兞繉?duì)象在執(zhí)行上下文進(jìn)入執(zhí)行階段時(shí),就變成了活動(dòng)對(duì)象厢拭,這一點(diǎn)在上一篇文章中已經(jīng)講過兰英,因此圖中使用了AO來表示。Active Object
是的供鸠,作用域鏈?zhǔn)怯梢幌盗凶兞繉?duì)象組成畦贸,我們可以在這個(gè)單向通道中,查詢變量對(duì)象中的標(biāo)識(shí)符,這樣就可以訪問到上一層作用域中的變量了薄坏。
小結(jié):
javascript變量可以保存兩種類型的值:基本類型值與引用類型值趋厉。基本類型的值源于以下五種基本數(shù)據(jù)類型:Undefined胶坠、Null君账、Boolean、Number沈善、String乡数。基本類型的值與引用類型的值具有以下的特點(diǎn):
- 基本類型值在內(nèi)存中占據(jù)固定大小空間闻牡,因此被保存在棧內(nèi)存中净赴;
- 從一個(gè)變量向另一個(gè)變量復(fù)制基本類型的值,會(huì)創(chuàng)建該值得副本澈侠;
- 引用類型的值是對(duì)象劫侧,保存在堆內(nèi)存中;
- 包含引用類型的變量實(shí)際上包含的并不是對(duì)象本身哨啃,而是指向該對(duì)象的指針烧栋;
- 從一個(gè)變量向另一個(gè)變量復(fù)制引用類型的值,復(fù)制其實(shí)是指針拳球,因此兩個(gè)變量最終會(huì)指向同一個(gè)對(duì)象审姓;
- 確定一個(gè)值是哪種基本類型可以用typeof操作符,而確定一個(gè)值是哪種引用類型用instanceof操作符祝峻;
所有的變量(包括基本類型和引用類型)都存在一個(gè)執(zhí)行環(huán)境中魔吐,這個(gè)執(zhí)行環(huán)境決定了變量的生命周期,以及哪一部分代碼可以訪問其中的變量莱找。以下是關(guān)于執(zhí)行環(huán)境的總結(jié):
- 執(zhí)行環(huán)境有全局執(zhí)行環(huán)境和函數(shù)執(zhí)行環(huán)境之分酬姆;
- 每次進(jìn)入一個(gè)新的執(zhí)行環(huán)境,都會(huì)創(chuàng)建一個(gè)用于搜索變量和函數(shù)的作用域鏈奥溺,和一個(gè)變量對(duì)象辞色;
- 通過作用域鏈,函數(shù)中的執(zhí)行環(huán)境不僅能夠訪問函數(shù)作用域中的變量浮定,而且有權(quán)訪問其父環(huán)境相满,乃至全局執(zhí)行環(huán)境;
- 變量的執(zhí)行環(huán)境有助于確定應(yīng)該何時(shí)釋放內(nèi)存桦卒;
javascript是一門具有自動(dòng)垃圾回收機(jī)制的編程語言立美,開發(fā)人員不必關(guān)心內(nèi)存分配和回收問題。以下有關(guān)回收的總結(jié):
- 離開作用域的值被自動(dòng)標(biāo)記為可以回收方灾,因此將在垃圾收集期間刪除建蹄;
-
標(biāo)記清除
是目前最流行的垃圾回收算法,這種算法的思想是給當(dāng)前不使用的值加上標(biāo)記,然后再回收躲撰; - 另外一種垃圾收集算法是
引用計(jì)數(shù)
针贬,這種算法的思想是跟蹤記錄所有值被引用的次數(shù)击费,IE舊版本使用這種算法拢蛋; - 當(dāng)代碼存在循環(huán)引用的時(shí)候,
引用計(jì)數(shù)
算法就會(huì)導(dǎo)致問題蔫巩; - 接觸變量的引用(
x = null
)不僅有助于消除循環(huán)引用現(xiàn)象谆棱,對(duì)垃圾回收也有好處。為了確保有效的回收內(nèi)存圆仔,應(yīng)該及時(shí)解除不再使用的全局對(duì)象垃瞧、全局對(duì)象屬性以及循環(huán)變量的引用。
參考:
理解Javascript_15_作用域分配與變量訪問規(guī)則,再送個(gè)閉包
前端基礎(chǔ)進(jìn)階(一):內(nèi)存空間詳細(xì)圖解
前端基礎(chǔ)進(jìn)階(二):執(zhí)行上下文詳細(xì)圖解