重學JavaScript【做用域、執行上下文和垃圾回收】

重學JavaScript 篇的目的是回顧基礎,方便學習框架和源碼的時候能夠快速定位知識點,查漏補缺,全部文章都同步在 公衆號(道道里的前端棧)github 上。html

原始值和引用值

在JavaScript中,數據分爲 原始值引用值,原始值就是最簡單的數據,通常也稱爲 值類型,引用值就是由多個值構成的對象,通常被叫作 引用類型。保存原始值的變量是按值訪問的,因此操做的是存儲在變量中的實際值。引用值是保存在內存中的對象,要想改變它,實際上操做的是對該對象的 引用前端

  • 原始值不能添加屬性,引用值能夠添加屬性git

  • 原始值複製給另外一個變量是兩個獨立的棧,引用值複製給另外一個變量是複製的引用地址,對象所在的堆不變。github

  • 對象被傳入方法並改變它的屬性時,對象在外部訪問的仍是原來的值面試

    function setName(obj){
      obj.name = "adc";
      obj = new Object();
      obj.name = "def"
    }
    let person = new Object();
    setName(person);
    console.log(person.name) // abc
    複製代碼

上面可得出,方法傳入的對象實際上是按值傳遞的,內部obj被重寫以後,obj會變成一個指向本地對象的指針,而這個指向本地的對象在函數結束後會被銷燬。數組

執行上下文

在JavaScript中,上下文 的概念特別重要,由於上下文決定了它們能夠訪問哪些數據和行爲。每一個上下文都有一個關聯的變量對象,這個對象裏包括了上下文中定義的全部東西。瀏覽器

  • 全局上下文,也就是最外層的上下文,通常就是 window
  • 函數上下文,執行的時候會被推入到一個上下文棧上,函數執行完畢後,上下文棧會彈出該函數上下文,將控制權返還給以前的執行上下文
  • 上下文是在函數調用的時候纔會生效的

如今咱們來模擬一個執行上下文的行爲:markdown

首先要知道的是,JavaScript的整個執行過程分爲兩個階段:編譯階段(由做用域規則肯定,編譯成可執行代碼),執行階段(引擎完成,該階段建立執行上下文)。閉包

咱們定義一個執行上下文棧是一個數組:ECStack = [],當JavaScript開始解釋執行代碼的時候,首先會遇到全局代碼,因此此時咱們壓入一個全局執行上下文 globalContext,當整個應用程序結束的時候,ECStack纔會被清空,因此ECStack底部永遠會有一個 globalContext。框架

ECStack = [
	globalContext
]
複製代碼

此時,若是碰到了一個函數:

// 要執行下面的函數
function fn(){
  function inFn(){}
  inFn()
}
fn()
複製代碼

那麼執行上下文棧會經歷如下過程:

// 壓棧
ECStack.push(globalContext)
ECStack.push(fnContext)
ECStack.push(inFnContext)

//彈出
ECStack.pop(inFnContext)
ECStack.pop(fnContext)
ECStack.pop(globalCotext)
複製代碼

執行上下文在建立階段,會發生三件事:

  1. 建立變量對象

  2. 建立做用域鏈

  3. this的指向

每一個執行上下文都會分配一個 變量對象(variable object,VO) ,它的屬性由 變量函數聲明 構成,在函數上下文的狀況下,參數列表也會被加入到變量對象中做爲屬性,不一樣做用域的變量對象也不一樣。

注意:只有函數聲明會被加入到變量對象中,而函數表達式不會!

// 函數聲明
function a(){}
typeof a //function

//函數表達式
var a - function fn(){}
typeof fn // undefined

複製代碼

當一個函數被激活的時候,會建立一個活動對象(activation object,AO)並分配給執行上下文,活動對象由 arguments 初始化構成,隨後它會被當作 變量對象 用於變量初始化。

function a(name, age){
  var gender = "male";
  function b() } a("小明", 20) 複製代碼

a 被調用時,在a的執行上下文會建立一個活動對象AO,而且被初始化爲:AO = [arguments],隨後AO又被當作變量對象VO進行變量初始化,此時:VO = [arguments].concat([name.age,gender,b])

通常狀況下變量對象包括:形參,函數聲明,和變量聲明,下面用代碼來表示一下某刻的變量對象都是什麼:

function fn(value){
  console.log(a);
  console.log(inFn);
  var a = 2;
  function inFn(){};
  var c = function() {};
  a = 3;
}
fn(1);
複製代碼

在進入執行上下文後,此時的AO是:

AO = {
	arguments: {
		0: 1,
    length: 1
	}
	value: 1,
  a: undefined,
  b: reference to function inFn(){},
  c: undefined
}
複製代碼

接下來代碼開始執行,執行完後,此時的AO是:

AO = {
	arguments: {
    0: 1,
    length: 1
  },
  value: 1,
  a: 3,
  b: reference to function inFn(){},
  c: reference to FunctionExpression "c"
}
複製代碼

從上面來看,代碼總體的執行順序應該是:

function fn(value){
  var a;
  function inFn(){};
  var d;
  console.log(a);
  console.log(inFn);
  a = 2;
  function inFn(){};
  c = function(){};
  a = 3;
}
複製代碼

每一個時間,只會存在一個激活的變量對象。

做用域

做用域決定了查找變量的方法,JavaScript裏採用的是 靜態做用域動態做用域,靜態做用域是在函數定義的時候纔會被決定,動態做用域是在函數被調用的時候定義,下面是一道經典面試題:

var a = 1;
function out(){
  var a = 2;
  inner();
}
function inner(){
  console.log(a)
}
out()
// 1
複製代碼

做用域和做用域之間是有連接關係的,在查找變量的時候,若是當前上下文沒有找到,就從父級執行上下文的變量對象中找,直到全局上下文。

函數的做用域在建立的時候決定,是由於內部有個屬性叫:[[scope]],它會保留全部的父變量,換句話講,它就是全部父變量對象的層級鏈,咱們能夠從控制檯找到某個函數裏的 [[scope]] ,可是他不表明完整的做用域鏈!

function out(){
  function inner(){}
}
複製代碼

函數建立時,各自的 [[scope]] 爲:

out.[[scope]] = [
	globalContext.VO
]
inner.[[scope]] = [
  outContext.AO,
  globalContext.VO
]
複製代碼

當函數被激活時,進入函數上下文,建立AO後,會將活動對象添加到做用域鏈的頂端,此時執行上下文的做用域鏈,咱們叫 Scope

Scope = [AO].concat([[scope]])
複製代碼

到如今爲止,做用域鏈建立完畢。

下面把執行上下文和做用域結合起來,看一下它的執行過程是怎樣的:

var scope = "global scope";
function fn(){
  var a = "local scope";
  return a;
}
scope();
複製代碼
  1. fn函數被建立,此時fn會維護一個私有屬性[[scope]],把當前環境的做用域鏈初始化到這個[[scope]]上

    fn.[[scope]] = [
    	globalContext.VO
    ]
    複製代碼
  2. 執行fn函數,建立fn的執行上下文,以後fn函數的執行上下文被壓入執行上下文棧

    ECStack = [
      fnContext,
      globalContext
    ]
    複製代碼
  3. fn函數複製內部的[[scope]]屬性,從而建立做用域鏈

    fnContext = {
    	Scope: fn.[[scope]]
    }
    複製代碼
  4. 此時fn的執行上下文和做用域鏈構建完畢,開始用 arguments 建立並初始化活動對象,加入形參,函數聲明和變量聲明

    fnContext = {
      AO: {
    		arguments: {
          length: 0
        },
        a: undefined
      },
      Scope: fn.[[scope]]
    }
    複製代碼
  5. 此時fn內部也構建完畢,開始將本身的活動對象AO壓入本身做用域鏈的頂端

    fnContext = {
      AO: {
    		arguments: {
          length: 0
        },
        a: undefined
      },
      Scope: [AO, [[Scope]]]
    }
    複製代碼

    注意,此時的做用域鏈就包括了 本身的AO 和 前面經過複製內部[[scope]]建立好的做用域鏈

  6. 此時,fn的做用域鏈,變量,執行上下文都完畢了,開始執行fn函數,接下來的每一步就是修改 AO 的值,而後把AO壓棧出棧,最終:

    ECStack = [
    	globalContext
    ]
    複製代碼

    有一個講的比較細的例子在這裏:一道JS面試題引起的思考

垃圾回收

在函數中,局部變量會在函數執行的時候存在,若是函數結束了,變量就不被須要了,它所佔用的內存就能夠釋放出來。經常使用的兩種機制爲 標記清理引用計數

標記清理是最經常使用的,就是每用到一次,該變量就會被標記一次,依次疊加,每不用一次(即離開上下文),標記就會減小一個,依次遞減。

引用計數就是對每一個值的引用作一個記錄,引用一次就加1,瀏覽器記錄的是引用的次數,若是該值引用的變量被其餘值覆蓋了,就減1,當引用數爲0時,釋放內存。

若是對變量引用不當,或者執行的最終做用域沒有釋放掉,那麼它就不會被標記和引用計數,此時就會形成 內存泄漏,又一道經典面試題:

function fn(value){
	return function(name){
		return value + name
	}
}
var fn2 = fn("123");
var name = fn2("小明")
複製代碼

經典閉包題,函數內返回一個匿名函數!咱們再來分析一遍: fn2調用了fn,返回了一個匿名函數,該匿名函數會持有fn函數做用域的VO,包括arguments和value。當fn執行結束被銷燬後,它的VO仍是會一直保存在內存中,它的VO仍然在匿名函數中存在,也就是說這個VO一直被用着,因此瀏覽器的垃圾回收機制不會對它作處理,此刻就成了內存泄漏。

要想避免內存泄漏,經常使用的方法就是:賦值爲null

fn2 = null
複製代碼

強制把fn2內部清空,這樣匿名函數的引用就成了null,此時它所使用的fn的VO就能夠被回收了。

參考連接:

個人公衆號:道道里的前端棧,每一天一篇前端文章,嚼碎的感受真奇妙~

相關文章
相關標籤/搜索