第4章 變數、作用域與記憶體
4.1 變數的原始值和引用值?
-
JavaScript 的變數是鬆散型別的,且變數不過就是特定時間點一個特定值的名稱而已。ECMAScript變數可以包含兩種型別的資料:原始值(primitive value),就是最簡單的資料;引用值(reference value)則是由多個值構成的物件。
-
原始值包含6種資料型別:Undefined、Null、Number、String、Boolean、Symbol。儲存原始值的變數是按值訪問的,操作的就是儲存在變數中的實際值。
-
引用值是儲存在記憶體中的物件。JavaScript不允許直接訪問記憶體位置,因此不能直接操作物件所在的記憶體空間。操作物件時實際上操作的是該物件的引用而非實際的物件本身。為此,儲存引用值的變數是按引用訪問
-
原始值不能有屬性,只有引用值(物件)可以動態新增屬性。
-
原始值複製時,原始值會被複制到新變數的位置,且兩個變數可以獨立使用。引用值複製時,儲存在變數中的值也會被複制到新變數所在的位置,區別在於,這裡儲存在變數中的值實際上是一個指標,複製的也是指標,指向儲存在記憶體中的物件,複製完成後,兩個變數指向的是同一個物件,因此一個物件上的變化會在另一個物件上反映出來。
-
ECMAScript 中所有函式的引數都是按值傳遞的。
function setName(obj){ obj.name = "Nicholas"; obj = new Object();//此處不能加let,因為obj在函式內已經是定義好的區域性變數 obj.name = "Tom"; } let myObj = new Object(); setName(myObj); console.log(myObj.name);//Nicholas,物件進入函式,被修改屬性,出函式後物件保留該屬性
-
typeof 操作符可以確定原始值(除了null)的資料型別,typeof null會返回 object。但由於物件的建構函式不同,如何確定物件的建構函式是什麼呢?即確定物件是哪種型別的物件。這時需要用到
instanceof
操作符。let person = new String(); console.log(person instanceof Object);//true console.log(person instanceof Number);//false; console.log(person instanceof String);//true
4.2 執行上下文和作用域
-
執行上下文就是當前 JavaScript 程式碼被解析和執行時所在環境,每個上下文都有一個關聯的變數物件(variable object)
-
全域性上下文是最外層的上下文。在瀏覽器中,全域性上下文就是我們常說的window 物件,因此所有通過 var 定義的全域性變數和函式都會成為 window 物件的屬性和方法。
-
每個函式都有自己的上下文。當代碼執行流進入函式時,函式的上下文被推到一個上下文棧上,在函式執行完之後,上下文棧會彈出該函式上下文,將控制器返還給之前的執行上下文。
-
上下文中的程式碼在執行的時候,會建立變數物件的一個作用域鏈(scope chain)。這個作用域鏈決定了各級上下文中的程式碼在訪問變數和函式時的順序。每個上下文都可以到上一級上下文中去搜索變數和函式,但任何上下文都不能到下一級上下文中去搜索。
-
重複的var宣告會被忽略,重複的let宣告會丟擲 SyntaxError。let宣告非常適合在迴圈中宣告迭代變數,因為 var 宣告的迭代變數會洩漏到迴圈外部。const 宣告的變數必須同時初始化為某個值,一經宣告,在其生命週期內的任何時候都不能再重新賦予新值。但用const宣告的物件可以給其新增屬性,如果想對 const 宣告的物件不能修改,可以用:
const o = Object.freeze({}); o.name = 'Jake'; console.log(o.name);//undefined
-
識別符號查詢:
var color = 'blue'; function getColor(){ let color = 'red'; let color1 = 'black'; { let color = 'green'; return color; } } console.log(getColor());//'green',如果是return color1,則返回'black'
4.3 垃圾回收
JavaScript 是使用垃圾回收的語言,也就是說執行環境負責在程式碼執行時管理記憶體。JavaScript通過自動記憶體管理實現記憶體分配和閒置資源回收。基本思路:確定哪個變數不會再使用,然後釋放它佔用的記憶體。這個過程是週期性的,即垃圾回收每隔一段時間(或者說某個預定時間)就會自動執行。垃圾回收程式必須跟蹤記錄哪個變數還會使用,哪個變數不會再使用,以便回收記憶體。如何標記未使用的變數也許會有不同的實現方式,不過在瀏覽器的發展史上用到過兩種主要的標記策略:標記清理和引用計數。
-
標記清理(較常用):當變數進入上下文,這個變數會被加上“存在於上下文中”的標記,當變數離開上下文時,也會被加上“離開上下文”的標記。垃圾回收程式執行的時候,會標記記憶體中儲存的所有變數。然後它會將所有在上下文中的變數,以及被上下中的變數引用的其他變數的標記都去掉。在此之後還存在標記的變數就是待刪除的了,原因是任何在上下文中的變數都訪問不了它們了。隨後垃圾回收程式做一次記憶體清理,銷燬帶標記的所有值並收回它們的記憶體。
-
引用計數(不常用):對每個值都記錄它被引用的次數。宣告變數並給它賦一個引用值時,這個值的引用數為1,如果儲存該值引用的變數被其他值覆蓋了,那麼引用數減1。當一個值的引用數為0時,就說明沒辦法再訪問這個值了,因此可以安全地收回其記憶體了。
-
記憶體管理:由於分配給瀏覽器的記憶體通常比分配給桌面軟體的記憶體要少的多,分配給移動瀏覽器的就更少了,這更多出於安全考慮,即為了避免執行大量的JavaScript的網頁耗盡了系統記憶體而導致作業系統崩潰。將記憶體佔有量保持在一個較小的值可以讓頁面效能更好。優化記憶體佔用的最佳手段就是保證在執行程式碼時只儲存必要的資料,將不必要的資料設定為null,從而釋放其記憶體,也叫做解除引用。
function creatPerson(name){ let localPerson = new Object(); localPerson.name = name; return localPerson; } let globalPerson = creatPerson("Nicholas"); globalPerson = null;//解除引用
-
通過const 和 let 宣告提升效能。
-
隱藏類和刪除操作。如果程式碼要求非常注重效能,這點非常重要。
//隱藏類 function article(){ this.title = 'just test'; } let a1 = new article(); let a2 = new article(); a2.author = 'Jake'; //此時兩個article例項對應了兩個不同的隱藏類,因此需要避免這種“先建立再補充”(ready-fire-aim)式的動態屬性賦值,並在建構函式中一次性宣告所有屬性。 function article(opt_author){ this.title = 'just test'; this.author = opt_author; } let a1 = new article(); let a2 = new article('Jake'); //避免動態刪除屬性操作 function article(){ this.title = 'just test'; this.author = 'Jake'; } let a1 = new article(); let a2 = new article(); delete a1.author;//動態刪除屬性操作和動態新增屬性的操作後果一樣,也需要避免 function article(){ this.title = 'just test'; this.author = 'Jake'; } let a1 = new article(); let a2 = new article(); a1.author = null;//用解除引用
-
-
避免記憶體洩漏(意外宣告全域性變數、定時器、JavaScript閉包)
-
靜態分配與物件池。如果某個函式頻繁呼叫,那麼其內部的區域性物件頻繁地賦值釋放,這就會使垃圾回收排程程式不停地來安排垃圾回收。為了避免這種情況,可以在初始化的某一時刻,建立一個物件池,用於管理一組可回收的物件。函式可以向這個物件池請求一個物件、設定其屬性、使用它,然後操作完成後再把它還給物件池。由於沒有在函式內部初始化物件,垃圾回收探測就不會發現有物件更替,因此垃圾回收程式就不會那麼頻繁地執行。