《JavaScript高級程序設計》筆記4:變量、作用域和內存問題

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

關鍵詞:引用類型的值(對象),基本類型的值(原始值),按值訪問,按引用訪問

ECMAScript變量可能包含兩種不同數據類型的值:基本類型值和引用類型值。

  • 基本類型值,指的是簡單的數據段。
  • 引用類型的值,指那些可能有多個值構成的對象。

將值賦給一個變量時,解析器必須確定這個值是基本類型的值還是引用類型值。5種基本數據類型(Undefined,Null,Boolean,Number,String)是按值訪問的,因此,可以操作保存在變量中的實際值。而引用類型的值是保存在內存中的對象。與其他語言不同,JavaScript不允許直接訪問內存中的位置的,也就是說不能直接操作對象的內存空間。在操作對象時,實際上是在操作對象的引用而不是實際的對象。為此,引用類型的值是按引用訪問的(當復制保存著對象的某個變量時,操作的是對象的引用。但是在為對象添加屬性的時候,操作的是實際的對象)。

動態的屬性

  • 只能給引用類型之動態添加屬性。
var person = new Object();
person.name = "Nicholas";
alert(person.name); // Nicholas

//我們不能給基本類型的值添加屬性,盡管不會導致任何錯誤。
var name = "Nicholas";
name.age = 27;
alert(name.age); // undefined

根本的一個原因在于:原始值是不可更改的:任何方法無法更改(或“突變”)一個原始值。字符串作為基本數據類型之一,同樣是如此:JavaScript禁止通過索引修改字符串的字符。字符串中所有的方法看上去返回了一個修改后的字符串,實際上返回的是一個新的字符串的值。總之,原始值不可變,對象引用可變。

var o = {x:1};
o.x = 2;
o.y = 2;
var a = [1,2,3];
a[0] = 0;
a[3] = 4;

var s = "hello";
s.toUpperCase();   //返回"HELLO",沒改變s的值
s                             //  => "hello"

復制變量值

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); // Nicholas

變量obj1保存了一個對象的新實例。然后,這個值被復制到了obj2中;換句話說,obj1和obj2都指向同一個對象。這樣,當為obj1添加name屬性后,可以通過obj2來訪問這個屬性。

傳遞參數

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"

對象也是按值傳遞的。例:

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"的新對象。
這說明,即使在函數內部修改了參數的值,但原始的引用仍然保持未變。實際上,當函數內部重寫obj時,這個變量引用的就是一個局部對象了。這個局部對象在函數執行完畢后立即被銷毀。

執行環境及作用域

關鍵詞:變量對象、全局執行環境、作用域鏈、活動對象

每個函數都有自己的執行環境。每個執行環境都有與之關聯的變量對象,環境中定義的所有變量和函數都保存在這個對象中。

在Web瀏覽器中,全局執行環境被認為是window對象,因此所有全局變量和函數都是作為window對象的屬性和方法創建。某個執行環境的所有代碼執行完畢后,該環境被銷毀,保存在其中的所有變量的屬性和函數定義也隨之銷毀(全局執行環境直到應用程序退出——關閉網頁或瀏覽器——時才會被銷毀)。

當代碼在一個環境中執行時,會創建變量對象的一個作用域鏈。用途是保證對執行環境有權訪問的所有變量和函數的有序訪問。作用域鏈前端,始終都是當前執行的代碼所在環境的變量對象。

環境是函數,則將其活動對象作為變量對象。活動對象最開始時只包含一個變量,即arguments對象(這個對象在全局環境中是不存在的)。

函數參數也被當作變量來對待,訪問規則與執行環境中其他變量相同。

延長作用域鏈

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

沒有塊級作用域

注意變量提升、缺乏聲明時造成全局變量。

垃圾收集

垃圾回收

垃圾回收
javascript具有垃圾回收的機制,也就是說,執行環境會負責管理代碼執行過程中使用的內存。其余的不多說,我們來分析一下函數中局部變量的正常生命周期。局部變量只在函數執行過程中存在。而在這個過程中,會為局部變量在棧(或堆)內存上分配相應的空間,以便存儲他們的值。然后在函數中使用這些變量,直到函數結束。此時,局部變量就沒有存在的必要了,因此可以釋放他們所占的內存以供他們使用。現在各大瀏覽器通常用采用的垃圾回收有兩種方法:標記清除、引用計數。

標記清除

這是javascript中最常用的垃圾回收方式。當變量進入執行環境是,就標記這個變量為“進入環境”。從邏輯上講,永遠不能釋放進入環境的變量所占用的內存,因為只要執行流進入相應的環境,就可能會用到他們。當變量離開環境時,則將其標記為“離開環境”。

垃圾收集器在運行的時候會給存儲在內存中的所有變量都加上標記。然后,它會去掉環境中的變量以及被環境中的變量引用的標記。而在此之后再被加上標記的變量將被視為準備刪除的變量,原因是環境中的變量已經無法訪問到這些變量了。最后。垃圾收集器完成內存清除工作,銷毀那些帶標記的值,并回收他們所占用的內存空間。

引用計數

另一種不太常見的垃圾回收策略是引用計數。引用計數的含義是跟蹤記錄每個值被引用的次數。當聲明了一個變量并將一個引用類型賦值給該變量時,則這個值的引用次數就是1。相反,如果包含對這個值引用的變量又取得了另外一個值,則這個值的引用次數就減1。當這個引用次數變成0時,則說明沒有辦法再訪問這個值了,因而就可以將其所占的內存空間給收回來。這樣,垃圾收集器下次再運行時,它就會釋放那些引用次數為0的值所占的內存。

但是用這種方法存在著一個問題,下面來看看代碼:

function problem(){
     var objA = new Object();
     var objB = new Object();
     objA.someOtherObject  = objB;
     objB.anotherObject = objA;
}

在這個例子中,objA和objB通過各自的屬性相互引用;也就是說這兩個對象的引用次數都是2。在采用引用計數的策略中,由于函數執行之后,這兩個對象都離開了作用域,函數執行完成之后,objA和objB還將會繼續存在,因為他們的引用次數永遠不會是0。這樣的相互引用如果說很大量的存在就會導致大量的內存泄露。

我們知道,IE中有一部分對象并不是原生JavaScript對象。例如,其BOM和DOM中的對象就是使用C++以COM(Component Object Model,組件對象)對象的形式實現的,而COM對象的垃圾回收器就是采用的引用計數的策略。因此,即使IE的Javascript引擎使用標記清除的策略來實現的,但JavaScript訪問的COM對象依然是基于引用計數的策略的。說白了,只要IE中涉及COM對象,就會存在循環引用的問題。看看下面的這個簡單的例子:

var element = document.getElementById("some_element");
var myObj = new Object();
myObj.element = element;
element.someObject = myObj;

我們知道,IE中有一部分對象并不是原生JavaScript對象。例如,其BOM和DOM中的對象就是使用C++以COM(Component Object Model,組件對象)對象的形式實現的,而COM對象的垃圾回收器就是采用的引用計數的策略。因此,即使IE的Javascript引擎使用標記清除的策略來實現的,但JavaScript訪問的COM對象依然是基于引用計數的策略的。說白了,只要IE中涉及COM對象,就會存在循環引用的問題。看看下面的這個簡單的例子:

var element = document.getElementById("some_element");
var myObj = new Object();
myObj.element = element;
element.someObject = myObj;

上面這個例子中,在一個DOM元素(element)與一個原生JavaScript對象(myObj)之間建立了循環引用。其中,變量myObj有一個名為element的屬性指向element;而變量element有一個名為someObject的屬性回指到myObj。由于循環引用,即使將例子中的DOM從頁面中移除,內存也永遠不會回收。

不過上面的問題也不是不能解決,我們可以手動切斷他們的循環引用

myObj.element = null;
element.someObject = null;

內存管理

使用JavaScript編程,我們一般都不需要管內存回收的問題,如果說想要寫出高水平的代碼還是有點問題值得注意。一個主要問題就是分配給WEB瀏覽器的可用內存通常比分配給桌面應用程序要少。這樣做的目的主要是出自于安全方面的考慮,目的是防止運行JavaScript的網頁耗盡全部系統內存導致系統崩潰。內存限制問題不僅會影響給變量分配內存,同時還會影響調用棧以及在一個線程中能夠同時執行的語句的數量。

因此,確保占用最少的內存可以讓頁面獲得更好的性能。而優化內存占用的最佳方式,就是執行中的代碼只保存必要的數據。一旦數據不在有用,最好通過將其值設置為null來釋放其引用——這個做法叫解除引用。這一做法適合于大多數全局變量和局部變量的屬性。局部變量會在他們離開執行環境的時候自動被解除引用,下面來看看代碼:

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

在這個例子中,變量globalPerson取得了createPerson()函數的返回值。在createPerson()函數內部,我們創建了一個對象并將其值賦給局部變量localPerson,然后又為局部變量添加了一個名為name 的屬性。最后,當調用這個函數的時候,localPerson以函數值的形式返回并賦值給globalPerson。由于localPerson在createPerson()函數執行完畢后就離開了執行環境,因此無需我們顯示地去為他們解除引用。但是對于globalPerson而言,則需要我們不使用它的時候手動為他解除引用。

不過,解除一個值的引用并不意味著自動回收該值所占的內存。解除引用的真正作用是讓值脫離執行環境,以便垃圾收集器下次運行時將其回收。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容