javascript之變量,做用域和內存問題

1. 基本類型和引用類型javascript

javascript變量可能包含兩種不一樣數據類型的值:基本類型值和引用類型值。基本類型值指的是那些保存在棧內存中的簡單數據段,即這種值徹底保存在內存中的一個位置。而引用類型值則是指那些保存在堆內存中的對象,意思是變量中保存的實際上只是一個指針,這個指針指向內存中的另外一個位置,該位置保存對象。 前端

javascript5種基本數據類型的值在內存中分別佔有固定大小的空間,所以能夠把它們的值保存在棧內存中。並且,這樣也能夠提升查詢變量的速度。對於保存基本類型值的變量,咱們說它們是按值訪問的。若是賦給變量的是一個引用類型的值,則必須在堆內存中爲這個值分配空間。因爲這種值的大小不固定,所以不能把它們保存到棧內存中。但內存地址的大小是固定的,所以能夠將內存地址保存在棧內存中。這樣,當查詢引用類型的變量時,就能夠首先從棧中讀取內存地址,而後再「順藤摸瓜」地找到保存在堆中的值。對於這種查詢變量值的方式,咱們把它叫作按引用訪問。java

動態屬性web

定義基本類型值和引用類型值的方式是相似的,建立一個變量併爲該變量賦值。可是,當這個值保存到變量中之後,對不一樣類型值能夠執行的操做則截然不同。對於引用類型的值,咱們能夠爲其添加屬性和方法,也能夠改變和刪除其屬性和方法:瀏覽器

var person = new Object();
person.name = "Nichoias";
alert(person.name);

如上例,若是對象不被銷燬或者這個屬性不被刪除,則這個屬性將一直存在。可是,咱們不能給基本類型的值添加屬性,儘管這樣作不會致使任何錯誤:安全

var name = "Nicholas";
name.age = 27;
alert(name.age);            //undefined

複製變量值函數

除了保存的方式不一樣以外,在從一個變量向另外一個變量複製基本類型值和引用類型值時,也存在不一樣。若是從一個變量向另外一個變量複製基本類型的值,會在棧中建立一個新值,而後把該值複製到爲新變量分配的位置上:工具

var num1 = 5;
var num2 = num1;

在此,num1中保存的值是5。當使用num1的值來初始化num2時,num2中也保存了值5。但num2中的5與num1中的5是徹底獨立的,該值只是num1中5的一個副本。此後,這兩個變量能夠參與任何操做而不會相互影響。性能

當從一個變量向另外一個變量複製引用類型的值時,一樣也會將存儲在棧中的值複製一份放到爲新變量分配的空間中。不一樣的是,這個值的副本其實是一個指針,而這個指針指向存儲在堆中的一個對象。複製操做結束後,兩個變量實際上將引用同一個對象。所以,改變其中一個變量,就會影響另外一個變量:優化

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

傳遞參數:

javascript中全部函數的參數都是按值傳遞的。也就是說,把函數外部的值複製給函數內部的參數,就和把值從一個變量複製到另外一個變量同樣。基本類型值的傳遞如同基本類型變量的複製同樣,而引用類型值的傳遞,則如同引用類型變量的複製同樣。

在向參數傳遞基本類型的值時,被傳遞的值會被複制給一個局部變量(即命名參數,或者用javascript的概念來講,就是arguments對象中的一個元素)。在向參數傳遞引用類型的值時,會把這個值在內存中的地址複製給一個局部變量,所以這個局部變量的變化會反映在函數外部:

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";
}
var person = new Object();
setName(person);
alert(person.name);                //"Nicholas"

檢測類型

要檢測一個變量是否是基本數據類型能夠用typeof操做符。說得更具體一點:typeof操做符是肯定一個變量是字符串,數值,布爾值,仍是nudefined的最佳工具。若是變量的值是一個對象或null,則typeof操做符會像下面例子中的那樣返回「object」:

var n = null;
var o = new Object();
alert(typeof n);                        //object
alert(typeof o);                        //object

雖然在檢測基本類型時typeof是很是得力的助手,但在檢測引用類型的值時,這個操做符的用處不大。javascript提供了instanceof操做符。若是變量是給定引用類型(由構造函數表示)的實例,那麼instanceof操做符就會返回true:

alert(person instanceof Object);            //變量person是object嗎
alert(colors instanceof Array);              //變量colors是Array嗎
alert(pattern instanceof RegExp);         //變量pattern是RegExp嗎

根據規定,全部引用類型的值都是Object的實例。所以,在檢測一個引用類型值和Object構造函數時,instanceof操做符始終會返回true。固然,若是使用instanceof操做符檢測基本類型的值,則該操做符始終會返回false,由於基本類型不是對象。

2. 執行環境及做用域

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

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

每一個函數在被調用時都會建立本身的執行環境。當執行流進入一個函數時,函數的環境就會被推入一個環境棧中。而在函數執行以後,棧將其環境彈出,把控制權返回給以前的執行環境。

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

標識符解析是沿着做用域鏈一級一級地搜索標識符的過程。搜索過程始終從做用域鏈的前端開始,而後逐級地向後回溯,直至找到標識符爲止。見下面的代碼:

var color = "blue";
function changeColor(){
    if(color === "blue"){
        color = "red";
    }else{
        color = "blue";
    }
}
changeColor();
alert("Color is noew " + color);

在這個簡單的例子中,函數changeColor()的做用域鏈包含兩個對象:它本身的變量對象(其中定義着arguments對象)和全局環境的變量對象。能夠在函數內部訪問變量color,就是由於能夠在這個做用域鏈中找到它。

var color = "blue";
function changeColor(){
    var anotherColor = "red";
    function awapColors(){
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
        //這裏能夠訪問color,anotherColor和tempColor
    }
    //這裏能夠訪問color和anotherColor,但不能訪問tempColor
    swapColors();
}
changeColor();
//這裏不能訪問anotherColor和tempColor,但但能夠訪問color
alert("Color is now " + color);

以上代碼共涉及3個執行環境:全局環境,changeColor()的局部環境和swapColors()的局部環境。全局環境中有一個變量color和一個函數changeColor()。changeColor()的局部環境中有一個名爲anotherColor的變量和一個名爲swapColors()的函數,但它也能夠訪問全局環境中的變量color。swapColors()的局部環境中有一個變量tempColor,該變量只能在這個環境中訪問到。不管全局環境仍是changeColor()的局部環境都無權訪問tempColor。然而,在swapColors()內部則能夠訪問其餘兩個環境中的全部變量,由於那兩個環境是它的父執行環境。

延長做用域鏈

雖然執行環境的類型總共只有兩種——全局和局部(函數),但仍是有其餘辦法來延長做用域鏈。這麼說是由於有些語句能夠在做用域鏈的前端臨時增長一個變量對象,該變量對象會在代碼執行後被移除。在兩種狀況下會發生這種現象。具體來講,就是當執行流進入下列任何一個語句時,做用域鏈就會獲得加長:

  • try-catch語句的catch塊

  • with語句

這兩個語句都會在做用域鏈的前端添加一個變量對象。對with語句來講,其變量對象中包含着爲指定對象的全部屬性和方法所做的變量聲明。對catch語句來講,其變量對象中包含的是被拋出的錯誤對象的聲明。這些變量對象都是隻讀的,所以在with和catch語句中聲明的變量都會被添加到所在執行環境的變量對象中。示例:

function buildUrl(){
    var qs = "?debug=true";
    with(location){
        var url = href + qs;
    }
    return url;
}
var result = buildUrl();
alert(result);

在此,with語句接收的是location對象,所以其變量對象中就包含了location對象的全部屬性和方法,而這個變量對象被添加到了做用域鏈的前端。buildUrl()函數中定義了一個變量qs。當在with語句中引用變量href時(實際引用的是location.href),能夠在當前執行環境的變量對象中找到。當引用變量qs時,引用的則是在buildUrl()中定義的那個變量,而該變量位於函數環境的變量對象中。至於with語句內部,則定義了一個名爲url的變量。因爲with語句的變量對象是隻讀的,結果url就成了函數執行環境的一部分,於是能夠做爲函數的值被返回。

沒有塊級做用域

javascript沒有塊級做用域常常會致使理解上的困惑。在其餘類C語言中,由花括號封閉的代碼塊都有本身的做用域(對javascript來講,這就是他們本身的執行環境):

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

這裏是在一個if語句中定義了變量color。若是是在C,C++或java中,color會在if語句執行完畢後被銷燬。但在javascript中,if語句中的變量聲明會將變量添加到當前的執行環境中。又例:

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

對於javascript來講,由for語句建立的變量i即便在for循環執行結束後,也依舊會存在於循環外部的執行環境中。

聲明變量:在使用var關鍵字聲明變量時,這個變量將被自動添加到距離最近的可用執行環境中。對於函數而言,這個最近的環境就是函數的局部環境。對於前面例子中的with語句而言,這個最近的環境也是函數的環境。若是變量在未經聲明的狀況下被初始化,那麼該變量會被自動添加到全局環境。如:

function add(num1, num2){
    var sum = num1 + num2;
    return sum;
}
var result = add(10, 20);            //30
alert(sum);                                  //因爲sum不是有效的變量,所以會致使錯誤

若是省略這個例子中的var關鍵字,那麼當add()執行完畢後,sum也將能夠訪問到:

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

這個例子中的變量sum在被初始化賦值時沒有使用var關鍵字。因而,當調用完add()以後,添加到全局環境中的變量sum將繼續存在,即便函數已經執行完畢。

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

3. 垃圾收集

javascript具備自動垃圾收集機制,也就是說,執行環境會負責管理代碼執行過程當中使用的內存。在編寫javascript程序時,開發人員不用再關心內存使用問題,所需內存的分配以及無用內存的回收徹底實現了自動管理。這種垃圾收集機制的原理其實很簡單:找出那些再也不繼續使用的變量,而後釋放其佔用的內存。爲此,垃圾收集器會按照固定的時間間隔,週期性地執行這一操做。垃圾收集器必須跟蹤哪一個變量有用哪一個變量沒用,對於再也不有用的變量打上標記,以備未來收回其佔用的內存。用於標識無用變量的策略可能會因實現而異,但具體到瀏覽器中的實現,則一般有兩個策略:標記清除,引用計數。

管理內存

使用具有垃圾收集機制的語言編寫程序,開發人員通常沒必要操心內存管理的問題。可是,javascript在進行內存管理及垃圾收集時面臨的問題仍是有點不同凡響。其中最主要的一個問題,就是分配給web瀏覽器的可用內存數量一般要比分配給桌面應用程序的少。這樣作的目的主要是出於安全方面的考慮,目的是防止運行javascript的網頁耗盡所有系統內存而致使系統崩潰。內存限制問題不只會影響給變量分配內存,同時還會影響調用棧以及在一個線程中可以同時執行的語句數量。

所以,確保佔用最少的內存可讓頁面得到更好的性能。而優化內存佔用的最佳方式,就是爲執行中的代碼只保存必要的數據。一旦數據再也不有用,最好經過將其設置爲null來釋放其引用——這個作法叫作解除引用。這一作法適用於大多數全局變量和全局對象的屬性。局部變量會在它們離開執行環境時自動被解除引用。示例:

function createPerson(name){
    var localPerson = new Object();
    localPerson.name = name;
    return localPerson;
}
var globalPerson = createPerson("Nicholas");
//手工解除globalPerson的引用
globalPerson = null;

因爲localPerson在createPerson()函數執行完畢後就離開了其執行環境,所以無需咱們顯式地去爲它解除引用。可是對於全局變量globalPerson而言,則須要咱們在不使用它的時候手工爲它解除引用,這也正是上面例子中最後一行代碼的目的。不過,解除一個值的引用並不意味着自動回收該值所佔用的內存。解除引用的真正做用是讓值脫離執行環境,以便垃圾收集器下次運行時將其回收。

相關文章
相關標籤/搜索