JavaScript學習筆記 - 變量、做用域與內存問題

本文記錄了我在學習前端上的筆記,方便之後的複習和鞏固。javascript

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

ECMAScript變量可能包含兩種不一樣數據類型的值:基本類型值和引用類型值。基本類型指的是簡單的數據段,而引用類型值指那些可能由多個值構成的對象。前端

數據類型:

  • 基本類型值:Undefined、Null、Boolean、Number、String;java

  • 引用類型值,也就是對象類型:Object、Array、Function、Date等;瀏覽器

聲明變量時不一樣的內存分配閉包

  • 基本類型值:存儲在棧(stack)中的簡單數據段,它們的值直接存儲在變量訪問的位置。這是由於這些基本類型佔據的空間是固定的,因此能夠將它們存儲在較小的內存區域 - 棧中。這樣存儲更便於迅速查尋變量的值。函數

  • 引用值:存儲在堆(heap)中的對象,也就是說,存儲在變量處的值是一個指針(point),指向存儲對象的內存地址。這是由於:引用值的大小會改變,因此不能把它放在中,不然會下降變量查尋的速度。相反,放在變量的棧空間中的值是該對象存儲在中的地址。地址的大小是固定的,因此把它存儲在中對變量性能無任何負面影響。性能

不一樣的內存分配機制也帶來了不一樣的訪問機制學習

javascript中是不容許直接訪問保存在堆內存中的對象的,也就是說不能直接操做對象的內存空間。因此在訪問一個對象時,首先獲得的是這個對象在堆內存中的地址,而後再按照這個地址去得到這個對象中的值,這就是傳說中的按引用訪問。而原始類型的值則是能夠直接訪問到的。優化

注意:當複製保存着對象的某個變量時,操做的事對象的引用。但在爲對象添加屬性時,操做的是實際的對象spa

複製變量的不一樣

  • 基礎類型值:在將一個保存着基礎類型值的變量複製給另外一個變量時,會將原始值的副本賦值給新變量,此後這兩個變量是徹底獨立的,他們只是擁有相同的value而已。

function addTen(num) {
    num += 10;
    return num;
}
var count = 20;
var result = addTen(count);
console.log(count); //20 沒有變化
console.log(result); //30
  • 引用值:在將一個保存着對象內存地址的變量複製給另外一個變量時,會把這個內存地址賦值給新變量,也就是說這兩個變量都指向了堆內存中的同一個對象,他們中任何一個做出的改變都會反映在另外一個身上。(這裏要理解的一點就是,複製對象時並不會在堆內存中新生成一個如出一轍的對象,只是多了一個保存指向這個對象指針的變量罷了)

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

參數傳遞的不一樣

首先咱們應該明確一點:ECMAScript中全部函數的參數都是按值來傳遞的。可是爲何涉及到基礎類型與引用類型的值時仍然有區別呢,還不就是由於內存分配時的差異。

  • 基礎類型值:只是把變量裏的值傳遞給參數,以後參數和這個變量互不影響。

  • 引用類型值:對象變量它裏面的值是這個對象在堆內存中的內存地址,這一點你要時刻銘記在心!所以它傳遞的值也就是這個內存地址,這也就是爲何函數內部對這個參數的修改會體如今外部的緣由了,由於它們都指向同一個對象呀。或許我這麼說了之後你對書上的例子仍是有點不太理解,那麼請看圖吧:

clipboard.png

因此,若是是按引用傳遞的話,是把第二格中的內容(也就是變量自己)整個傳遞進去(就不會有第四格的存在了)。但事實是變量把它裏面的值傳遞(複製)給了參數,讓這個參數也指向原對象。所以若是在函數內部給這個參數賦值另外一個對象時,這個參數就會更改它的值爲新對象的內存地址指向新的對象,但此時原來的變量仍然指向原來的對象,這時候他們是相互獨立的;但若是這個參數是改變對象內部的屬性的話,這個改變會體如今外部,由於他們共同指向的這個對象被修改了呀!來看下面這個例子吧:(傳說中的call by sharing)

var obj1 = {
  value:'111'
};
 
var obj2 = {
  value:'222'
};
 
function changeStuff(obj){
  obj.value = '333';
  obj = obj2;       
  return obj.value;
}
 
 
var foo = changeStuff(obj1);
 
console.log(foo);// '222' 參數obj指向了新的對象obj2
console.log(obj1.value);//'333'

obj1仍然指向原來的對象,之因此value改變了,
是由於changeStuff裏的第一條語句,這個時候obj是指向obj1的 .
再囉嗦一句,若是是按引用傳遞的話,這個時候obj1.value應該是等於'222'

能夠把ECMAScript函數的參數想象成局部變量

4.1.4 檢測類型

若是變量的值是一個對象null,則typeof操做符會返回"object".

一般咱們並非想知道某個值是對象,而是想知道它是什麼類型的對象。爲此,ECMAScript提供了instanceof操做符;

若是對象是給定引用類型的實例,那麼instanceof操做符就會返回true

console.log(person instanceof Object);  //變量person是Object嗎?
console.log(colors instanceof Array);   //變量colors是Array嗎?
console.log(pattern instanceof RegExp); //變量pattern是RegExp嗎?

根據規定,全部引用類型的值都是Object的實例。在檢查一個引用類型值和Object構造函數時,instanceof操做符始終會返回true

4.2執行壞境和做用域

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

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

當代碼在一個環境執行時,會建立變量對象的一個做用域鏈。做用域鏈的用途,是保證對執行環境有權訪問的全部變量和函數的有序訪問

4.2.1 延長做用域鏈

有些語句能夠在做用域鏈的前端臨時增長一個變量對象,該變量對象會在代碼執行後被移除。有兩種狀況下會發生這種現象。

  • try-catch 語句中的 catch 塊

  • with 語句

對 with 來講,將會指定對象添加到做用域鏈中。對 catch 來講,會建立一個新的變量對象,其中包含的是被拋出的錯誤對象的聲明。

var oMyself = {
    sFirstname: "Aidan",
    sLastName: "Dai"
}

function create(){
    var sLastName = "Wen"
    with(oMyself){
        //將oMyself做爲本身的執行環境
        sAllName = sFirstname +" " + sLastName;
    }
    return sAllName;
}
var sMyName = create();
console.log(sMyName); //Aidan Dai

4.2.2 沒有塊級做用域

對於有塊級做用域的語言來講,for語句初始化變量的表達式所定義的變量,只會存在於循環的環境之中。而對於JavaScript來講,由for語句建立的變量i即便在for循環執行結束後,也依舊會存在於循環外部的執行環境中。

1. 聲明變量
使用var聲明的變量會自動被添加到最接近的環境中。在函數內部,最接近環境的就是函數的局部環境;在with語句中,最接近的環境就是函數環境。若是初始化變量時沒有使用var聲明,該變量會自動被添加到全局環境。

注意:在編寫JavaScript中,不聲明而直接初始化變量時一個錯誤的作法,由於這樣可能會致使意外。在嚴格模式下,初始化未經聲明的變量會致使錯誤。

2.查詢標識符
搜索過程從做用域鏈的前端開始,向上逐級查詢與給定名字匹配的標識符。若是在局部環境找到,搜索過程中止,變量就緒。若是在局部環境中沒有找到該變量名,則繼續沿做用域向上搜索。搜索過程將一直追溯到全局環境的變量對象。在全局環境也沒找到的話則說明該變量還沒有聲明。

var color = "blue";

function getColor() {
    return color;
}

console.log(getColor()); //"blue";

4.3 垃圾收集

JavaScript具備自動垃圾收集機制,也就是說,執行環境會負責管理代碼執行過程當中使用的內存。
JavaScript垃圾回收的機制很簡單:找出再也不使用的變量,而後釋放掉其佔用的內存,可是這個過程不是時時的,由於其開銷比較大,因此垃圾回收器會按照固定的時間間隔週期性的執行。

變量生命週期

什麼叫再也不使用的變量?再也不使用的變量也就是生命週期結束的變量,固然只多是局部變量,全局變量的生命週期直至瀏覽器卸載頁面纔會結束。局部變量只在函數的執行過程當中存在,而在這個過程當中會爲局部變量在棧或堆上分配相應的空間,以存儲它們的值,而後再函數中使用這些變量,直至函數結束(閉包中因爲內部函數的緣由,外部函數並不能算是結束

一旦函數結束,局部變量就沒有存在必要了,能夠釋放它們佔用的內存。貌似很簡單的工做,爲何會有很大開銷呢?這僅僅是垃圾回收的冰山一角,就像剛剛提到的閉包,貌似函數結束了,其實尚未,垃圾回收器必須知道哪一個變量有用,哪一個變量沒用,對於再也不有用的變量打上標記,以備未來回收。用於標記無用的策略有不少,常見的有兩種方式

4.3.1 標記清除

這是JavaScript最多見的垃圾回收方式,當變量進入執行環境的時候,好比函數中聲明一個變量,垃圾回收器將其標記爲「進入環境」,當變量離開環境的時候(函數執行結束)將其標記爲「離開環境」。至於怎麼標記有不少種方式,好比特殊位的反轉、維護一個列表等,這些並不重要,重要的是使用什麼策略,原則上講不可以釋放進入環境的變量所佔的內存,它們隨時可能會被調用的到。

垃圾回收器會在運行的時候給存儲在內存中的全部變量加上標記,而後去掉環境中的變量以及被環境中變量所引用的變量(閉包),在這些完成以後仍存在標記的就是要刪除的變量了,由於環境中的變量已經沒法訪問到這些變量了,最後,垃圾收集器完成內存清除工做,銷燬那些帶標記的值並回收它們所佔用的內存空間。

4.3.2 引用計數

在低版本IE中常常會出現內存泄露,不少時候就是由於其採用引用計數方式進行垃圾回收。引用計數的策略是跟蹤記錄每一個值被使用的次數,當聲明瞭一個變量並將一個引用類型賦值給該變量的時候這個值的引用次數就加1,若是該變量的值變成了另一個,則這個值得引用次數減1,當這個值的引用次數變爲0的時候,說明沒有變量在使用,這個值無法被訪問了,所以能夠將其佔用的空間回收,這樣垃圾回收器會在運行的時候清理掉引用次數爲0的值佔用的空間。

4.3.3 性能問題

垃圾收集器是週期性運行的,並且若是爲變量分配的內存數量很可觀,那麼回收工做量也是至關大的。在這種狀況下,肯定垃圾收集的時間間隔是一個很是重要的問題。

事實上,在有的瀏覽器中能夠觸發垃圾收集過程,但咱們不建議這樣作。在IE中調用window.CollectGarbage()方法會當即執行垃圾收集。在Opera7及更高版本中,調用window.opera.collect()也會啓動垃圾收集例程。

4.3.4管理內存

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

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

//手工解除globalPerson的引用

globalPerson = null;

4.4 小結

基本類型值和引用類型值具備如下特色:

  • 基本類型值在內存中佔據固定大小的空間,所以被保存在棧內存中;

  • 從一個變量向另外一個變量複製基本類型的值,會建立這個值得一個副本;

  • 引用類型的值是對象,保存在堆內存中;

  • 包含引用類型值得變量實際上包含的並非對象自己,而是指向該對象的指針;

  • 從一個變量向另外一個變量複製引用類型的值,複製的實際上是指針,所以兩個變量最終都指向同一個對象;

  • 肯定一個值是哪一種基本類型可使用typeof操做符,而肯定一個值是哪一種引用類型可使用instanceof操做符。
    全部變量(包括基本類型和引用類型)都存在於一個執行環境(也稱爲做用域)當中,這個執行環境決定了變量的生命週期,以及哪一部分代碼能夠訪問其中的變量。

最後,若有錯誤和疑惑請指出,多謝各位大哥

相關文章
相關標籤/搜索