變量羽圃、作用域和內(nèi)存問題

1.1 基本類型和引用類型的值

ECMAScript變量可能包含兩種不同數(shù)據(jù)類型的值:基本類型值引用類型值乾胶。基本類型值指的是簡單的數(shù)據(jù)段,而引用類型值指那些可能由多個值構(gòu)成的對象朽寞。

1.1.1 動態(tài)的屬性

對于引用類型的值识窿,我們可以為其添加屬性和方法,也可以改變和刪除其屬性和方法脑融。

1.1.2 復(fù)制變量值

如果從一個變量向另一個變量復(fù)制基本類型的值喻频,會在變量對象上創(chuàng)建一個新值,然后把該值復(fù)制到為新變量分配的位置上肘迎。
  當(dāng)從一個變量向另一個變量復(fù)制引用類型的值時甥温,同樣也會將儲存在變量對象中的值復(fù)制一份放到為新變量分配的空間中。不同的是妓布,這個值的副本實際上是一個指針姻蚓,而這個指針指向存儲在堆中的一個對象。復(fù)制操作結(jié)束后秋茫,兩個變量實際上將引用用一個對象史简。因此,改變其中一個變量肛著,就會影響另一個變量圆兵。

1.1.3 傳遞參數(shù)

ECMAScript中所有函數(shù)的參數(shù)都是按值傳遞的。也就是說枢贿,把函數(shù)外部的值復(fù)制給函數(shù)內(nèi)部的參數(shù)殉农,就和把值從一個變量復(fù)制到另一個變量一樣。
  在向參數(shù)傳遞基本類型的值時局荚,被傳遞的值會復(fù)制給一個局部變量(即命名參數(shù)超凳,或者用ECMAScript的概念來說,就是arguments對象中的一個元素)耀态。在向參數(shù)傳遞引用類型的值時轮傍,會把這個值在內(nèi)存中的地址復(fù)制給一個局部變量,因此這個局部變量的變化會反映在函數(shù)的外部首装。
  使用數(shù)值等基本類型值來說明按值傳遞參數(shù)比較簡單创夜,但如果使用對象,那問題就不怎么好理解了仙逻。請看以下例子:

function setName(obj) {
    obj.name = "Nicholas";
}

var person  = new Object();
setName(person);
alert(person.name); // "Nicholas"

以上代碼創(chuàng)建一個對象驰吓,并將其保存在變量person中涧尿。然后,這個變量被傳遞到setName()函數(shù)中之后就被復(fù)制給了obj檬贰。在這個函數(shù)內(nèi)部姑廉,obj和person引用的是同一個對象。換句話說翁涤,即使這個變量是按值傳遞的桥言,obj也會按引用來訪問同一個對象。于是葵礼,當(dāng)在函數(shù)內(nèi)部為obj添加name屬性后限书,函數(shù)外部的person也將有所反映;因為person指向的對象在堆內(nèi)存中只有一個章咧,而且是全局對象。有很多開發(fā)人員錯誤地認(rèn)為:在局部作用域中修改的對象會在全局作用域中反映出來能真,就說明參數(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"的新對象。但是蝙泼,當(dāng)接下來再訪問person.name時程剥,顯示的值仍然是"Nicholas“。這說明即使在函數(shù)內(nèi)部修改了參數(shù)的值汤踏,但原始的引用仍然保持未變织鲸。實際上,當(dāng)在函數(shù)內(nèi)部重寫obj時溪胶,這個變量的引用就是一個局部對象了搂擦。而這個局部對象會在函數(shù)執(zhí)行完畢后立即被銷毀。

1.1.4 檢測類型

在檢測基本數(shù)據(jù)類型的時候哗脖,可以使用typeof操作符瀑踢。但是,在檢測引用類型的值時才避,這個操作符的用處不大橱夭。通常,我們并不是想知道某個值是對象桑逝,而是想知道它是什么類型的對象棘劣。對此,ECMAScript提供了instanceof操作符肢娘,其語法如下所示:

result = variable instanceof constructor

如果變量是給定引用類型的實例呈础,那么instanceof操作符就會返回true舆驶。根據(jù)規(guī)定,所有引用類型的值都是Object的實例而钞。因此沙廉,在檢測一個引用類型值和Object構(gòu)造函數(shù)時,instanceof操作符始終返回true臼节。當(dāng)然撬陵,如果使用instanceof操作符檢測基本類型的值,則該操作符始終會返回false网缝,因為基本類型不是對象巨税。


1.2 執(zhí)行環(huán)境及作用域

1.2.1 執(zhí)行環(huán)境

執(zhí)行環(huán)境(execution context,EC)粉臊,又稱執(zhí)行上下文草添,定義了變量或函數(shù)有權(quán)訪問的其他數(shù)據(jù),決定了它們各自的行為扼仲。

JavaScript中執(zhí)行環(huán)境可以分為三種:

  • Global Code(默認(rèn)的代碼執(zhí)行環(huán)境)
  • Function Code(函數(shù)被調(diào)用時远寸,函數(shù)體中運行的代碼)
  • Eval Code(在Eval函數(shù)內(nèi)運行的代碼)

執(zhí)行環(huán)境可以認(rèn)為是一個對象,大體結(jié)構(gòu)如下:

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)境驰后,棧頂是當(dāng)前(活動的)執(zhí)行環(huán)境。下面是執(zhí)行環(huán)境棧的抽象視圖:

執(zhí)行環(huán)境棧抽象視圖.png

當(dāng)瀏覽器首次載入腳本時矗愧,它將默認(rèn)進入全局執(zhí)行上下文(在Web瀏覽器中灶芝,全局執(zhí)行環(huán)境被認(rèn)為是window對象)。如果執(zhí)行流進入一個函數(shù)時唉韭,將會創(chuàng)建一個新的執(zhí)行環(huán)境并且將它壓入一個環(huán)境棧中夜涕。而在函數(shù)執(zhí)行結(jié)束之后,棧將其環(huán)境彈出属愤,把控制權(quán)返回給之前的執(zhí)行環(huán)境钠乏。

1.2.3 變量對象(variable object,VO)

每個執(zhí)行環(huán)境都有一個與之關(guān)聯(lián)的變量對象春塌,環(huán)境中定義的所有變量和函數(shù)都保存在這個環(huán)境中晓避。但是,VO不包含以下兩種情況:

  • 函數(shù)表達(dá)式不在VO中
  • 函數(shù)的執(zhí)行環(huán)境中只壳,沒有使用var聲明的變量(這種變量是"全局"的聲明方式俏拱,并不在與該函數(shù)的執(zhí)行環(huán)境與之相聯(lián)的VO中)

VO分為全局上下文VO(全局對象,Global object)和函數(shù)上下文的AO吼句。

  1. 進入執(zhí)行上下文時锅必,VO的初始化過程具體如下(該過程是有先后順序的):
  • 函數(shù)的形參(arguments,當(dāng)進入函數(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)存在的屬性。
  1. 執(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ù)值
    }
};

當(dāng)然点把,AO也同時擁有VO的屬性橘荠。

1.2.5 作用域鏈(scope chain)

當(dāng)代碼在一個環(huán)境中執(zhí)行時,會創(chuàng)建變量對象的一個作用域鏈郎逃。作用域鏈的用途哥童,是保證對執(zhí)行環(huán)境有權(quán)訪問的所有變量和函數(shù)的有序訪問。作用域鏈的前端褒翰,始終都是當(dāng)前執(zhí)行的代碼所在環(huán)境的變量對象贮懈。如果這個環(huán)境是函數(shù),則將其活動對象作為變量對象优训。
  作用域鏈中的下一個變量對象來自包含(外部)環(huán)境朵你,而再下一個變量對象則來自下一個包含環(huán)境。這樣揣非,一直延續(xù)到全局執(zhí)行環(huán)境抡医;全局執(zhí)行環(huán)境的變量對象始終都是作用域鏈中的最后一個對象。
  標(biāo)識符(可以認(rèn)為是變量早敬,函數(shù)聲明或者函數(shù)中的參數(shù))解析是沿著作用域鏈一級一級地搜索標(biāo)識符的過程忌傻。搜索過程始終從作用域鏈的前端開始大脉,然后逐級地向后回溯,直至找到標(biāo)識符為止(如果找不到標(biāo)識符水孩,通常會導(dǎo)致錯誤發(fā)生)镰矿。

1.2.6 this

this是執(zhí)行上下文的一個屬性,不是某個變量對象的屬性荷愕。this的值直接從執(zhí)行上下文中獲取衡怀,而不會從作用域鏈中搜尋。也就是說this的值只取決于進入上下文時的情況安疗。所以抛杨,this是不允許賦值的。


1.3 執(zhí)行上下文創(chuàng)建

JavaScript解釋器創(chuàng)建執(zhí)行上下文的兩個階段

  1. 創(chuàng)建階段(函數(shù)被調(diào)用荐类,但是在開始執(zhí)行函數(shù)代碼之前):
  • 初始化作用域鏈
  • 創(chuàng)建VO/AO(詳情見VO的初始化過程)
  • 設(shè)置this的值
  1. 激活/代碼執(zhí)行階段:
  • 在當(dāng)前上下文執(zhí)行/解釋函數(shù)代碼怖现,并隨著代碼一行行執(zhí)行設(shè)置變量的值

示例代碼:

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)象吊输。具體來說饶号,就是當(dāng)執(zhí)行流進入下列任何一個語句時,作用域鏈就會得到加長:

  • try-catch語句的catch塊
  • with語句

這兩個語句都會在作用域鏈的前端添加一個變量對象季蚂。對with語句來說茫船,會將指定的對象添加到作用域鏈中。對catch語句來說扭屁,會創(chuàng)建一個新的變量對象算谈,其中包含的是被拋出的錯誤對象的聲明。


1.5 沒有塊級作用域

在其他類C的語言中料滥,由花括號封閉的代碼塊都有自己的作用域(如果用ECMAScript的話來講然眼,就是它們自己的執(zhí)行環(huán)境),因而支持根據(jù)條件來定義變量葵腹。例如高每,下面的代碼在JavaScript中并不會得到想象中的結(jié)果:

if (true) {
    var color = "blue";
}

alert(color);  //"blue"

在JavaScript中,if語句中的變量聲明會將變量添加到當(dāng)前的執(zhí)行環(huán)境(在這里是全局環(huán)境)中践宴。在使用for語句時尤其要牢記這一差異觉义,例如:

for (var i  = 0; i < 10; i++) {
    doSomething(i);
}

alert(i); //10

1.6 垃圾收集

JavaScript具有自動垃圾收集機制,也就是說浴井,執(zhí)行環(huán)境會負(fù)責(zé)代碼執(zhí)行過程中使用的內(nèi)存晒骇。這種垃圾收集機制的原理其實很簡單:找出那些不再繼續(xù)使用的變量,然后釋放其占用的內(nèi)存。為此洪囤,垃圾收集器會按照固定的時間間隔(或代碼執(zhí)行中預(yù)定的收集時間)徒坡,周期性地執(zhí)行這一操作。
  下面我們來分析一下函數(shù)中局部變量的正常生命周期瘤缩。局部變量只在函數(shù)執(zhí)行的過程中存在喇完。而在這個過程中,會為局部變量在棧(或堆)內(nèi)存上分配相應(yīng)的空間剥啤,以便儲存它們的值锦溪。然后再函數(shù)中使用這些變量,直至函數(shù)執(zhí)行結(jié)束府怯。此時刻诊,局部變量就沒有存在的必要了,因此可以釋放它們的內(nèi)存以供將來使用牺丙。垃圾收集器必須跟蹤哪個變量有用哪個變量沒用则涯,對于不再有用的變量打上標(biāo)記,以備將來收回其占用的內(nèi)存冲簿。用于標(biāo)識無用變量的策略可能會因?qū)崿F(xiàn)而異粟判,但具體到瀏覽器中的實現(xiàn),則通常有兩個策略峦剔。

1.6.1 標(biāo)記清除

JavaScript中最常用的垃圾收集方式是標(biāo)記清除(mark-and-sweep)档礁。當(dāng)變量進入環(huán)境(例如,在函數(shù)中聲明一個變量)時吝沫,就將這個變量標(biāo)記為“進入環(huán)境”呻澜。從邏輯上講,永遠(yuǎn)不能釋放進入環(huán)境的變量所占用的內(nèi)存野舶,因為只要執(zhí)行流進入相應(yīng)的環(huán)境,就可能會用到它們宰衙。而當(dāng)變量離開環(huán)境時平道,則將其標(biāo)記為“離開環(huán)境”。
  垃圾收集器在運行的時候會給儲存在內(nèi)存中的所有變量都加上標(biāo)記(當(dāng)然供炼,可以使用任何標(biāo)記方式)一屋。然后,它會去掉環(huán)境中的變量以及被環(huán)境中的變量引用的變量的標(biāo)記袋哼。而在此之后再被加上標(biāo)記的變量將被視為準(zhǔn)備刪除的變量冀墨,原因是環(huán)境中的變量已經(jīng)無法訪問到這些變量了。最后涛贯,垃圾收集器完成內(nèi)存清除工作诽嘉,銷毀那些帶標(biāo)記的值并回收它們所占用的內(nèi)存空間。

1.6.2 引用計數(shù)

另一種不太常見的垃圾收集策略叫做引用計數(shù)(reference counting)。引用計數(shù)的含義是跟蹤記錄每個值被引用的次數(shù)虫腋。當(dāng)聲明了一個變量并將一個引用類型值賦給該變量時骄酗,則這個值的引用次數(shù)就是1。相反悦冀,如果包含對這個值引用的變量又取得了另外一個值趋翻,則這個值的引用次數(shù)減1。當(dāng)這個值的引用次數(shù)變成0時盒蟆,則說明沒有辦法再訪問這個值了踏烙,因而就可以將其占用的內(nèi)存空間回收起來。這樣历等,當(dāng)垃圾收集器下次再運行時讨惩,它就會釋放那些引用次數(shù)為零的值所占用的內(nèi)存。
  Netscape Navigator 3.0是最早使用引用計數(shù)策略的瀏覽器募闲,但很快它就遇到了一個嚴(yán)重的問題:循環(huán)引用步脓。循環(huán)引用指的是對象A中包含一個指向?qū)ο驜的指針,而對象B中也包含一個指向?qū)ο驛的引用浩螺。請看下面的例子:

function problem() {
    var objectA = new Object();
    var objectB = new Object();

    objectA.someOtherObject = objectB;
    objectB.anotherObject = objectA;
}

在這個例子中靴患,objectA和objectB通過各自的屬性相互引用;也就是說要出,這兩個對象的引用次數(shù)都是2鸳君。在采用標(biāo)記清除策略的實現(xiàn)中,由于函數(shù)執(zhí)行之后患蹂,這兩個對象都離開了作用域或颊,因此這種相互引用不是個問題。但在采用引用計數(shù)策略的實現(xiàn)中传于,當(dāng)函數(shù)執(zhí)行完畢后囱挑,objectA和objectB還將繼續(xù)存在,因為它們的引用次數(shù)永遠(yuǎn)不會是0沼溜。假如這個函數(shù)被重復(fù)多次調(diào)用平挑,就會導(dǎo)致大量內(nèi)存得不到回收。為此系草,Netscape Navigator 4.0中放棄了引用計數(shù)方式通熄,轉(zhuǎn)而采用標(biāo)記清除來實現(xiàn)其垃圾收集機制。

1.7 管理內(nèi)存

優(yōu)化內(nèi)存占用的最佳方式找都,就是為執(zhí)行中的代碼保存必要的數(shù)據(jù)唇辨。一旦數(shù)據(jù)不再有用,最好通過將其值設(shè)置為null來釋放其引用——這個做法叫做解除引用(dereferencing)能耻。這一做法適用于大多數(shù)全局變量和全局對象的屬性赏枚。
  不過亡驰,解除一個值的引用并不意味著自動回收該值所占用的內(nèi)存。解除引用的真正作用是讓值脫離執(zhí)行環(huán)境嗡贺,以便垃圾收集器下次運行時將其回收隐解。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市诫睬,隨后出現(xiàn)的幾起案子煞茫,更是在濱河造成了極大的恐慌,老刑警劉巖摄凡,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件续徽,死亡現(xiàn)場離奇詭異,居然都是意外死亡亲澡,警方通過查閱死者的電腦和手機钦扭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來床绪,“玉大人客情,你說我怎么就攤上這事●海” “怎么了膀斋?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長痹雅。 經(jīng)常有香客問我仰担,道長,這世上最難降的妖魔是什么绩社? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任摔蓝,我火速辦了婚禮,結(jié)果婚禮上愉耙,老公的妹妹穿的比我還像新娘贮尉。我一直安慰自己,他們只是感情好朴沿,可當(dāng)我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布猜谚。 她就那樣靜靜地躺著,像睡著了一般悯仙。 火紅的嫁衣襯著肌膚如雪龄毡。 梳的紋絲不亂的頭發(fā)上吠卷,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天锡垄,我揣著相機與錄音,去河邊找鬼祭隔。 笑死货岭,一個胖子當(dāng)著我的面吹牛路操,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播千贯,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼屯仗,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了搔谴?” 一聲冷哼從身側(cè)響起魁袜,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎敦第,沒想到半個月后峰弹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡芜果,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年鞠呈,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片右钾。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡蚁吝,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出舀射,到底是詐尸還是另有隱情窘茁,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布后控,位于F島的核電站庙曙,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏浩淘。R本人自食惡果不足惜捌朴,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望张抄。 院中可真熱鬧砂蔽,春花似錦、人聲如沸署惯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽极谊。三九已至诡右,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間轻猖,已是汗流浹背帆吻。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留咙边,地道東北人猜煮。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓次员,卻偏偏與公主長得像,于是被迫代替她去往敵國和親王带。 傳聞我的和親對象是個殘疾皇子淑蔚,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內(nèi)容