第四章 變量、做用域和內存問題

按照ECMA-262定義,JavaScript的變量鬆散類型的本質,決定了:前端

  • 它還只是在特定時間用於保存特定值的一個名字而已。算法

  • 變量的值及其數據類型能夠再腳本的生命週期內改變。segmentfault

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

  • 基本類型瀏覽器

    • 簡單的數據段(Undefined、Null、Bollean、Number和String);安全

    • 基本類型變量按值進行訪問,由於能夠操做保存在變量中的實際的值。函數

  • 引用類型性能

    • 可能由多個值構成的對象;優化

    • 是保存在內存中的對象;ui

    • 與其它語言不一樣,JavaScript不容許直接訪問內存中的位置,即不能直接操做對象所在的內存空間。實際操做時,是在操做對象的引用而不是實際的對象。這就是所謂的 引用類型的值是按引用訪問的。(書中註釋提出:這種說法不嚴密,當複製保存這對象的某個變量時,操做是對對象的引用。但在爲對象添加屬性時,操做的是實際的對象。----圖靈社區"壯壯的前端之路"注)url

4.1.1 動態的屬性

參考:http://segmentfault.com/a/1190000002789651

基本類型和引用類型變量定義方式相同:建立一個變量併爲該變量賦值。
不一樣類型值可執行操做則截然不同,例如只能給引用類型值動態地添加屬性:

  • 引用值類型

    var person = new Object();
    person.name = "Nicholas";
    alert(person.name); //"Nicholas"
    //若是對象不被銷燬或name屬性不被刪除,則這個屬性將永遠存在。
  • 基本類型

    var name = "Nicholas";
    name.age = 27;
    alert(name.age); //undefined
    //爲基本類型變量添加屬性不會報錯,但沒法訪問。

4.1.2 複製變量值

  • 基本類型

    • 基本類型的變量存放在棧區(內存中的棧內存)的。

    • 從一個變量向另外一個變量複製時,會在變量對象上建立一個新值,而後把該值複製到爲新變量分配的位置上。兩個變量能夠參與任何操做而不相互影響。例如:

var num1 = 5;
    var num2 = num1;
    num1= 6;
    console.log(num2); //5
  • 引用類型

    • 引用類型的值同時保存在棧內存和堆內存中。棧區保存變量標識符和指向對內存中該對象的指針。

    • 從一個變量向另外一個變量複製時,會將存儲在變量對象中的只複製一份放到爲新變量分配的空間中。但,這個值其實是一個指針,這個指針指向存儲在堆中的一個新對象。複製結束後,兩個變量實際上將引用同一個對象。改變其中一個變量,就會影響另外一個,例如:

var obj1 = new Object();
    var obj2 = obj1;
    obj1.name = "Nicholas";
    alert(obj2.name); //"Nicholas"

4.1.3 傳遞參數

ECMAScript中全部函數的參數都是按值傳遞的。即把函數外部的值賦值給函數內部的參數,等價於把值從一個變量複製到另外一個變量同樣。能夠把ECMAScript函數的參數理解爲局部變量。例如:

  • 基本類型

function addTen(num){
        num += 10;
        return num;
    }
    var count = 20;
    var result = addTen(count);
    alert(count); //20
    alert(result); //30
  • 引用類型

function setName(obj){
        obj.name = "Nicholas";
        obj = new Object();
        obj.name = "Greg";
    }
    var person = new Object();
    setName(person);
    alert(person.name); //"Nicholas"

    //即便在函數內部修改了參數的值,希望是的引用仍然保持不變。
    //實際上,當在函數內部重寫obj時,這個變量引用改的就是一個局部對象了。而這個局部對象會在函數執行完畢後當即被銷燬。

4.1.4 檢測類型

  • typeof 檢測基本數據類型

var s = "Nicholas";
var b = true;
var i = 22;
var u;
var n = null; //注意:使用typeof操做符,null返回object
var o = new Object;

alert(typeof s); //string
alert(typeof b); //bollean
alert(typeof i); //number
alert(typeof u); //undefined
alert(typeof n); //object
alert(typeof o); //object
  • instanceof 檢測引用值類型值是何種類型的對象

語法:result = variable instanceof constructor

若是變量是給定引用類型(根據它的原型鏈來識別,見第6章)的實例,instanceof返回true.

var person = new ();
    var color = ["blue","yellow","red"];

    alert(person instanceof Object);//true
    alert(color instanceof Array); //true

4.2 執行環境及做用域

執行環境 定義了變量或函數有權訪問的其餘數據,決定了他們各自的行爲。每一個執行環境都有一個與之關聯的變量對象(variable object), 環境中定義的全部變量和函數都保存在這個對象中。(這個對象咱們沒法訪問,解析器在處理數據時在後臺使用。)

全局執行環境 表示最外圍的執行環境。在Web瀏覽器中,全局環境被認爲是window對象(詳見第7章)。所以全部全局變量和函數都是做爲window對象的屬性和方法建立的。某個執行環境中的全部代碼執行完畢後,該環境被銷燬,保存在其中的全部變量和函數定義也隨之銷燬(全局執行環境知道應用程序退出---例如關閉網頁或瀏覽器時纔會被銷燬)。

每一個函數都有本身的執行環境。當執行流進入一個函數時,函數的環境會被推入一個環境棧中。而函數執行以後,棧將其彈出,把控制權返回給以前的執行環境。ECMAScript程序中的執行流正是由這個方便的機制控制着。

做用域鏈(scope chain)---代碼在一個環境中執行時,會建立變量對象的做用域鏈。用於保證對執行環境有權訪問的全部變量和函數的有序訪問。做用域鏈的前端,始終都是當前執行的代碼所在環境的變量對象,若是是個函數,則將其活動對象(activation object)做爲變量對象。活動對象在最開始時只包含一個變量,即arguments對象(這個對象在全局環境中不存在)。做用域鏈中的下一個變量對象來自包含(外部)環境,而再下一個變量對象則來自下一個包含環境。這樣,一直延續到全局環境 ;全局執行環境的變量始終都是自做用域鏈中的最後一個對象

標識符解析 是沿着做用域鏈一級一級地搜索標識符的過程。搜索過程始終從更做用域鏈的前端開始,而後逐級地向後回溯,知道找到標識符位置(若找不到,一般會致使錯誤)。

例如:

var color = "blue";

function changeColor(){
    if(color === "blue"){
        color = "red";
    }else{
        color = "blue";
    }
}
//changeColor()的做用域鏈包含兩個對象:他本身的變量對象(其中定義着arguments對象)和全局環境的變量對象。
//能夠在函數內部訪問變量color,就是由於能夠在這個做用域中找到它。

changeColor();
alert("Color is now" + color);

此外,在局部做用域定義的變量能夠再局部環境中與全局變量互換使用。例如:

var color = "blue";

function changeColor(){
    var anotherColor = "red";

    function swapColors(){
        var tempColor = anotherColor;

        anotherColor = color;
        color = tempColor;
        //這裏能夠訪問color,anotherColor和tempColor
    }
    //這裏能夠訪問color和anotherColor, 但不能訪問tempColor
}
//這裏只能訪問color
changeColor();
  • 內部環境能夠經過做用域鏈訪問全部的外部環境,但外部環境不能訪問內部環境中的任何變量和函數;

  • 這些環境之間是線性的、由次序的;

  • 每一個環境均可以向上搜索做用域鏈,以查詢變量和函數名,但任何環境都不能經過向下搜索做用域鏈而進入另外一個執行環境;

  • 函數參數也被看成變量來對待,所以訪問規則與執行環境中的其餘變量相同。

4.2.1 延長做用域鏈

下列兩個語句能夠再做用域鏈的前端臨時增長一個變量對象,該變量對象會在代碼執行後被移除。具體來講,就是當執行流進入下列任一語句時,做用域鏈就會獲得加長。

//try-catch語句的catch塊;
//with語句;

例如:

function buildUrl(){
    var qs = "?debug=true";

    with(location){
        var url = href + qs;
    }
    return url;
}
  • with語句接受了一個location對象,所以其變量對象中就包含了location對象的全部屬性和方法,而這個變量對象被添加到了做用域鏈的前端。

  • buildUrl()函數中定義了了一個變量qs.當在with語句中引用變量href時(實際引用的是location.href),能夠再當前執行環境的變量對象中找到。當引用了變量qs時,引用改的則是在buiildUrl()中定義的那個變量,而該變量位於函數換進改的變量對象中。

  • 至於with語句內部,則定義了一個名爲url的變量,於是url變量就成了函數執行環境的一部分,因此能夠做爲函數的值被返回。

4.2.2 沒有塊級做用域

1. 聲明變量

  • 使用var聲明的變量會自動被添加到最接近的環境中。在函數內部,最接近的環境就是函數的局部環境;在with語句中,最接近的環境是函數環境。

  • 若是初始化變量時沒有使用var, 改變兩會自動被添加到全局變量。

例如:

function add(num1, num2){
    var sum = num1 + num2;
    return sum;
}
var result = add(10, 20); //30
alert(sum); //報錯

//若省略第二行的var, 那麼add()執行完畢後,sum依然能夠被訪問到,alert(sum)返回30.

在嚴格模式下,初始化未聲明變量會致使錯誤。

2. 查詢標識符

查詢標識符過程
當在某個環境中爲了讀取或寫入而引用一個標識符時,必須經過搜索來肯定該標識符實際表明什麼。搜索過程從做用域鏈的前端開始向上 逐級查詢與給定名字匹配的標識符。若是在局部環境中找到了該標識符,搜索過程中止,變量就緒。若是在局部環境沒有找到該變量名,則繼續沿做用域鏈向上搜索。搜索過程一直追溯到全局環境的變量對象。

例如:

var color = "blue";
function getColor(){
    return color;
}

alert(getColor()); //"blue"

clipboard.png

調用getColor()時,用到了變量color.爲了肯定變量color的值,將開始搜索:

  1. 搜索getColor()的變量對象,查找其中是否包含一個名爲color的標識符,沒找到,進行第2步;

  2. 向上繼續搜索到了定義這個變量的變量對象(全局環境的變量對象),找到了名爲color的標識符。

注:在這個過程當中,若是存在一個局部的變量的定義,則搜索自動中止,再也不進入另外一個變量對象。即若是局部環境中存在着同名標識符,就不會使用位於父環境中的標識符。

例如:

var color = "blue";

function getColor(){
    var color = "red";
    return color;
}

alert(getColor()); //"red"

查詢變量是有代價的。很明顯,訪問局部變量要比訪問全局變量更快,由於不用向上搜索做用域鏈。JavaScript引擎在優化標識符查詢方面作得不錯,所以這個差異在未來恐怕就能夠忽略不計了

4.3 垃圾收集

JavaScript具備自動垃圾收集機制。(執行環境會負責管理代碼執行過程當中使用的內存)

函數中局部變量的正常生命週期

  1. 局部變量只在函數執行的過程當中存在。而在這個過程當中,會爲局部變量在棧(或堆)內存上分配相應的空間,以便存儲他們的值;

  2. 而後在函數中使用這些變量,直至函數執行結束;

  3. 此時,即可以釋放局部變量的內存以供未來使用。

不一樣瀏覽器,一般有兩個策略:標記清除、引用計數。

4.3.1 標記清除

JavaScript中最經常使用的垃圾收集方式是標記清除(mark-and-sweep). 當變量進入環境(例如,在函數中聲明一個變量)時,就將這個變量標記爲「進入環境」。從邏輯上說永遠不能釋放進入環境的變量所佔用的內存,由於只要執行流進入相應的環境,就可能會用到他們。而當變量離開環境是,則將其標記爲「離開環境」 。

可使用任何方式來標記變量。好比,經過翻轉某個特殊的位來記錄一個變量什麼時候進入環境,或者使用一個「進入環境的」變量列表一個「離開環境的」變量列表來跟蹤那個變量發生了變化。採起什麼策略好比何標記更重要

垃圾收集器的內存清除工做:

  1. 垃圾收集器 在運行的時候會給存儲在內存中的全部變量都加上標記(固然,可使用任何標記方式);

  2. 它會去掉環境中的變量以及被環境中的變量引用的變量的標記;

  3. 而在此以後再被加上標記的變量將被視爲準備刪除的變量。由於環境中的變量已經沒法訪問到這些變量了;

  4. 最後,垃圾收集器完成內存清除工做,銷燬那些表明及的值並回收他們所佔用的內存空間。

注:到2008年爲止,IE、Firefox、Opera、Chrome和Safari的JavaScript實現使用的都是標記清除是的垃圾回收策略(或相似的策略),只不過垃圾收集的時間間隔互有不一樣。

4.3.2 引用計數

(不太常見)跟蹤記錄每個值被引用的次數。

當生命了一個變量並將一個引用類型值賦給該變量是,則這個值的引用次數就是1. 在賦給另外一個變量,引用次數再+1. 相反,若是包含對這個值引用的變量又取得了另一個值,則這個值的引用次數-1. 當這個值的引用次數變成0時,說明沒辦法在訪問這個值,於是就能夠將其佔用的內存回收回來。如此,當垃圾收集器再次運行時,他就會釋放哪些引用次數爲0的值所佔用的內存。

這種方式可能遇到循環引用的問題,例如Netscape Navigator 3.0是最先使用引用計數策略的瀏覽器:

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

    objectA.someOtherObject = objectB;
    objectB.someOtherObject = objectA;
}
//當problem()函數執行完畢後,objectA和objectB引用次數爲2. 若函數反覆調用,便會致使大量內存的不到回收。
//Netscape在Navigator4.0以後便改用標記清除策略。

IE9+,把BOM和DOM對象都轉換成了真正的JavaScript對象。避免了兩種垃圾收集算法並存致使的問題,也消除了常見的內存泄露現象。

4.3.3 性能問題

垃圾回收機制是週期性運行的,並且 若是爲變量分配的內存數量很可觀,那麼回收工做量也是至關大的。此時,肯定垃圾回收間隔時間很是重要。

在有的瀏覽器中能夠觸發垃圾收集機制,但不建議這麼作。在IE中,調用window.CollectGarbge()方法會當即執行垃圾收集。在Opera7及更高版本中,調用window.opera.collect()也會啓動垃圾收集機制。

4.3.4 管理內存

  • 分配給Web瀏覽器的可用內存數量一般比分配給桌面應用程序的少。主要是出於安全方面的考慮,防止運行JavaScript的網頁耗盡所有系統內存致使系統崩潰。

  • 內存限制問題不只會影響給變量分配內存,也會影響調用棧以及在一個線程中可以同時執行的語句數量。

  • 優化內存佔用的最佳方式,就是爲執行中的戴嘛只保存必要的數據。一旦數據不在游泳,最好經過最好將其設置爲null來釋放其引用----- 解除引用 (dereferencing)。適用於大多數全局變量和全局對象的屬性。

例如:

function createPerson(name){
    var localPerson = new Object();
    localPerson.name = name;
    return localPerson;
}

var  globalPerson = createPerson("Nicholas");

globalPerson = null;//手動解除globalPerson的引用

注:解除一個值的引用並不意味着自動回收該值所佔用的內存。真正做用是讓值脫離執行環境,以便垃圾回收器下次運行時將其回收。

相關文章
相關標籤/搜索