一眼看穿👀JS變量,作用域和內存問題

這篇文章將梳理下環境,作用域鏈,變量對象和活動對象,以及內存管理問題。

基本類型和引用類型的值

我們都知道JS中的數據類型有兩大類,基本數據類型和引用數據類型,下面從三個方面來解剖他們

①保存方式

基本類型的值是指簡單的數據段,引用類型的值是指那些可能由多個值構成的對象。

  • 基本類型

    • 按值訪問
    • 可以直接操作保存在變量中實際的值
  • 引用類型

    • 按引用地址訪問
    • 保存在內存中的對象,而JS不能不允許直接訪問內存中的位置,也就是說不能直接操作對象的內存空間,所以說在實際操作過程中操作的是對象的引用,而不是實際的對象。

②複製變量值

  • 基本類型在複製變量值的時候,會在變量對象上創建一個新值,然後把該值複製到爲新變量分配的位置上。也就是說基礎類型的值複製給新變量後,會在棧內存中開闢一個新的地址空間去存儲值,原值和複製值參與任何操作都互不影響
  • 引用類型在複製變量值的時候,同樣會在棧內存中開闢一個新的地址空間去存儲值,只不過,引用類型複製的是指針,原值和複製值的指針指向同一堆內存中存儲的值,也就是說着兩個變量實際上將引用同一對象,因此改變其中一個變量,就會影響到另一個變量。

③傳遞參數

先了解一個基本原則,ECMAScript中所有函數的參數都是按值傳遞的,千萬不能覺得在局部作用域中修改的對象會在全局作用域中反映出來,就說明參數是按引用傳遞的

根據這個原則,如果參數值是基本類型的,在函數內部修改值,並不會影響到函數外部的值,但如果是引用類型的,參數依舊是值傳遞,只不過傳遞的是棧內存的地址值,因此函數內部的修改會影響到函數外部的值。
下面看一個🌰

let obj_value = {
  a: 1,
  b: 2
}
function func(val) {
  val.a = 3
  val.c = 6
  console.log(val)  // {a: 3, b: 2, c: 6}
}
console.log(obj_value) // {a: 1, b: 2}
func(obj_value)
console.log(obj_value) // {a: 3, b: 2, c: 6}

下面🌰能證明引用類型的參數也是按值傳遞的

function func(obj) {
  obj.a = 1
  obj = {}
  obj.a = 2
}
let test = {}
func(test)
console.log(test.a) // 1

上面的🌰,按照我們理解應該打印出a=2,但事與願違,首先,test在函數func中新增了一個a屬性並賦值爲1,此時,obj中傳遞的是引用類型在棧內存中存儲的地址值,也就是說函數內的obj複製的是test地址,他們兩個共同指向一個對象,因此通過obj新增,修改刪除操作都會反映到函數外部,接下來再看函數內的第二條語句,obj={},這就不得了了,這是重寫,也就是說它會抹去obj原本存儲的地址值,這就切斷了test和obj共同指向一個對象這個聯繫,因此第三條語句,obj.a=2就是函數內部的事情了。

所以總結一句話,引用類型的增刪改操作與其關聯所有對象都會受到波及和影響,重新就會切斷自身與其餘對象的聯繫

檢測類型

typeof()(只適用於基本類型,不適用於對象)

typeof函數可用於檢測string,number,boolean,undefined,function還是symbol,但如果變量的值是引用類型或null,則typeof會返回object

ECMA-262規定任何在內部實現[[call]]方法的對象都應該在應用typeof操作符時返回"function"
對於正則表達式類型的typeof檢測,在IE和Firefox中會返回object,其餘的返回function。

let func = function() {}
console.log(typeof (''))  // string
console.log(typeof (1))   // number
console.log(typeof (true)) // boolean
console.log(typeof (undefined)) // undefined
console.log(typeof ({})) // object
console.log(typeof (null)) // object
console.log(typeof (Symbol(''))) //symbol
console.log(typeof (func)) // function

instanceof(只適用於對象,不適用於基本類型)

instanceof操作符用於判斷是什麼類型的對象
如果變量是給定引用類型的實例,那麼instanceof操作符就會返回true

  • 根據規定,所有的引用類型的值都是Object的實例,因此在檢測一個引用類型值和Object構造函數時,instanceof操作符始終會返回true,
  • instanceof操作符檢測基本類型的值,該操作符始終會返回false,因爲基本類型不是對象。

執行環境及作用域

執行環境也稱爲環境,它定義了變量或函數有權訪問的其他數據,決定了它們各自的行爲。每個執行環境都有一個與之關聯的變量對象,環境中定義的所有變量和函數都保存在這個對象中

全局執行環境是最外圍的一個執行環境,根據ECMAScript實現所在的宿主環境不同,表示執行環境的對象也不一樣,在web瀏覽器中,全局執行環境被認爲是window對象,因此,在瀏覽器中,創建的所有全局變量和函數都是作爲window對象的屬性和方法。

ECMAScript程序中執行流的控制機制

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

作用域鏈

代碼在環境中執行,就會創建變量對象的作用域鏈,作用域鏈的用途,是保證對執行環境有權訪問的所有變量和函數的有序訪問。作用域鏈的前端,始終都是當前執行的代碼所在的環境的變量對象,如果這個環境是函數,則將其活動對象作爲變量對象。什麼是活動對象呢?活動對象實際就是變量對象在真正執行時的另一種形式。活動對象一開始只包含一個變量,即arguments對象。作用域中的下一個變量對象來自外部環境,再下一個變量對象來自下一個環境,層層嵌套,一直延續到全局執行環境,全局執行環境的變量對象始終都是作用域鏈中的最後一個對象

環境的訪問是沿着作用域鏈進行的,作用域鏈是單向的,即由裏到外,內部環境可以訪問外部環境,反之不行。

變量對象和活動對象的概念

變量對象(VO)
變量對象是與執行上下文對應的概念,定義執行上下文中的所有變量,函數以及當前執行上下文函數的參數列表,也就是說變量對象定義着一個函數內定義的參數列表、內部變量和內部函數
clipboard.png
變量對象的內部順序是參數列表->內部函數->內部變量
變量對象的創建過程

  • 檢查當前執行環境的參數列表,建立Arguments對象。
  • 檢查當前執行環境上的function函數聲明,每檢查到一個函數聲明,就在變量對象中以函數名建立一個屬性,屬性值則指向函數所在的內存地址。
  • 檢查當前執行環境上的所有var變量聲明,每檢查到一個var聲明,如果VO(變量對象)中已存在function屬性名,則調過,不存在就在變量對象中以變量名建立一個屬性,屬性值爲undefined。

變量對象是在函數被調用,但是函數尚未執行的時刻被創建的,這個創建變量對象的過程實際就是函數內數據(函數參數,內部變量,內部函數)初始化的過程。

活動對象(AO)
未進入執行階段之前,變量對象中的屬性都不能訪問!但是進入執行階段之後,變量對象轉變爲了活動對象,裏面的屬性都能被訪問了,然後開始進行執行階段的操作。所以活動對象實際就是變量對象在真正執行時的另一種形式。

全局變量對象
我們上面說的都是函數上下文中的變量對象,是根據執行上下文中的數據(參數、變量、函數)確定其內容的,全局上下文中的變量對象則有所不同。以瀏覽器爲例,全局變量對象是window對象,全局上下文在執行前的初始化階段,全局變量、函數都是被掛載倒window上的。
執行上下文的生命週期
clipboard.png

延長作用域鏈

執行環境的類型就兩種——全局和局部(函數)
延長作用域鏈的意思是在作用域鏈的前端臨時增加一個變量對象,該變量對象會在代碼執行後被移除。

延長方法(以下兩個語句都會在作用域鏈的前端添加一個變量對象):

  • try-catch語句的catch塊:會創建一個新的變量對象,其中包含的是被拋出的錯誤對象的聲明。
  • with語句:會指定的對象添加到作用域鏈中
通過with語句延長作用域鏈
function addLink() {
  let name = 'george'
  with(local) {
    var url = href + name // 此時通過with語句將local對象添加到addLink環境的頭部,因此在addLink中就有權可以訪問local對象的屬性和方法
  }
  return url 
}

沒有塊級作用域

JS只有函數作用域和全局作用域,沒有塊級作用域。

聲明變量

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

查詢表示符

當某個環境中爲了讀取和寫入一個標識符時,必須通過搜索來確定標識符實際代表什麼。搜索過程從作用域鏈的前端開始,沿着作用域鏈向上查找,一直追溯到全局環境變量對象,找到標識符,搜索過程停止,反之,返回undefined。

垃圾收集

JS具有自動垃圾收集機制,也就是說,執行環境會負責管理代碼執行過程中使用的內存

收集原理

找出那些不再繼續使用的變量,然後釋放其佔用的內存,爲此垃圾收集器會按照固定的時間間隔(或代碼中預定的收集時間),週期性地執行這一操作。

收集方法

  • 標記清除
  • 引用計數

標記清除(最常用的垃圾收集方式)

原理:垃圾收集器在運行的時候回給存儲在內存中的所有變量都加上標記,然後去掉環境中的變量以及被環境中的變量引用的變量的標記,最後刪除被標記的變量。
標記清除算法將“不再使用的對象”定義爲“無法到達的對象”。即從根部(在JS中就是全局對象)出發定時掃描內存中的對象,凡是能從根部到達的對象,保留。那些從根部出發無法觸及到的對象被標記爲不再使用,稍後進行回收。

引用計數

原理:通過名字很好理解,引用計數,就是跟蹤記錄每個值被引用的次數,當引用次數爲0時,將其刪除。
計數方法:當聲明一個變量並將一個引用類型值賦給該變量時,則這個值的引用次數就是1,如果同一個值被賦給另一個變量,則該值的引用次數加1,相反,包含這個值引用的變量又取的另一個值,則這個值的引用次數減1。

引用計數的嚴重問題——循環引用
循環引用指的是對象A中包含一個指向對象B的指針,而對象B中也包含一個指向對象A的引用。

當出現循環引用的時候,引用次數永遠不可能爲0,這會導致內存得不到回收。

解決方法:手動斷開不需要的引用,即,將引用對象置爲null

立即執行垃圾回收函數
IE中:window.CollectGarbage()
Opera7或更高版本:window.opera.collect()

管理內存

  • 分配給web瀏覽器的可用內存數量通常要比分配給桌面應用程序的少
  • 對於瀏覽器而言,確保佔用最少的內存可以讓頁面獲得更好的性能。
  • 優化內存佔用的最佳方式,就是爲執行中的代碼只保存必要的數據,一旦數據不再有用,最好通過將其值設置爲null來釋放其引用,這個做法叫做解除引用。這一做法適用於大多數全局變量和全局對象的屬性,局部變量會在它們離開執行環境時自動被解除引用。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章