JS作用域的深入理解

作用域

作用域是指程序源代碼中定義變量的區(qū)域,JavaScript 采用詞法作用域(lexical scoping),也就是靜態(tài)作用域。

靜態(tài)作用域 & 動態(tài)作用域

靜態(tài) :因為 JavaScript 采用的是詞法作用域,函數(shù)的作用域在函數(shù)定義的時候就決定了。
動態(tài) :而與詞法作用域相對的是動態(tài)作用域,函數(shù)的作用域是在函數(shù)調(diào)用時才決定的。

看個例子??:

var value = 1
function foo () {
  console.log(value)
}
function bar () {
  var value = 2
  foo()
}
bar()  // 輸出神馬呢?

// 假設(shè):JavaScript采用的是靜態(tài)作用域。
// 那么函數(shù)的作用域在函數(shù)定義時就決定了。
// 執(zhí)行到foo函數(shù),先從foo函數(shù)內(nèi)部查找局部變量value。
// 如果沒有就根據(jù)書寫位置,查找上面一層代碼,也就是value = 1。
// 所以結(jié)果是1。

// 假設(shè):JavaScript采用的是動態(tài)作用域。
// 那么函數(shù)的作用域在函數(shù)調(diào)用時決定。
// 執(zhí)行到foo函數(shù),先從foo函數(shù)內(nèi)部查找局部變量value。
// 如果沒有就從調(diào)用函數(shù)的作用域,也就是bar函數(shù)內(nèi)部查找value變量,也就是value = 2。
// 所以結(jié)果是2。

前面說了:JavaScript采用的是靜態(tài)作用域,所以這個例子的結(jié)果是 1。

如果問到 JavaScript 的執(zhí)行順序,那么直觀的印象就是順序執(zhí)行。然而 JavaScript 引擎并非一行一行的分析和執(zhí)行,而是一段一段的分析執(zhí)行。執(zhí)行一段代碼的時候,會進(jìn)行一個“準(zhǔn)備工作”,比如變量提升、函數(shù)提升。

那這個“一段”是怎么劃分的?怎樣“準(zhǔn)備工作”呢?

當(dāng)執(zhí)行到一個函數(shù)的時候,就會進(jìn)行“準(zhǔn)備工作”,用個更專業(yè)的說法就是“執(zhí)行上下文(execution context)”。

執(zhí)行上下文棧

那可是函數(shù)有很多,怎么管理這么多的執(zhí)行上下文呢?

JavaScript 引擎創(chuàng)建了 執(zhí)行上下文棧(Execution context stack, ECS)來管理執(zhí)行上下文。

模擬執(zhí)行上下文棧的行為,我們定義執(zhí)行上下文棧是一個數(shù)組:

  ECStack = []

當(dāng) JavaScript 開始要解釋執(zhí)行代碼的時候,最先遇到的是全局代碼,所以初始化的時候首先會向執(zhí)行上下文棧壓入一個全局執(zhí)行上下文,用 globalContext 表示,并且只有當(dāng)整個應(yīng)用程序結(jié)束的時候,ECStask才會被清空,所以ECStask最底部永遠(yuǎn)有個 globalContext:

  ECStask = [
    globalContext
  ]

??例如這段代碼:

function fn3 () {
  console.log('fn3')
}
function fn2 () {
  fn3()
}
function fn1 () {
  fn2()
}
fn1()

當(dāng)執(zhí)行一個函數(shù)的時候,就會創(chuàng)建一個執(zhí)行上下文,并且壓入執(zhí)行上下文棧,當(dāng)函數(shù)執(zhí)行完畢的時候,就會將函數(shù)的執(zhí)行上下文從棧中彈出。所以:

// 偽代碼

// fn1()
ECStask.push(<fn1> functionContext)

// fn1調(diào)用了fn2,創(chuàng)建fn2的執(zhí)行上下文
ECStask.push(<fn2> functionContext)

// fn2調(diào)用了fn3,再創(chuàng)建fn3的執(zhí)行上下文
ECStask.push(<fn3> functionContext)

// fn3執(zhí)行完畢
ECStack.pop();

// fn2執(zhí)行完畢
ECStack.pop();

// fn1執(zhí)行完畢
ECStack.pop();

再看兩個相似的例子??:

var scope = 'blobal scope'
function checkscope () {
  var scope = 'local scope'
  function f () {
    return scope
  }
  return f()
}
checkscope()

// 模擬執(zhí)行上下文代碼:
// ECStask.push(<checkscope> functionContext)
// ECStask.push(<f> functionContext)
// ECStack.pop()
// ECStack.pop()
var scope = 'blobal scope'
function checkscope () {
  var scope = 'local scope'
  function f () {
    return scope
  }
  return f
}
checkscope()()

// 模擬執(zhí)行上下文代碼:
// ECStask.push(<checkscope> functionContext)
// ECStack.pop()
// ECStask.push(<f> functionContext)
// ECStack.pop()

對于每個執(zhí)行上下文都有三個重要屬性:

  • 變量對象(Variable object,VO)
  • 作用域鏈(Scope chain)
  • this

變量對象

變量對象是與 執(zhí)行上下文 相關(guān)的數(shù)據(jù)作用域,存儲了在上下文中定義的變量和函數(shù)聲明。

因為不同執(zhí)行上下文的變量對象不同,所以來了解全局上下文的變量對象函數(shù)上下文的變量對象

全局上下文

全局上下文中的變量對象就是 全局對象

執(zhí)行全局代碼時,創(chuàng)建全局執(zhí)行上下文,全局上下文被壓入執(zhí)行上下文棧,然后初始化:

// 壓入執(zhí)行上下文棧
ECStack = [
  globalContext
]
// 全局上下文初始化
globalContext = {
  VO: [global],
  Scope: [globalContext.VO],
  this: globalContext.VO
}

函數(shù)上下文

在函數(shù)上下文中,我們用活動對象(activation object, AO)來表示變量對象。

活動對象是在進(jìn)入函數(shù)上下文時刻被創(chuàng)建的,它通過函數(shù)的 arguments 屬性初始化。arguments 屬性值是 Arguments 對象。

執(zhí)行過程

執(zhí)行上下文的代碼會分成兩個階段進(jìn)行處理:

  1. 進(jìn)入執(zhí)行上下文
  2. 代碼執(zhí)行

進(jìn)入執(zhí)行上下文

當(dāng)進(jìn)入執(zhí)行上下文時,這時候還沒有執(zhí)行代碼,變量對象會包括:

  1. 函數(shù)的所有形參(如果是函數(shù)上下文)
    • 由名稱和對應(yīng)組成的一個變量對象的屬性被創(chuàng)建
    • 沒有實參,屬性值設(shè)為 undefined
  2. 函數(shù)聲明
    • 由名稱和對應(yīng)值(函數(shù)對象function-object)組成一個變量對象的屬性被創(chuàng)建
    • 如果變量對象已經(jīng)存在相同名稱的屬性,則完全替換這個屬性
  3. 變量聲明
    • 由名稱和對應(yīng)值(undefined)組成一個變量對象的屬性被創(chuàng)建
    • 如果變量名稱跟已經(jīng)聲明的形式參數(shù)或函數(shù)相同,則變量聲明不會干擾已經(jīng)存在的這類屬性

例子??來了:

function foo (a) {
  var b = 2
  function c () {}
  var d = function () {}
  b = 3
}
foo(1)

在進(jìn)入執(zhí)行上下文后,這時候的AO是:

AO = {
  arguments: {
    0: 1,
    length: 1
  },
  a: 1,
  b: undefined,
  c: reference to function c () {},
  d: undefind
}

在代碼執(zhí)行階段,會順序執(zhí)行代碼,根據(jù)代碼修改變量對象的值,所以執(zhí)行代碼后的AO是:

AO = {
  arguments: {
    0: 1,
    length: 1
  },
  a: 1,
  b: 3,
  c: reference to function c () {},
  d: reference to FunctionExpression "d"
}

所以變量對象總結(jié)幾句是:

  1. 全局上下文的變量對象初始化是全局對象
  2. 函數(shù)上下文的變量對象初始化只包括 Arguments 對象
  3. 在進(jìn)入執(zhí)行上下文時會給變量對象添加形參、函數(shù)聲明、變量聲明等初始化屬性值
  4. 在代碼執(zhí)行階段,會再次修改變量對象的屬性值

作用域鏈

當(dāng)在查找變量的時候,會先從當(dāng)前上下文的變量對象中查找,如果沒有找到,就會從父級執(zhí)行上下文的變量對象中查找,一直找到全局上下文的變量對象,也就是全局對象。這樣由多個執(zhí)行上下文的變量對象構(gòu)成的鏈就叫做作用域鏈。

下面我們以一個函數(shù)的創(chuàng)建和激活兩個時期來講解作用域鏈?zhǔn)侨绾蝿?chuàng)建和變化的。

函數(shù)創(chuàng)建

上文講到 JavaScript 是靜態(tài)作用域,函數(shù)的作用域在函數(shù)定義的時候就決定了。

這是因為函數(shù)有一個內(nèi)部屬性[[scope]],當(dāng)函數(shù)創(chuàng)建的時候,就會保存所有父變量對象到里面,可以理解為[[scope]]就是所有父變量對象的層級鏈(并不代表完整的作用域鏈)。

例子??:

function foo () {
  function bar () {
    ...
  }
}
// 函數(shù)創(chuàng)建時,各自的[[scope]]為:
// foo.[[scope]] = [
//   globalContext.VO
// ]
// 
// bar.[[scope]] = [
//   fooContext.AO,
//   blobalContext.VO
// ]

函數(shù)激活

當(dāng)函數(shù)激活時進(jìn)入函數(shù)上下文,創(chuàng)建VO/AO后,就會將活動對象添加到作用域鏈的前端。

這時候執(zhí)行上下文的作用域鏈,我們命名為 Scope:

  Scope = [AO].concat([[Scope]])

作用域鏈創(chuàng)建完畢~

縷縷順

因為如果直接說完函數(shù)的作用域就講作用域鏈的話,里面的執(zhí)行上下文就會懵。所以先說的函數(shù)執(zhí)行里面的執(zhí)行上下文才說的作用域鏈。有點亂沒關(guān)系,現(xiàn)在來縷縷順,當(dāng) js 解釋器開始工作的時候:

  • 創(chuàng)建全局執(zhí)行上下文,壓入執(zhí)行上下文棧,并初始化
  • 函數(shù)被創(chuàng)建的時候,就有內(nèi)部屬性作用域鏈 [[scope]]
  • 函數(shù)被調(diào)用時:
    • 創(chuàng)建函數(shù)執(zhí)行上下文,壓入執(zhí)行上下文棧中
    • 函數(shù)執(zhí)行上下文棧初始化:
      • 復(fù)制函數(shù) [[scope]] 屬性創(chuàng)建作用域鏈
      • 用 arguments 創(chuàng)建活動對象
      • 初始化活動對象,即加入形參、函數(shù)聲明、變量聲明
      • 將活動對象 (AO) 壓入作用域鏈頂端
    • 函數(shù)代碼執(zhí)行
  • 函數(shù)執(zhí)行結(jié)束的時候,位于棧頂?shù)膱?zhí)行上下文被彈出,繼續(xù)執(zhí)行新的位于棧頂?shù)膱?zhí)行上下文

這種方式保證了只有位于棧頂?shù)膱?zhí)行上下文才會被執(zhí)行,也就是實現(xiàn)了單線程。

一個大例子??

以下面為例,結(jié)合變量對象和執(zhí)行上下文棧,我們總結(jié)一下函數(shù)執(zhí)行上下文中作用域鏈和變量對象的創(chuàng)建過程:

var scope = 'blobal scope'
function checkscope () {
  var scope = 'local scope'
  function f () {
    return scope
  }
  return f()
}
checkscope()
  1. 執(zhí)行全局代碼,創(chuàng)建全局執(zhí)行上下文,全局執(zhí)行上下文被壓入執(zhí)行上下文棧
  ECStask = [
    blobalContext
  ]
  1. 全局上下文初始化
blobalContext = {
  VO: [vlobal],
  Scope: [globalContext.VO],
  this: globalContext.VO
}
  1. 初始化的同時,checkscope 函數(shù)被創(chuàng)建,保存作用域鏈到內(nèi)部屬性[[scope]]
checkscope.[[scope]] = [
  blobalContext.VO
]
  1. 執(zhí)行 checkscope 函數(shù),創(chuàng)建 checkscope 函數(shù)執(zhí)行上下文,壓入執(zhí)行上下文棧
ECStask = [
  checkscopeContext,
  globalContext
]
  1. checkscope 函數(shù)執(zhí)行上下文初始化
    • 復(fù)制函數(shù) [[scope]] 屬性創(chuàng)建作用域鏈
    • 用 arguments 創(chuàng)建活動對象
    • 初始化活動對象 (AO),即加入形參、函數(shù)聲明、變量聲明
    • 將活動對象壓入 checkscope 作用域鏈頂端
    • 同時 f 函數(shù)被創(chuàng)建,保存作用域鏈到 f 函數(shù)的內(nèi)部屬性 [[scope]]
checkscopeContext = {
  AO: {
    arguments: {
      length: 0
    },
    scope: undefined,
    f: reference to function f () {}
  },
  Scope: [AO, blobalContext.VO],
  this: undefined
}

fscope.[[scope]] = [
  checkscopeContext.AO,
  blobalContext.VO
]
  1. 執(zhí)行 f 函數(shù),創(chuàng)建 f 函數(shù)執(zhí)行上下文,f 函數(shù)執(zhí)行上下文被壓入執(zhí)行上下文棧
ECStask = [
  fContext,
  checkscopeContext,
  globalContext
]
  1. f 函數(shù)執(zhí)行上下文初始化,和第5步相似:
    • 復(fù)制函數(shù)[[scope]]屬性創(chuàng)建作用域鏈
    • 用 arguments 創(chuàng)建活動對象
    • 初始化活動對象(AO),即加入形參、函數(shù)聲明、變量聲明
    • 將活動對象壓入 f 作用域鏈頂端
fContext = {
  AO: {
    arguments: {
      length: 0
    }
  },
  Scope: [AO, checkscopeContext.AO, blobalContext.VO],
  this: undefined
}
  1. f 函數(shù)執(zhí)行,沿著作用域鏈查找 scope 值,返回 scope 值
  2. f 函數(shù)執(zhí)行完畢,f 函數(shù)上下文從執(zhí)行上下文棧中彈出
ECStack = [
  checkscopeContext,
  globalContext
]
  1. checkscope 函數(shù)執(zhí)行完畢,checkscope 執(zhí)行上下文從執(zhí)行上下文棧中彈出
ECStack = [
  globalContext
]
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容