javascript:變數、作用域和記憶體問題

南郭竽發表於2018-04-21

javascript讀書筆記

變數、作用域和記憶體問題

1.x 變數型別
var a = 1;
var b = a; // 建立一個新的變數,和新的值
////////////////////

var p = new Object();
var q = p; // 建立一個新的變數,指向 p 指向的記憶體。

為什麼說對於引用型別,引數傳遞是按值傳遞。

var person = new Object();

function setname(obj) {
    obj.name = '張飛';
    obj = new Object();
    obj.name = '王菲';
}

setname(person);

console.log(person.name); // 張飛

輸出結果是 張飛

(這一點上,和java的表現形式相同)

class Person {
    public String name;
}

public class Main {
    public static void main(String[] args) {
        Person p = new Person();
        setName(p);
        System.out.println(p.name); // 張飛
    }


    public static void setName(Person person) {
        person.name = "張飛";
        person = new Person();
        person.name = "王菲";
    }
}

輸出張飛

-<>-

其實這個問題,涉及到兩個點:引數傳遞與變數作用域。

2.x 執行環境
var color = 'blue';
function changeColor() {
    var anotherColor = "red";

    function swapColor() {
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
        // 這裡可以訪問color,anotherColor,tempColor
    }

    swapColor();
    // 這裡可以訪問 color,anotherColor,但不能訪問 tempColor
}
// 這裡能訪問 color,但不能訪問 anotherColor 和 tempColor.

console.log(color);

內部環境可以通過作用域鏈(scope chain)訪問所有外部環境,但是外部環境不能訪問內部環境中的任何變數和函式。這些環境直接的聯絡是線性的,有序的。每個環境都可以向上搜尋作用域鏈,以查詢變數和函式名;但是任何環境都不能通關過向下搜尋作用域鏈而進入另一個執行環境。
對於這裡的 swapColor()而言,其作用域鏈中包含3個物件:swapColor()的變數物件、changeColor()的變數物件和全域性變數物件。swapColor()的區域性環境開始時會先在自己的變數物件中搜尋變數和函式名,如果搜尋不到則再搜尋上一級的作用域鏈。changeColor()的作用域鏈中只包含兩個物件:它自己的變數物件和全域性的變數物件。這也就是說,它不能訪問swapColor()的環境。

2.1.x 延長作用域鏈

雖然說執行環境的型別總共只有兩種–全域性和區域性(函式),但是還是有其他辦法來延長作用域鏈。這麼說是因為有些語句可以在作用域鏈的前端臨時增加一個變數物件,該變數物件會再程式碼執行後被移除。
在兩種情況下會發生這種現象。具體來說,就是當執行流進入下列任何一個語句時,作用域鏈就會得到延長:

  • try-catch語句的catch塊;
  • with語句。

這兩個語句都會在作用域鏈的前端增加一個變數物件。對於with語句來說,會將指定的的物件新增到作用域鏈中,對於catch語句來說,會建立一個新的變數物件,其中包含的是被丟擲的錯誤物件的宣告。

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

在此,with語句接收的是location物件,因此其變數物件中就包含了location物件的所有屬性和方法,而這個變數物件被新增到了作用域鏈的前端。buildUrl()函式中定義了一個變數qs。當在with語句中引用變數href時(實際引用的是location.href),可以在當前執行環境的變數物件中找到。當引用變數qs時,引用的則是在buildUrl()中定義的那個變數,而該變數位於函式環境的變數物件中。至於with語句內容,則定義了一個名為url的變數,因而url就成了函式執行環境的一部分,所有可以作為函式的值被返回。

2.2.x 沒有塊級作用域

Javascript沒有塊級作用域經常會導致理解上的困惑。在其他類C的語言中,由花括號的程式碼塊都有自己的中作用域(如果用ECMAScript的話來講,就是它們自己的執行環境),因而支援根據條件來定義變數。例如,下面的程式碼在Javascript中並不好得到想象中的結果:

iftrue){
    var color="blue";
}

alert(color); // "blue"

這裡是在一個if語句中定義了變數color。如果是在C / C++ / Javacolor會在if語句執行完畢後銷燬。但在Javascript中,if語句中的變數宣告會將變數新增到當前的執行環境(在這裡是全域性環境)中。

  • 宣告變數

使用var宣告的變數會自動被新增到最近的環境中。在函式內容,最近的環境就是函式的區域性環境;在with語句中,最接近的環境是函式環境。如果初始化變數時沒有使用var宣告,該變數會自動被新增到全域性環境。(嚴苛模式下,不使用var宣告而直接使用,會出現語法錯誤。)

2.3.x 管理記憶體

確保佔用最少的記憶體可以讓頁面獲得更好的效能。而優化記憶體佔用的最佳方式,就是為執行的程式碼只儲存必要的資料。一旦資料不再有用,最好通過將其值設定為null來釋放其引用–這個做法叫做解除引用(dereferencing)。這一做法適用於大多數全域性變數和全域性物件的屬性。區域性變數會在它們離開執行環境時自動被解除引用,如下面這個例子所示:

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

var globalPerson = cretePerson('張飛');

// 不再使用該變數時,手動解除 globalPerson 的引用

globalPerson = null;

在這個例子中,變數 globalPerson 取得了createPerson()函式的返回值。在createPerson()函式內部,我們建立了一個物件並將其賦給區域性變數person,然後又為該變數新增了一個名為name的屬性。最後,當呼叫這個函式時,person以函式值的形式返回被賦給全域性變數globalPerson。由於personcreatePerson()函式執行完畢後就離開了其執行環境,因此無需我們顯示地去為它解除引用。但是對於全域性變數globalPerson而言,則需要我們在不使用它的時候手工為它解除引用,這也正是上面例子中最後一行程式碼的目的。

2.4.x 小結

Javascript變數可以用來儲存兩種型別的值:基本型別值和引用型別值。基本型別的值源自5種基本資料型別:Undefined,Null,Boolean,Number,String。基本型別值和引用型別值具有以下特點:

  • 基本型別值在記憶體中佔據固定大小的空間,因此被儲存在棧記憶體中;
  • 從一個變數向另一個變數複製基本型別的值,會建立這個值的一個副本;
  • 引用型別的值是物件,儲存在堆記憶體中;
  • 包含引用型別的值實際上包含的並不是物件本身,而是一個指向該物件的指標;
  • 從一個變數向另一個變數複製引用型別的值,複製的其實是指標,因此兩個變數最終都指向同一個物件;
  • 確認一個值是哪種基本型別可以使用typeof操作符,而確定一個值是哪種引用型別可以使用instanceof操作符。

所有變數(包括基本型別和引用型別)都存在於一個執行環境(也稱為作用域)當中,這個執行環境決定了變數的生命週期,以及哪一部分程式碼可以訪問其中的變數。以下是關於執行環境的幾點總結:

  • 執行環境有全域性執行環境(也稱為全域性環境)和函式執行環境之分;
  • 每次進入一個新的執行環境,都會建立一個用於搜尋變數和函式的作用域鏈;
  • 函式的區域性環境不僅有權訪問函式的作用域變數,而且有權訪問其包含(父)環境,乃至全域性環境;
  • 全域性環境只能訪問全域性環境中定義的變數和函式,而不能直接訪問區域性環境中的任何資料;
  • 變數的執行環境有助於確定應該何時釋放記憶體。

Javascript的垃圾收集例程總結:

  • 離開作用域的值將被自動標記為可以回收,因此將在垃圾收集期間被刪除。
  • “標記清除”是目前主流的垃圾收集演算法,這種演算法是給當前不使用的值加上標記,然後再回收其記憶體。
  • 另一種垃圾收集演算法是“引用計數”,這種演算法的思想是跟蹤記錄所有值被引用的次數。Javascript引擎目前都不再使用這種演算法;但是在IE中訪問非原生Javascript物件(如DOM元素,BOM元素)時,這種演算法仍然可能會導致問題。
  • 當程式碼中存在迴圈引用的現象時,“引用計數”演算法就會導致問題。
  • 解除變數的引用不僅有助於消除迴圈引用現象,而且對垃圾回收也有好處,為了確保有效地回收記憶體,應該及時解除不再使用的全域性物件、全域性物件屬性以及迴圈引用變數的引用。

相關文章