Javascript 變量、做用域和內存問題

基本類型和引用類型的值

基本類型值:簡單的數據段 ,五種基本類型(Number Boolean String Null Undefined)的值都是基本類型值,基本類型的值在內存中大小固定,所以保存在棧內存中。
引用類型值:可能由多個值構成的對象。不能操做引用類型的內存空間。保存在堆內存中。前端

動態的屬性

引用類型值

咱們能夠爲引用類型的值添加、修改、刪除屬性和方法,好比:正則表達式

var cat = new Animal();
cat.name = "cat";
cat.speak = function()  {
  alert(this.name);
};
cat.speak(); // cat
基本類型值

然而爲基本類型的值添加屬性和方法是無效的。算法

var name = "Sue";
name.age = 18;
alert(name.age); //undefined

複製變量值

基本類型值
var num1 = 5;
var num2 = 5;

num1與num2的內存空間是徹底獨立的,對一方的改變不會影響到另外一方。後端

引用類型值
var obj1 = new Object();
var obj2 = obj1;
obj2.name = "Sue";
alert(obj1.name); // Sue

當咱們將對象obj1複製給obj2時,只是建立了一個指針副本,這個指針副本與obj1指向同一個保存在堆內存中的對象。所以改變一方,另外一方也會發生相應的改變。瀏覽器

傳遞參數

實參與形參
var num = 2
function add(num1, num2) {
  return num1 + num2;
}
add(1, num);

在上述代碼中,add(1, num)傳入的參數是實參,而arguments[]老是獲取由實參串起來的參數值,在函數體中的num1 num2是形參,至關於聲明瞭兩個局部變量,指向arguments[0] arguments[1]app

按值傳遞

ECMAScript 中全部函數的參數都是按值傳遞的,把函數外部的值複製給函數內部的參數,就和把值從一個變量複製到另外一個變量同樣(不管是基本類型仍是引用類型)。函數

function changeStuff(num, obj1, obj2)
{
    num = num * 10;
    obj1.item = "changed";
    obj2 = {item: "changed"};
}
var num = 10;
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};
changeStuff(num, obj1, obj2);
console.log(num);   // 10
console.log(obj1.item);    // changed
console.log(obj2.item);    // unchanged

以上的例子是怎麼說明ECMAScript中函數的參數都是按值傳遞的呢?
首先基本數據類型,全局變量num複製自身給參數num,兩者是徹底獨立的,改動不會相互影響。
關於對象,咱們彷佛看到了函數內部對obj1對象屬性的改動反應到了函數外部,而obj2從新賦值爲另外一個對象卻沒有反應到外部,這是爲何呢?書中解釋得有點簡單,筆者找了一下資料,原來傳入對象的時候,其實傳入的是對象在內存中的地址,當在函數中改變對象的屬性時,是在同一個區域進行操做,因此會在函數外反映出來,然而,若是對這個局部變量從新賦值,內存中的地址改變,就不會對函數外的對象產生影響了,這種思想稱爲 call by sharing。性能

 However, since the function has access to the same object as the caller (no copy is made), mutations to those objects, if the objects are  mutable, within the function are visible to the caller, which may appear to differ from call by value semantics. Mutations of a mutable object within the function are visible to the caller because the object is not copied or cloned — it is shared. Wikipedia

檢測類型

哪一種基本數據類型

使用typeof能夠辨認String Number Undefined Boolean Object還有函數。測試

typeof("name"); //string
typeof(18); //number
typeof(undefined); //undefined
typeof(null); //object
typeof(true); //boolean
typeof(new Array()); //object
typeof(Array); //function

正則表達式在某些瀏覽器中typeof返回結果爲object,某些返回function優化

什麼類型的對象

instanceof能夠判斷是不是給定類型的實例

var a = new Array;
a instanceof Array; //true

使用instanceof測試基本數據類型時,用於返回false

執行環境和做用域

執行環境(execution context)

定義了變量或函數有權訪問的其餘數據。每一個執行環境都有一個與之關聯的變量對象(variable object),環境中定義的全部變量和函數都保存在這個對象中。

全局執行環境

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

函數執行環境

當執行流進入一個函數時,函數的環境就會被推入一個環境棧中。而在函數執行以後,棧將其環境彈出,將控制器返還給以前的執行環境。

做用域鏈(scope chain)

當代碼在一個環境中執行時,會建立變量對象的一個做用域鏈,以保證對執行環境有權訪問的全部變量和函數的有序訪問。做用域鏈的前端,始終都是當前執行的代碼所在環境的變量對象,對於全局執行環境,就是window對象,對於函數執行環境,就是該函數的活動對象。做用域鏈的後續,是該函數對象的[[scope]]屬性(全局執行環境沒有後續)。

函數對象

在一個函數被定義時,會建立這個函數對象的[[scope]]屬性,指向這個函數的外圍。

活動對象

在一個函數被調用時,會建立一個活動對象,首先將該函數的形參和實參(arguments)添加進該活動對象,而後添加函數體內聲明的變量和函數(提早聲明,在剛進入該函數執行環境時,值爲undefined),這個活動對象將做爲該函數執行環境做用域鏈的最前端。
關於JS的提早聲明機制,咱們舉個例子證實一下:

function add (num1){
    console.log(num2);
    var num3 = 4;
    return num1 + num2;
}
add(1); //undefined 5

上述代碼中,咱們在變量聲明前使用它,卻沒有跑出ReferenceError,說明函數執行時,一開始,num2就已經聲明瞭。

內部環境能夠經過做用域鏈訪問全部的外部環境,但外部環境不能訪問內部環境中的任何變量和函數。這些環境之間的聯繫是線性、有次序的。每一個環境均可以向上搜索做用域鏈,以查詢變量和函數名;但任何環境都不能經過向下搜索做用域鏈而進入另外一個執行環境。
咱們舉一個例子,順便理解一下前面的概念:

var num = 10;
function add (num1, num2) {
  function preAdd(num) {
    var pre = 1;
    return pre  + num;
  num1 = preAdd(num1);
  var result = num1 + num2 + num;
  return result;
}
add(10, 20);

開始執行add()時,首先建立一個執行上下文,而後建立一個活動對象,將arguments num1 num2 pre preAdd() result保存在活動對象中,並將活動對象放在做用域鏈的前端,執行上下文取得add保存的[[scope]],並將其放入做用域鏈的後端,而後執行到preAddpreAdd建立一個執行上下文,並壓入棧頂,建立一個活動對象,保存arguments num pre ,放在做用域鏈的前端,取得preAdd[[scope]],放入做用域鏈的後端。當編譯器開始解析pre時,首先從preAdd做用域鏈的前端開始找,找到了馬上中止。當編譯器開始解析result = num1 + num2 + num,因爲在add的做用域鏈前端(局部變量)中沒有該變量,所以繼續在做用域後端中尋找,並最終在全局變量中找到了num

延長做用域鏈

with

在塊做用域內,將指定變量放在做用域鏈的前端

try-catch

建立一個新的變量對象,其中包含的是被拋出的錯誤對象的聲明,將這個對象放在做用域鏈的最前端,catch執行結束後,做用域鏈恢復。

沒有塊級做用域

ECMAScript中沒有塊級做用域,所以塊的執行環境與其外部的執行環境相同。

聲明變量

使用var 聲明的變量會自動被添加到最接近的環境中。若是初始化變量時沒有使用var 聲明,該變量會自動被添加到全局環境(嚴格模式下,這樣寫會拋錯)。

查詢標識符

當對一個變量進行讀取或修改操做時,咱們首先要搜索到它,搜索的順序如圖:
標識符解析是沿着做用域鏈一級一級地搜索標識符的過程。搜索過程始終從做用域鏈的前端開始,而後逐級地向後回溯,直至找到標識符爲止(若是找不到標識符,一般會致使錯誤發生)。

性能

垃圾收集

Javascript具備自動垃圾收集機制,週期性地回收那些再也不使用的變量,並釋放其佔用的內存。

標記清除(mark-and-sweep)

這是Javascript中最經常使用的垃圾收集方式,當變量進入環境時,將其標記爲「進入環境」,離開環境時,標記爲「離開環境」。理論上,不能夠回收標記爲「進入環境」的變量。

可使用任何方式來標記變量。好比,能夠經過翻轉某個特殊的位來記錄一個變量什麼時候進入環境,或者使用一個「進入環境的」變量列表及一個「離開環境的」變量列表來跟蹤哪一個變量發生了變化。說到底,如何標記變量其實並不重要,關鍵在於採起什麼策略。

引用計數(reference counting)

不太常見,跟蹤記錄每一個值被引用的次數。
當聲明瞭一個變量並將一個引用類型值賦給該變量時,則這個值的引用次數就是1。若是同一個值又被賦給另外一個變量,則該值的引用次數加1。相反,若是包含對這個值引用的變量又取得了另一個值或當它們的生命期結束的時候,要給它們所指向的對象的引用計數減1。當這個值的引用次數變成0 時,則說明沒有辦法再訪問這個值了,於是就能夠將其佔用的內存空間回收回來。

var a = new Cat(); // 1
var b = a; // 2
var c = b; // 3
b = new Dog(); // 2
c = new Fox(); // 1
a = new Object(); // 0

這樣看起來,引用計數法彷佛沒什麼問題,然而,當遇到循環引用時,就跪了。。。

var a = new Object(); //a指向的Object的引用次數+1
var b = new Object(); //b指向的Object的引用次數+1
a.another = b; //b指向的Object的引用次數+1
b.another = a; //a指向的Object的引用次數+1

此時,兩個對象的引用次數都爲2,用於都不會變爲0,永遠都不會被GC,浪費內存。
因爲引用計數存在上述問題,所以早在Navigator 4.0就放棄了這一策略,但循環引用帶來的麻煩卻依然存在。
IE 中有一部分對象並非原生JavaScript 對象。例如,BOM 和DOM 中的對象就是使用C++以COM(Component Object Model,組件對象模型)對象的形式實現的,COM的垃圾回收策略是引用計數法,所以只要涉及到COM對象,就會存在循環引用的問題,舉一個例子:

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

IE9 把BOM 和DOM 對象都轉換成了真正的JavaScript 對象。這樣,就避免了
兩種垃圾收集算法並存致使的問題。

管理內存

因爲系統分配給瀏覽器的內存比較小(比桌面應用小),而內存限制勢必會影響網頁性能,所以Javascript中,優化內存佔用是一個必要的問題,最佳方式就是隻保留必要的數據。局部變量會在離開執行環境後自動解除引用,然後被GC,所以咱們只需在再也不須要某個全局變量時,將其設爲null,來解除它對內存的引用(即解除引用dereferencing),適用於大多數全局變量和全局對象的屬性。
針對上一節的例子,咱們可使用一樣的方法:

myObject.element = null;
element.someObject = null
相關文章
相關標籤/搜索