javascript高級(jí)程序設(shè)計(jì)(第4章)-- 變量、作用域和內(nèi)存問題

第四章:變量瘤袖、作用域和內(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;
復(fù)制基本類型

在變量對(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ā)生了變化。這就是引用類型的特性检疫。

復(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)境入棧

全局環(huán)境入棧之后镊靴,其中的可執(zhí)行代碼開始執(zhí)行铣卡,直到遇到了changeColor(),這一句激活函數(shù)changeColor創(chuàng)建它自己的執(zhí)行環(huán)境偏竟,因此第二步就是changeColor的執(zhí)行環(huán)境入棧煮落。

changeColor入棧

changeColor的環(huán)境入棧之后,控制器開始執(zhí)行其中的可執(zhí)行代碼踊谋,遇到swapColors()之后又激活了一個(gè)執(zhí)行環(huán)境蝉仇。因此第三步是swapColors的執(zhí)行上下文入棧。

swapColor入棧

在swapColors的可執(zhí)行代碼中殖蚕,再?zèng)]有遇到其他能生成執(zhí)行環(huán)境的情況量淌,因此這段代碼順利執(zhí)行完畢,swapColors的環(huán)境從棧中彈出嫌褪。

swapColor出棧

swapColors的執(zhí)行環(huán)境彈出之后呀枢,繼續(xù)執(zhí)行changeColor的可執(zhí)行代碼,也沒有再遇到其他執(zhí)行環(huán)境笼痛,順利執(zhí)行完畢之后彈出裙秋。這樣,ECStack中就只身下全局環(huán)境了缨伊。

changeColor出棧

全局上下文在瀏覽器窗口關(guān)閉后出棧摘刑。

圖解函數(shù)調(diào)用棧

詳細(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í)行代碼)

mark

作用域和作用域鏈

作用域:
  • 在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)建终息。

mark
作用域鏈:

作用域鏈夺巩,是由當(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í)行其他代碼揪荣。

mark

變量對(duì)象(Variable Object)

變量對(duì)象的創(chuàng)建,依次經(jīng)歷了以下幾個(gè)過程往史。

  1. 建立arguments對(duì)象仗颈。檢查當(dāng)前執(zhí)行環(huán)境中的參數(shù),建立該對(duì)象下的屬性與屬性值椎例。
  2. 檢查當(dāng)前執(zhí)行環(huán)境的函數(shù)聲明挨决,也就是使用function關(guān)鍵字聲明的函數(shù)。在變量對(duì)象中以函數(shù)名建立一個(gè)屬性订歪,屬性值為指向該函數(shù)所在內(nèi)存地址的引用脖祈。如果函數(shù)名的屬性已經(jīng)存在,那么該屬性將會(huì)被新的引用所覆蓋刷晋。
  3. 檢查當(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;
mark

根據(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)為是更加貼切的形容。如圖漆诽。

mark

注意侮攀,因?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_01_理解內(nèi)存分配

理解Javascript_15_作用域分配與變量訪問規(guī)則,再送個(gè)閉包

前端基礎(chǔ)進(jìn)階(一):內(nèi)存空間詳細(xì)圖解

前端基礎(chǔ)進(jìn)階(二):執(zhí)行上下文詳細(xì)圖解

前端基礎(chǔ)進(jìn)階(三):變量對(duì)象詳解

前端基礎(chǔ)進(jìn)階(四):詳細(xì)圖解作用域鏈與閉包

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末坪郭,一起剝皮案震驚了整個(gè)濱河市个从,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌歪沃,老刑警劉巖嗦锐,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異沪曙,居然都是意外死亡奕污,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門液走,熙熙樓的掌柜王于貴愁眉苦臉地迎上來碳默,“玉大人,你說我怎么就攤上這事缘眶≈龈” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵巷懈,是天一觀的道長该抒。 經(jīng)常有香客問我,道長砸喻,這世上最難降的妖魔是什么柔逼? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮割岛,結(jié)果婚禮上愉适,老公的妹妹穿的比我還像新娘。我一直安慰自己癣漆,他們只是感情好维咸,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般癌蓖。 火紅的嫁衣襯著肌膚如雪瞬哼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天租副,我揣著相機(jī)與錄音坐慰,去河邊找鬼。 笑死用僧,一個(gè)胖子當(dāng)著我的面吹牛结胀,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播责循,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼糟港,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了院仿?” 一聲冷哼從身側(cè)響起秸抚,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎歹垫,沒想到半個(gè)月后剥汤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡县钥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年秀姐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片若贮。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡省有,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出谴麦,到底是詐尸還是另有隱情蠢沿,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布匾效,位于F島的核電站舷蟀,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏面哼。R本人自食惡果不足惜野宜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望魔策。 院中可真熱鬧匈子,春花似錦、人聲如沸闯袒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至其徙,卻和暖如春胚迫,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背唾那。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來泰國打工访锻, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人通贞。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓朗若,卻偏偏與公主長得像,于是被迫代替她去往敵國和親昌罩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

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