第四章:變量、作用域和內(nèi)存問(wèn)題
本章內(nèi)容:
- 理解基本類型和引用類型
- 理解執(zhí)行環(huán)境
- 理解垃圾回收機(jī)制
4.1 基本類型和引用類型
ECMAScript中的變量包含兩種不同類型的值: 基本類型值和引用類型值
基本類型有:Undefined、Null、Boolean、Number、String。
這五種數(shù)據(jù)類型是按值訪問(wèn)的。
引用類型的值是保存在內(nèi)存中的對(duì)象。 javascript不允許直接訪問(wèn)內(nèi)存位置。
引用類型的值按引用訪問(wèn)的。
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
我們通過(guò)var obj1 = obj2
執(zhí)行一次復(fù)制引用類型的操作。引用類型的復(fù)制同樣也會(huì)為新的變量自動(dòng)分配一個(gè)新的值保存在變量對(duì)象中,但不同的是,這個(gè)新的值,僅僅只是引用類型的一個(gè)地址指針。當(dāng)?shù)刂分羔樝嗤瑫r(shí),盡管他們相互獨(dú)立,但是在變量對(duì)象中訪問(wèn)到的具體對(duì)象實(shí)際上是同一個(gè)。如圖所示。
因此當(dāng)我改變obj1時(shí),obj2也發(fā)生了變化。這就是引用類型的特性。
4.1.3 傳遞參數(shù)
ECMAScript中所有函數(shù)的參數(shù)均是按值傳遞。也就是說(shuō),會(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ì)象。但是,訪問(wèn)person.name的時(shí)候顯示仍然是zhangzhuo。說(shuō)明對(duì)象是傳值而非傳遞引用。
4.1.4 檢測(cè)類型
檢測(cè)一個(gè)基本類型可以用typeof
:
檢測(cè)引用類型(判斷變量是什么類型的對(duì)象),ECMAScript提供了instanceof
(原理:根據(jù)原型鏈來(lái)識(shí)別)。用法:
result = variable instanceof constructor; // 返回值 true or false
// eg:
alert(person instanceof Object); //變量person是Object嗎
alert(colors instanceof Array); //變量colors是Array嗎
如果使用instanceof檢測(cè)基本類型,會(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ǔ)在棧中的簡(jiǎn)單數(shù)據(jù)段,也就是說(shuō),他們的值直接存儲(chǔ)在變量訪問(wèn)的位置。
堆是存放數(shù)據(jù)的一種離散數(shù)據(jù)結(jié)構(gòu),在javascript中,引用值是存放在堆中的。
那為什么引用值要放在堆中,而原始值要放在棧中,不都是在內(nèi)存中嗎,為什么不放在一起呢?那接下來(lái),讓我們來(lái)探索問(wèn)題的答案!
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);
然后我們來(lái)看一下內(nèi)存分析圖:
變量num,bol,str為基本數(shù)據(jù)類型,它們的值,直接存放在棧中,obj,person,arr為復(fù)合數(shù)據(jù)類型,他們的引用變量存儲(chǔ)在棧中,指向于存儲(chǔ)在堆中的實(shí)際對(duì)象。
由上圖可知,我們無(wú)法直接操縱堆中的數(shù)據(jù),也就是說(shuō)我們無(wú)法直接操縱對(duì)象,但我們可以通過(guò)棧中對(duì)對(duì)象的引用來(lái)操作對(duì)象,就像我們通過(guò)遙控機(jī)操作電視機(jī)一樣,區(qū)別在于這個(gè)電視機(jī)本身并沒(méi)有控制按鈕。
現(xiàn)在讓我們來(lái)回答為什么引用值要放在堆中,而原始值要放在棧中的問(wèn)題:
記住一句話:能量是守衡的,無(wú)非是時(shí)間換空間,空間換時(shí)間的問(wèn)題
堆比棧大,棧比堆的運(yùn)算速度快,對(duì)象是一個(gè)復(fù)雜的結(jié)構(gòu),并且可以自由擴(kuò)展,如:數(shù)組可以無(wú)限擴(kuò)充,對(duì)象可以自由添加屬性。將他們放在堆中是為了不影響棧的效率。而是通過(guò)引用的方式查找到堆中的實(shí)際對(duì)象再進(jìn)行操作。相對(duì)于簡(jiǎn)單數(shù)據(jù)類型而言,簡(jiǎn)單數(shù)據(jù)類型就比較穩(wěn)定,并且它只占據(jù)很小的內(nèi)存。不將簡(jiǎn)單數(shù)據(jù)類型放在堆是因?yàn)橥ㄟ^(guò)引用到堆中查找實(shí)際對(duì)象是要花費(fèi)時(shí)間的,而這個(gè)綜合成本遠(yuǎn)大于直接從棧中取得實(shí)際值的成本。所以簡(jiǎn)單數(shù)據(jù)類型的值直接存放在棧中。
4.2 執(zhí)行環(huán)境和作用域
執(zhí)行函數(shù)
執(zhí)行環(huán)境(execution context, 有的地方也翻譯為執(zhí)行上下文)是javascript中最重要的一個(gè)概念。執(zhí)行環(huán)境定義了變量或者函數(shù)有權(quán)訪問(wè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)行起來(lái)會(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ì)以棧的方式來(lái)處理它們,這個(gè)棧,我們稱其為函數(shù)調(diào)用棧(call stack)。棧底永遠(yuǎn)都是全局環(huán)境,而棧頂就是當(dāng)前正在執(zhí)行的環(huán)境。
當(dāng)代碼在執(zhí)行過(guò)程中,遇到以上三種情況,都會(huì)生成一個(gè)執(zhí)行環(huán)境,放入棧中,而處于棧頂?shù)沫h(huán)境執(zhí)行完畢之后,就會(huì)自動(dòng)出棧。為了更加清晰的理解這個(gè)過(guò)程,根據(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來(lái)表示處理執(zhí)行環(huán)境的的堆棧。我們很容易知道,第一步,首先是全局環(huán)境入棧。
全局環(huán)境入棧之后,其中的可執(zhí)行代碼開(kāi)始執(zhí)行,直到遇到了changeColor()
,這一句激活函數(shù)changeColor
創(chuàng)建它自己的執(zhí)行環(huán)境,因此第二步就是changeColor的執(zhí)行環(huán)境入棧。
changeColor的環(huán)境入棧之后,控制器開(kāi)始執(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í)行代碼,也沒(méi)有再遇到其他執(zhí)行環(huán)境,順利執(zhí)行完畢之后彈出。這樣,ECStack中就只身下全局環(huán)境了。
全局上下文在瀏覽器窗口關(guān)閉后出棧。
詳細(xì)了解了這個(gè)過(guò)程之后,我們就可以對(duì)執(zhí)行上下文總結(jié)一些結(jié)論了。
- js是單線程的;
- 同步執(zhí)行,只有棧頂?shù)沫h(huán)境處于執(zhí)行中,其他上下文需要等待
- 全局環(huán)境只有唯一的一個(gè),它在瀏覽器關(guān)閉時(shí)出棧
- 函數(shù)的執(zhí)行環(huán)境的個(gè)數(shù)沒(méi)有限制
- 每次某個(gè)函數(shù)被調(diào)用,就會(huì)有個(gè)新的執(zhí)行環(huán)境為其創(chuàng)建,即使是調(diào)用的自身函數(shù),也是如此。
為了鞏固一下執(zhí)行環(huán)境的理解,我們?cè)賮?lái)繪制一個(gè)例子的演變過(guò)程,這是一個(gè)簡(jiǎn)單的閉包例子。
function f1(){
var n=999;
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
因?yàn)閒1中的函數(shù)f2在f1的可執(zhí)行代碼中,并沒(méi)有被調(diào)用執(zhí)行,因此執(zhí)行f1時(shí),f2不會(huì)創(chuàng)建新的上下文,而直到result執(zhí)行時(shí),才創(chuàng)建了一個(gè)新的。具體演變過(guò)程如下。 (入棧相當(dāng)于要執(zhí)行代碼)
作用域和作用域鏈
作用域:
- 在JavaScript中,我們可以將作用域定義為一套規(guī)則,這套規(guī)則用來(lái)管理引擎如何在當(dāng)前作用域以及嵌套的子作用域中根據(jù)標(biāo)識(shí)符名稱進(jìn)行變量查找。
- 作用域與執(zhí)行環(huán)境是完全不同的兩個(gè)概念。我知道很多人會(huì)混淆他們,但是一定要仔細(xì)區(qū)分。
- JavaScript中只有全局作用域與函數(shù)作用域(因?yàn)閑val我們平時(shí)開(kāi)發(fā)中幾乎不會(huì)用到它,這里不討論)。
JavaScript代碼的整個(gè)執(zhí)行過(guò)程,分為兩個(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ì)符合訪問(wèn)權(quán)限的變量和函數(shù)的有序訪問(wèn)。
當(dāng)代碼在一個(gè)環(huán)境中執(zhí)行的時(shí)候,會(huì)創(chuàng)建變量對(duì)象和一個(gè)作用域鏈(scope chain)。是保證對(duì)執(zhí)行環(huán)境有權(quán)訪問(wèn)所有變量和函數(shù)的有序訪問(wèn)。作用域鏈的前端,始終是當(dāng)前的執(zhí)行環(huán)境的變量對(duì)象。如果這個(gè)環(huán)境是函數(shù),則將其變量對(duì)象(activation object)作為活動(dòng)對(duì)象。變量對(duì)象最開(kāi)始只包含一個(gè)變量,即arguments對(duì)象(這個(gè)對(duì)象在全局環(huán)境中是不存在的)。
標(biāo)識(shí)符的解析是沿著作用域鏈一級(jí)一級(jí)地搜索標(biāo)識(shí)符的過(guò)程。搜索過(guò)程始終是從作用域鏈的前端開(kāi)始。
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)部訪問(wèn)變量color,就是因?yàn)榭梢栽谧饔糜蜴溦业剿?/p>
延伸閱讀3: 作用域與作用域鏈
在訪問(wèn)一個(gè)變量的時(shí)候,就必須存在一個(gè)可見(jiàn)性的問(wèn)題,這就是作用域。更深入的說(shuō),當(dāng)訪問(wèn)一個(gè)變量或者調(diào)用一個(gè)函數(shù)的時(shí)候,javaScript引擎將不同執(zhí)行位置上的變量對(duì)象按照規(guī)則構(gòu)建一個(gè)鏈表。在訪問(wèn)一個(gè)變量的時(shí)候,先從鏈表的第一個(gè)變量對(duì)象中查找,如果沒(méi)有則在第二個(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ì)開(kāi)始執(zhí)行代碼,這個(gè)時(shí)候,會(huì)完成變量賦值,函數(shù)引用,以及執(zhí)行其他代碼。
變量對(duì)象(Variable Object)
變量對(duì)象的創(chuàng)建,依次經(jīng)歷了以下幾個(gè)過(guò)程。
- 建立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ì)直接跳過(guò),原屬性值不會(huì)被修改。
許多讀者在閱讀到這的時(shí)候會(huì)因?yàn)橄旅娴倪@樣場(chǎng)景對(duì)于“跳過(guò)”一詞產(chǎn)生疑問(wèn)。既然變量聲明的foo遇到函數(shù)聲明的foo會(huì)跳過(guò),可是為什么最后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)建過(guò)程。也就是執(zhí)行環(huán)境的創(chuàng)建過(guò)程。而foo = 20
是在執(zhí)行環(huán)境的執(zhí)行過(guò)程中運(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ì)跳過(guò)undefined的賦值
// var foo = undefined;
// 然后開(kāi)始執(zhí)行階段代碼的執(zhí)行
console.log(foo); // function foo
foo = 20;
根據(jù)這個(gè)規(guī)則,理解變量提升就變得十分簡(jiǎn)單了。
在上面的規(guī)則中我們看出,function聲明會(huì)比var聲明優(yōu)先級(jí)更高一點(diǎn)。為了幫助大家更好的理解變量對(duì)象,我們結(jié)合一些簡(jiǎn)單的例子來(lái)進(jìn)行探討。
// demo01
function test() {
console.log(a);
console.log(foo());
var a = 1;
function foo() {
return 2;
}
}
test();
在上例中,我們直接從test()的執(zhí)行環(huán)境開(kāi)始理解。全局作用域中運(yùn)行test()
時(shí),test()的執(zhí)行上下文開(kāi)始創(chuàng)建。為了便于理解,我們用如下的形式來(lái)表示
// 創(chuàng)建過(guò)程
testEC = {
// 變量對(duì)象
VO: {},
scopeChain: {}
}
// 因?yàn)楸疚臅簳r(shí)不詳細(xì)解釋作用域鏈,所以把變量對(duì)象專門提出來(lái)說(shuō)明
// VO 為 Variable Object的縮寫,即變量對(duì)象
VO = {
arguments: {...}, //注:在瀏覽器的展示中,函數(shù)的參數(shù)可能并不是放在arguments對(duì)象中,這里為了方便理解,我做了這樣的處理
foo: <foo reference> // 表示foo的地址引用
a: undefined
}
未進(jìn)入執(zhí)行階段之前,變量對(duì)象中的屬性都不能訪問(wèn)!但是進(jìn)入執(zhí)行階段之后,變量對(duì)象轉(zhuǎn)變?yōu)榱嘶顒?dòng)對(duì)象,里面的屬性都能被訪問(wèn)了,然后開(kāi)始進(jìn)行執(zhí)行階段的操作。
這樣,如果再面試的時(shí)候被問(wèn)到變量對(duì)象和活動(dòng)對(duì)象有什么區(qū)別,就又可以自如的應(yīng)答了,他們其實(shí)都是同一個(gè)對(duì)象,只是處于執(zhí)行環(huán)境的不同生命周期。不過(guò)只有處于函數(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();
再來(lái)一個(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ì)跳過(guò)而不會(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ì)符合訪問(wèn)權(quán)限的變量和函數(shù)的有序訪問(wèn)。
為了幫助大家理解作用域鏈,我我們先結(jié)合一個(gè)例子,以及相應(yīng)的圖示來(lái)說(shuō)明。
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ù)組來(lái)表示作用域鏈,數(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)講過(guò),因此圖中使用了AO來(lái)表示。Active Object
是的,作用域鏈?zhǔn)怯梢幌盗凶兞繉?duì)象組成,我們可以在這個(gè)單向通道中,查詢變量對(duì)象中的標(biāo)識(shí)符,這樣就可以訪問(wèn)到上一層作用域中的變量了。
小結(jié):
javascript變量可以保存兩種類型的值:基本類型值與引用類型值?;绢愋偷闹翟从谝韵挛宸N基本數(shù)據(jù)類型:Undefined、Null、Boolean、Number、String?;绢愋偷闹蹬c引用類型的值具有以下的特點(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)境決定了變量的生命周期,以及哪一部分代碼可以訪問(wè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ì)象;
- 通過(guò)作用域鏈,函數(shù)中的執(zhí)行環(huán)境不僅能夠訪問(wèn)函數(shù)作用域中的變量,而且有權(quán)訪問(wèn)其父環(huán)境,乃至全局執(zhí)行環(huán)境;
- 變量的執(zhí)行環(huán)境有助于確定應(yīng)該何時(shí)釋放內(nèi)存;
javascript是一門具有自動(dòng)垃圾回收機(jī)制的編程語(yǔ)言,開(kāi)發(fā)人員不必關(guān)心內(nèi)存分配和回收問(wèn)題。以下有關(guān)回收的總結(jié):
- 離開(kāi)作用域的值被自動(dòng)標(biāo)記為可以回收,因此將在垃圾收集期間刪除;
-
標(biāo)記清除
是目前最流行的垃圾回收算法,這種算法的思想是給當(dāng)前不使用的值加上標(biāo)記,然后再回收; - 另外一種垃圾收集算法是
引用計(jì)數(shù)
,這種算法的思想是跟蹤記錄所有值被引用的次數(shù),IE舊版本使用這種算法; - 當(dāng)代碼存在循環(huán)引用的時(shí)候,
引用計(jì)數(shù)
算法就會(huì)導(dǎo)致問(wèn)題; - 接觸變量的引用(
x = null
)不僅有助于消除循環(huán)引用現(xiàn)象,對(duì)垃圾回收也有好處。為了確保有效的回收內(nèi)存,應(yīng)該及時(shí)解除不再使用的全局對(duì)象、全局對(duì)象屬性以及循環(huán)變量的引用。
參考:
理解Javascript_15_作用域分配與變量訪問(wèn)規(guī)則,再送個(gè)閉包
前端基礎(chǔ)進(jìn)階(一):內(nèi)存空間詳細(xì)圖解
前端基礎(chǔ)進(jìn)階(二):執(zhí)行上下文詳細(xì)圖解