1.1 基本類型和引用類型的值
ECMAScript變量可能包含兩種不同數(shù)據(jù)類型的值:基本類型值和引用類型值。基本類型值指的是簡單的數(shù)據(jù)段,而引用類型值指那些可能由多個值構成的對象。
1.1.1 動態(tài)的屬性
對于引用類型的值,我們可以為其添加屬性和方法,也可以改變和刪除其屬性和方法。
1.1.2 復制變量值
如果從一個變量向另一個變量復制基本類型的值,會在變量對象上創(chuàng)建一個新值,然后把該值復制到為新變量分配的位置上。
當從一個變量向另一個變量復制引用類型的值時,同樣也會將儲存在變量對象中的值復制一份放到為新變量分配的空間中。不同的是,這個值的副本實際上是一個指針,而這個指針指向存儲在堆中的一個對象。復制操作結束后,兩個變量實際上將引用用一個對象。因此,改變其中一個變量,就會影響另一個變量。
1.1.3 傳遞參數(shù)
ECMAScript中所有函數(shù)的參數(shù)都是按值傳遞的。也就是說,把函數(shù)外部的值復制給函數(shù)內(nèi)部的參數(shù),就和把值從一個變量復制到另一個變量一樣。
在向參數(shù)傳遞基本類型的值時,被傳遞的值會復制給一個局部變量(即命名參數(shù),或者用ECMAScript的概念來說,就是arguments對象中的一個元素)。在向參數(shù)傳遞引用類型的值時,會把這個值在內(nèi)存中的地址復制給一個局部變量,因此這個局部變量的變化會反映在函數(shù)的外部。
使用數(shù)值等基本類型值來說明按值傳遞參數(shù)比較簡單,但如果使用對象,那問題就不怎么好理解了。請看以下例子:
function setName(obj) {
obj.name = "Nicholas";
}
var person = new Object();
setName(person);
alert(person.name); // "Nicholas"
以上代碼創(chuàng)建一個對象,并將其保存在變量person中。然后,這個變量被傳遞到setName()函數(shù)中之后就被復制給了obj。在這個函數(shù)內(nèi)部,obj和person引用的是同一個對象。換句話說,即使這個變量是按值傳遞的,obj也會按引用來訪問同一個對象。于是,當在函數(shù)內(nèi)部為obj添加name屬性后,函數(shù)外部的person也將有所反映;因為person指向的對象在堆內(nèi)存中只有一個,而且是全局對象。有很多開發(fā)人員錯誤地認為:在局部作用域中修改的對象會在全局作用域中反映出來,就說明參數(shù)是按引用傳遞的。為了證明對象是按值傳遞,下面將舉一個經(jīng)過修改的例子:
function setName(obj) {
obj.name = "Nicholas";
obj = new Object();
obj.name = "Greg";
}
var person = new Object();
setName(person);
alert(person.name); // "Nicholas"
如果以上修改的例子中,person是按引用傳遞的,那么person就會自動被修改為指向其name屬性值為"Greg"的新對象。但是,當接下來再訪問person.name時,顯示的值仍然是"Nicholas“。這說明即使在函數(shù)內(nèi)部修改了參數(shù)的值,但原始的引用仍然保持未變。實際上,當在函數(shù)內(nèi)部重寫obj時,這個變量的引用就是一個局部對象了。而這個局部對象會在函數(shù)執(zhí)行完畢后立即被銷毀。
1.1.4 檢測類型
在檢測基本數(shù)據(jù)類型的時候,可以使用typeof操作符。但是,在檢測引用類型的值時,這個操作符的用處不大。通常,我們并不是想知道某個值是對象,而是想知道它是什么類型的對象。對此,ECMAScript提供了instanceof操作符,其語法如下所示:
result = variable instanceof constructor
如果變量是給定引用類型的實例,那么instanceof操作符就會返回true。根據(jù)規(guī)定,所有引用類型的值都是Object的實例。因此,在檢測一個引用類型值和Object構造函數(shù)時,instanceof操作符始終返回true。當然,如果使用instanceof操作符檢測基本類型的值,則該操作符始終會返回false,因為基本類型不是對象。
1.2 執(zhí)行環(huán)境及作用域
1.2.1 執(zhí)行環(huán)境
執(zhí)行環(huán)境(execution context,EC),又稱執(zhí)行上下文,定義了變量或函數(shù)有權訪問的其他數(shù)據(jù),決定了它們各自的行為。
JavaScript中執(zhí)行環(huán)境可以分為三種:
- Global Code(默認的代碼執(zhí)行環(huán)境)
- Function Code(函數(shù)被調用時,函數(shù)體中運行的代碼)
- Eval Code(在Eval函數(shù)內(nèi)運行的代碼)
執(zhí)行環(huán)境可以認為是一個對象,大體結構如下:
EC = {
VariableObject: /* arguments, vars, function declaration */ ,
ScopeChain: /* variable object, all parent scopes */ ,
this: /* context object */
}
1.2.2 執(zhí)行環(huán)境棧
一系列活動的執(zhí)行環(huán)境從邏輯上形成一個執(zhí)行環(huán)境棧。棧底總是全局環(huán)境,棧頂是當前(活動的)執(zhí)行環(huán)境。下面是執(zhí)行環(huán)境棧的抽象視圖:
當瀏覽器首次載入腳本時,它將默認進入全局執(zhí)行上下文(在Web瀏覽器中,全局執(zhí)行環(huán)境被認為是window對象)。如果執(zhí)行流進入一個函數(shù)時,將會創(chuàng)建一個新的執(zhí)行環(huán)境并且將它壓入一個環(huán)境棧中。而在函數(shù)執(zhí)行結束之后,棧將其環(huán)境彈出,把控制權返回給之前的執(zhí)行環(huán)境。
1.2.3 變量對象(variable object,VO)
每個執(zhí)行環(huán)境都有一個與之關聯(lián)的變量對象,環(huán)境中定義的所有變量和函數(shù)都保存在這個環(huán)境中。但是,VO不包含以下兩種情況:
- 函數(shù)表達式不在VO中
- 函數(shù)的執(zhí)行環(huán)境中,沒有使用var聲明的變量(這種變量是"全局"的聲明方式,并不在與該函數(shù)的執(zhí)行環(huán)境與之相聯(lián)的VO中)
VO分為全局上下文VO(全局對象,Global object)和函數(shù)上下文的AO。
- 進入執(zhí)行上下文時,VO的初始化過程具體如下(該過程是有先后順序的):
- 函數(shù)的形參(arguments,當進入函數(shù)執(zhí)行環(huán)境時)——變量對象的一個屬性,其屬性名就是形參的名字,其值就是實參的值;對于沒有傳遞的參數(shù),其值為
undefined
; - 函數(shù)聲明(FD,F(xiàn)unctionDeclaration) —— 變量對象的一個屬性,其屬性名和值都是函數(shù)對象創(chuàng)建出來的;如果變量對象已經(jīng)包含了相同名字的屬性,則完全替換它的值;
- 變量聲明(var,VariableDeclaration) —— 變量對象的一個屬性,其屬性名即為變量名,其值為undefined;如果變量名和已經(jīng)聲明的函數(shù)名或者函數(shù)的參數(shù)名相同,則不會影響已經(jīng)存在的屬性。
- 執(zhí)行代碼階段時,VO中的一些屬性undefined值將會確定。
1.2.4 活動對象(activation object,AO)
在函數(shù)的執(zhí)行上下文中,VO是不能直接訪問的,此時由活動對象(activation object)扮演VO的角色。AO是在進入函數(shù)的執(zhí)行上下文時創(chuàng)建的,通過arguments對象初始化。
AO = {
arguments: {
callee: ,
length: ,
properties-indexes: //函數(shù)傳參參數(shù)值
}
};
當然,AO也同時擁有VO的屬性。
1.2.5 作用域鏈(scope chain)
當代碼在一個環(huán)境中執(zhí)行時,會創(chuàng)建變量對象的一個作用域鏈。作用域鏈的用途,是保證對執(zhí)行環(huán)境有權訪問的所有變量和函數(shù)的有序訪問。作用域鏈的前端,始終都是當前執(zhí)行的代碼所在環(huán)境的變量對象。如果這個環(huán)境是函數(shù),則將其活動對象作為變量對象。
作用域鏈中的下一個變量對象來自包含(外部)環(huán)境,而再下一個變量對象則來自下一個包含環(huán)境。這樣,一直延續(xù)到全局執(zhí)行環(huán)境;全局執(zhí)行環(huán)境的變量對象始終都是作用域鏈中的最后一個對象。
標識符(可以認為是變量,函數(shù)聲明或者函數(shù)中的參數(shù))解析是沿著作用域鏈一級一級地搜索標識符的過程。搜索過程始終從作用域鏈的前端開始,然后逐級地向后回溯,直至找到標識符為止(如果找不到標識符,通常會導致錯誤發(fā)生)。
1.2.6 this
this是執(zhí)行上下文的一個屬性,不是某個變量對象的屬性。this的值直接從執(zhí)行上下文中獲取,而不會從作用域鏈中搜尋。也就是說this的值只取決于進入上下文時的情況。所以,this是不允許賦值的。
1.3 執(zhí)行上下文創(chuàng)建
JavaScript解釋器創(chuàng)建執(zhí)行上下文的兩個階段:
- 創(chuàng)建階段(函數(shù)被調用,但是在開始執(zhí)行函數(shù)代碼之前):
- 初始化作用域鏈
- 創(chuàng)建VO/AO(詳情見VO的初始化過程)
- 設置this的值
- 激活/代碼執(zhí)行階段:
- 在當前上下文執(zhí)行/解釋函數(shù)代碼,并隨著代碼一行行執(zhí)行設置變量的值
示例代碼:
function foo(i) {
var a = 'hello';
var b = function PrivateB() {};
function c() {}
}
foo(22);
針對上述代碼,創(chuàng)建階段得到AO:
fooExecutionContext = {
ScopeChain: { ... },
VariableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c(),
a: undefined,
b: undefined
},
this: { ... }
}
激活階段,AO被更新:
fooExecutionContext = {
ScopeChain: { ... },
VariableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c(),
a: 'hello',
b: pointer to function PrivateB()
},
this: { ... }
}
1.4 延長作用域鏈
有些語句可以在作用域鏈的前端臨時增加一個變量對象,該變量對象會在代碼執(zhí)行后被移除。在兩種情況下會發(fā)生這種現(xiàn)象。具體來說,就是當執(zhí)行流進入下列任何一個語句時,作用域鏈就會得到加長:
- try-catch語句的catch塊
- with語句
這兩個語句都會在作用域鏈的前端添加一個變量對象。對with語句來說,會將指定的對象添加到作用域鏈中。對catch語句來說,會創(chuàng)建一個新的變量對象,其中包含的是被拋出的錯誤對象的聲明。
1.5 沒有塊級作用域
在其他類C的語言中,由花括號封閉的代碼塊都有自己的作用域(如果用ECMAScript的話來講,就是它們自己的執(zhí)行環(huán)境),因而支持根據(jù)條件來定義變量。例如,下面的代碼在JavaScript中并不會得到想象中的結果:
if (true) {
var color = "blue";
}
alert(color); //"blue"
在JavaScript中,if語句中的變量聲明會將變量添加到當前的執(zhí)行環(huán)境(在這里是全局環(huán)境)中。在使用for語句時尤其要牢記這一差異,例如:
for (var i = 0; i < 10; i++) {
doSomething(i);
}
alert(i); //10
1.6 垃圾收集
JavaScript具有自動垃圾收集機制,也就是說,執(zhí)行環(huán)境會負責代碼執(zhí)行過程中使用的內(nèi)存。這種垃圾收集機制的原理其實很簡單:找出那些不再繼續(xù)使用的變量,然后釋放其占用的內(nèi)存。為此,垃圾收集器會按照固定的時間間隔(或代碼執(zhí)行中預定的收集時間),周期性地執(zhí)行這一操作。
下面我們來分析一下函數(shù)中局部變量的正常生命周期。局部變量只在函數(shù)執(zhí)行的過程中存在。而在這個過程中,會為局部變量在棧(或堆)內(nèi)存上分配相應的空間,以便儲存它們的值。然后再函數(shù)中使用這些變量,直至函數(shù)執(zhí)行結束。此時,局部變量就沒有存在的必要了,因此可以釋放它們的內(nèi)存以供將來使用。垃圾收集器必須跟蹤哪個變量有用哪個變量沒用,對于不再有用的變量打上標記,以備將來收回其占用的內(nèi)存。用于標識無用變量的策略可能會因實現(xiàn)而異,但具體到瀏覽器中的實現(xiàn),則通常有兩個策略。
1.6.1 標記清除
JavaScript中最常用的垃圾收集方式是標記清除(mark-and-sweep)。當變量進入環(huán)境(例如,在函數(shù)中聲明一個變量)時,就將這個變量標記為“進入環(huán)境”。從邏輯上講,永遠不能釋放進入環(huán)境的變量所占用的內(nèi)存,因為只要執(zhí)行流進入相應的環(huán)境,就可能會用到它們。而當變量離開環(huán)境時,則將其標記為“離開環(huán)境”。
垃圾收集器在運行的時候會給儲存在內(nèi)存中的所有變量都加上標記(當然,可以使用任何標記方式)。然后,它會去掉環(huán)境中的變量以及被環(huán)境中的變量引用的變量的標記。而在此之后再被加上標記的變量將被視為準備刪除的變量,原因是環(huán)境中的變量已經(jīng)無法訪問到這些變量了。最后,垃圾收集器完成內(nèi)存清除工作,銷毀那些帶標記的值并回收它們所占用的內(nèi)存空間。
1.6.2 引用計數(shù)
另一種不太常見的垃圾收集策略叫做引用計數(shù)(reference counting)。引用計數(shù)的含義是跟蹤記錄每個值被引用的次數(shù)。當聲明了一個變量并將一個引用類型值賦給該變量時,則這個值的引用次數(shù)就是1。相反,如果包含對這個值引用的變量又取得了另外一個值,則這個值的引用次數(shù)減1。當這個值的引用次數(shù)變成0時,則說明沒有辦法再訪問這個值了,因而就可以將其占用的內(nèi)存空間回收起來。這樣,當垃圾收集器下次再運行時,它就會釋放那些引用次數(shù)為零的值所占用的內(nèi)存。
Netscape Navigator 3.0是最早使用引用計數(shù)策略的瀏覽器,但很快它就遇到了一個嚴重的問題:循環(huán)引用。循環(huán)引用指的是對象A中包含一個指向對象B的指針,而對象B中也包含一個指向對象A的引用。請看下面的例子:
function problem() {
var objectA = new Object();
var objectB = new Object();
objectA.someOtherObject = objectB;
objectB.anotherObject = objectA;
}
在這個例子中,objectA和objectB通過各自的屬性相互引用;也就是說,這兩個對象的引用次數(shù)都是2。在采用標記清除策略的實現(xiàn)中,由于函數(shù)執(zhí)行之后,這兩個對象都離開了作用域,因此這種相互引用不是個問題。但在采用引用計數(shù)策略的實現(xiàn)中,當函數(shù)執(zhí)行完畢后,objectA和objectB還將繼續(xù)存在,因為它們的引用次數(shù)永遠不會是0。假如這個函數(shù)被重復多次調用,就會導致大量內(nèi)存得不到回收。為此,Netscape Navigator 4.0中放棄了引用計數(shù)方式,轉而采用標記清除來實現(xiàn)其垃圾收集機制。
1.7 管理內(nèi)存
優(yōu)化內(nèi)存占用的最佳方式,就是為執(zhí)行中的代碼保存必要的數(shù)據(jù)。一旦數(shù)據(jù)不再有用,最好通過將其值設置為null來釋放其引用——這個做法叫做解除引用(dereferencing)。這一做法適用于大多數(shù)全局變量和全局對象的屬性。
不過,解除一個值的引用并不意味著自動回收該值所占用的內(nèi)存。解除引用的真正作用是讓值脫離執(zhí)行環(huán)境,以便垃圾收集器下次運行時將其回收。