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

luweiCN發表於2018-04-08

《JavaScript高階程式設計》學習筆記

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

1. 變數

1.1 資料型別

js中有兩種資料型別的變數:

  • 基本型別:簡單的資料段

Undefined、Null、Boolean、Number和String

  • 引用型別:可能由多個值構成的物件

Object、Array、Date、RegExp、Function等

1.2 基本型別和引用型別的比較

1.2.1 訪問方式

  • 基本型別:按值引用

基本資料型別是按值訪問的,因為可以操作儲存在變數中的實際的值。基本型別值在記憶體中佔據固定大小的空間,因此被儲存在棧記憶體中;

  • 引用型別:根據情況不同

js不允許直接訪問記憶體中的位置,所以當複製儲存著物件的某個變數時,操作的是物件的引用;但是在為物件新增屬性時,操作的是 是實際的物件。引用型別的值是物件,儲存在堆記憶體中;

1.2.2 動態屬性

  • 基本型別:無動態屬性

不能給基本型別的值新增屬性,儘管這樣做不會導致任何錯誤

var name = "Nicholas";
name.age = 27;
alert(name.age); //undefined
複製程式碼
  • 引用型別:動態屬性

只能給引用型別的值新增屬性,也可以刪除或者修改引用型別的值的屬性或方法

var person = new Object();
person.name = "Nicholas";
alert(person.name); //"Nicholas"
複製程式碼

1.2.3 複製變數的值

  • 基本型別:會建立這個值的一個副本

從一個變數向另一個變數複製基本型別的值,會在變數物件上建立一個新值,然後把該值複製 到為新變數分配的位置上

var num1 = 5;
var num2 = num1;
num1 = 3;
console.log(num2); // 5
複製程式碼
  • 引用型別:只會儲存引用型別的值的指標

從一個變數向另一個變數複製引用型別的值,複製的其實是指標,因此兩個變數最終都指向同一個物件;

var obj1 = new Object();
var obj2 = obj1;
obj1.name = "Nicholas";
alert(obj2.name); //"Nicholas"
複製程式碼

1.2.4 傳遞引數

ECMAScript 中所有函式的引數都是按值傳遞的。也就是說,把函式外部的值複製給函式內部的引數,就和把值從一個變數複製到另一個變數一樣。基本型別值的傳遞如同基本型別變數的複製一樣,而引用型別值的傳遞,則如同引用型別變數的複製一樣。

  • 基本型別
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"的新物件。但是,當接下來再訪問 person.name 時,顯示的值仍然是"Nicholas"。這說明即使在函式內部修改了引數的值,但原始的引用仍然保持未變。實際上,當在函式內部重寫 obj 時,這個變數引用的就是一個區域性物件了。而這個區域性物件會在函式執行完畢後立即被銷燬。

1.2.5 檢測型別

  • 基本型別:typeof

typeof 操作符是確定一個變數是字串、數值、布林值,還是 undefined 的最佳工具。使用typeof操作符檢測函式時,該操作符會返回"function"。

var s = "Nicholas";
var b = true;
var i = 22;
var u;
var n = null;
var o = new Object();
alert(typeof s); //string
alert(typeof i); //number
alert(typeof b); //boolean
alert(typeof u); //undefined
alert(typeof n); //object
alert(typeof o); //object
複製程式碼
  • 引用型別:instanceof

instanceof 運算子用來測試一個物件在其原型鏈中是否存在一個建構函式的 prototype 屬性。如果變數是給定引用型別(根據它的原型鏈來識別)的例項,那麼 instanceof 操作符就會返回 true。

person instanceof Object; // 變數 person 是 Object 嗎?
colors instanceof Array; // 變數 colors 是 Array 嗎?
pattern instanceof RegExp; //變數pattern是RegExp嗎?
null instanceof Object; //false 可以區分null和物件
複製程式碼

根據規定,所有引用型別的值都是 Object 的例項。因此,在檢測一個引用型別值和 Object 建構函式時,instanceof 操作符始終會返回 true。當然,如果使用 instanceof 操作符檢測基本型別的值,則該操作符始終會返回 false,因為基本型別不是物件。

[1, 2, 3] instanceof Object; // true
[1, 2, 3] instanceof Array; // true
複製程式碼

2. 作用域

2.1 執行環境(execution context:執行上下文)

2.1.1 執行環境概述

當 JavaScript 程式碼執行一段可執行程式碼(executable code)時,會建立對應的執行上下文(execution context)。全域性執行環境是最外圍的一個執行環境。所有全域性變數和函式都是作為全域性執行環境的屬性和方法建立的。某個執行環境中的所有程式碼執行完 畢後,該環境被銷燬,儲存在其中的所有變數和函式定義也隨之銷燬(全域性執行環境直到應用程式退出——例如關閉網頁或瀏覽器——時才會被銷燬)。在 Web 瀏覽器中,全域性執行環境被認為是 window 物件,在node中,全域性環境為 global 物件。

2.1.2 執行環境的屬性

對於每個執行上下文,都有三個重要屬性:

  • 變數物件(Variable object,VO)

變數物件是與執行上下文相關的資料作用域,儲存了在上下文中定義的變數和函式宣告。

  • 作用域鏈(Scope chain)

當查詢變數的時候,會先從當前上下文的變數物件中查詢,如果沒有找到,就會從父級(詞法層面上的父級)執行上下文的變數物件中查詢,一直找到全域性上下文的變數物件,也就是全域性物件。這樣由多個執行上下文的變數物件構成的連結串列就叫做作用域鏈。

  • this

2.2 延長作用域鏈

有些語句可以在作用域鏈的前端臨時增加一個變數物件,該變數物件會在程式碼執行後被移 除。在兩種情況下會發生這種現象。

  • try-catch 語句的 catch 塊

  • with 語句

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

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

  return url;
}
複製程式碼

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

2.2.3 沒有塊級作用域

JavaScript 沒有塊級作用域,但是 JavaScript 有函式作用域的概念,變數在宣告它的函式體以及這個函式體巢狀的任意函式體內都是有定義的。

if (true) {
  var color = "blue";
} else {  
  var size = "medium";
}

alert(color); //"blue"
alert(size); //"undefined"
複製程式碼

對於有塊級作用域的語言來說,for 語句初始化變數的表示式所定義的變數,只會存在於迴圈的環 境之中。而對於 JavaScript 來說,由 for 語句建立的變數 i 即使在 for 迴圈執行結束後,也依舊會存在 於迴圈外部的執行環境中。

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

alert(i); //10
複製程式碼

使用 var 宣告的變數會自動被新增到最接近的環境中。在函式內部,最接近的環境就是函式的區域性環境;在 with 語句中,最接近的環境是函式環境。如果初始化變數時沒有使用 var 宣告,該變數會自動被新增到全域性環境。

function add(num1, num2) {
  sum = num1 + num2;
  return sum;
}

var result = add(10, 20); //30
alert(sum); //30
複製程式碼

3. 記憶體(垃圾回收)

JavaScript 具有自動垃圾收集機制,也就是說,執行環境會負責管理程式碼執行過程中使用的記憶體。這種垃圾收集機制的原理其實很簡單:找出那些不再繼續使用的變數,然後釋放其佔用的記憶體。為此,垃圾收集器會按照固定的時間間隔(或程式碼執行中預定的收集時間),週期性地執行這一操作。

函式中區域性變數的正常生命週期:區域性變數只在函式執行的過程中存在。而在 這個過程中,會為區域性變數在棧(或堆)記憶體上分配相應的空間,以便儲存它們的值。然後在函式中使 用這些變數,直至函式執行結束。此時,區域性變數就沒有存在的必要了,因此可以釋放它們的記憶體以供 將來使用。在這種情況下,很容易判斷變數是否還有存在的必要;但並非所有情況下都這麼容易就能得 出結論。垃圾收集器必須跟蹤哪個變數有用哪個變數沒用,對於不再有用的變數打上標記,以備將來收 回其佔用的記憶體。

通常有兩個策略來標識無用變數。

3.1 標記清除

JavaScript 中最常用的垃圾收集方式是標記清除(mark-and-sweep)。這個演算法把“物件是否不再需要”簡化定義為“物件是否可以獲得”。這個演算法假定設定一個叫做根(root)的物件(在Javascript裡,根是全域性物件)。定期的,垃圾回收器將從根開始,找所有從根開始引用的物件,然後找這些物件引用的物件……從根開始,垃圾回收器將找到所有可以獲得的物件和所有不能獲得的物件。這個演算法遵循“有零引用的物件”總是不可獲得的,但是相反卻不一定。

3.2 引用計數

另一種不太常見的垃圾收集策略叫做*引用計數(reference counting)。“物件是否不再需要”簡化定義為“物件有沒有其他物件引用到它”。如果沒有引用指向該物件(零引用),物件將被垃圾回收機制回收。引用計數的含義是跟蹤記錄每個值被引用的次數。當宣告瞭一個變數並將一個引用型別值賦給該變數時,則這個值的引用次數就是1。如果同一個值又被賦給另一個變數,則該值的引用次數加 1。相反,如果包含對這個值引用的變數又取得了另外一個值,則這個值的引用次數減 1。當這個值的引用次數變成0時,則說明沒有辦法再訪問這個值了,因而就可以將其佔用的記憶體空間回收回來。這樣,當垃圾收集器下次再執行時,它就會釋放那些引用次數為零的值所佔用的記憶體。

引用計數的問題:迴圈引用

迴圈引用指的是物件 A 中包含一個指向物件 B 的指標,而物件 B 中也包含一個指向物件 A 的引用

function problem(){
  var objectA = new Object();
  var objectB = new Object();
  objectA.someOtherObject = objectB;
  objectB.anotherObject = objectA;
}
複製程式碼

在這個例子中,objectA和objectB通過各自的屬性相互引用;也就是說,這兩個物件的引用次 數都是 2。在採用標記清除策略的實現中,由於函式執行之後,這兩個物件都離開了作用域,因此這種相互引用不是個問題。但在採用引用計數策略的實現中,當函式執行完畢後,objectA和objectB還將繼續存在,因為它們的引用次數永遠不會是 0。假如這個函式被重複多次呼叫,就會導致大量記憶體得不到回收。

objectA.someOtherObject = null;
objectB.anotherObject = null;
複製程式碼

為了避免類似這樣的迴圈引用問題,最好是在不使用它們的時候手工斷開原生 JavaScript 物件與 DOM 元素之間的連線。例如,可以使用下面的程式碼消除前面例子建立的迴圈引用。

3.3 效能問題

垃圾收集器是週期性執行的,而且如果為變數分配的記憶體數量很可觀,那麼回收工作量也是相當大 的。在這種情況下,確定垃圾收集的時間間隔是一個非常重要的問題。

3.4 管理記憶體

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

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

相關文章