JS作用域的深入理解

作用域

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

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

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

看個(gè)例子??:

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

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

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

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

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

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

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

執(zhí)行上下文棧

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

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

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

  ECStack = []

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

  ECStask = [
    globalContext
  ]

??例如這段代碼:

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

當(dāng)執(zhí)行一個(gè)函數(shù)的時(shí)候,就會(huì)創(chuàng)建一個(gè)執(zhí)行上下文,并且壓入執(zhí)行上下文棧,當(dāng)函數(shù)執(zhí)行完畢的時(shí)候,就會(huì)將函數(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();

再看兩個(gè)相似的例子??:

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()

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

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

變量對(duì)象

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

因?yàn)椴煌瑘?zhí)行上下文的變量對(duì)象不同,所以來(lái)了解全局上下文的變量對(duì)象函數(shù)上下文的變量對(duì)象。

全局上下文

全局上下文中的變量對(duì)象就是 全局對(duì)象

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

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

函數(shù)上下文

在函數(shù)上下文中,我們用活動(dòng)對(duì)象(activation object, AO)來(lái)表示變量對(duì)象。

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

執(zhí)行過(guò)程

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

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

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

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

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

例子??來(lái)了:

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

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

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

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

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

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

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

作用域鏈

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

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

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

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

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

例子??:

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

函數(shù)激活

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

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

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

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

縷縷順

因?yàn)槿绻苯诱f(shuō)完函數(shù)的作用域就講作用域鏈的話,里面的執(zhí)行上下文就會(huì)懵。所以先說(shuō)的函數(shù)執(zhí)行里面的執(zhí)行上下文才說(shuō)的作用域鏈。有點(diǎn)亂沒(méi)關(guān)系,現(xiàn)在來(lái)縷縷順,當(dāng) js 解釋器開(kāi)始工作的時(shí)候:

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

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

一個(gè)大例子??

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

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. 初始化的同時(shí),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)建活動(dòng)對(duì)象
    • 初始化活動(dòng)對(duì)象 (AO),即加入形參、函數(shù)聲明、變量聲明
    • 將活動(dòng)對(duì)象壓入 checkscope 作用域鏈頂端
    • 同時(shí) 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)建活動(dòng)對(duì)象
    • 初始化活動(dòng)對(duì)象(AO),即加入形參、函數(shù)聲明、變量聲明
    • 將活動(dòng)對(duì)象壓入 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)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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