首頁  >  文章  >  web前端  >  JavaScript詳細解析之作用域鏈

JavaScript詳細解析之作用域鏈

WBOY
WBOY轉載
2022-11-03 16:02:482146瀏覽

本篇文章為大家帶來了關於JavaScript的相關知識,其中主要介紹了作用域鏈的相關內容,作用域是一套規則,負責收集並維護由所有聲明的標識由符(變數)組成的一系列查詢,並實施一套非常嚴格的規則,確定目前執行的程式碼對這些標識符的存取權限;下面一起來看一下,希望對大家有幫助。

JavaScript詳細解析之作用域鏈

【相關推薦:JavaScript影片教學web前端

1. 作用域是什麼?

作用域是一套規則,負責收集並維護由所有宣告的識別碼(變數)組成的一系列查詢,並實施一套非常嚴格的規則,確定目前執行的程式碼對這些識別符的存取權限。

用更好理解的話闡述作用域是什麼,則是:

  • #作用域首先是一套規則
  • 此規則的用處是用來儲存變量,以及如何有限制的取得變數。

用一個不一定完全恰當的形容來類比作用域就是,存在一個國際銀行,你將手裡各國的貨幣存入其中,當你要取出一些錢時,它有一套規則限定你只能在可使用貨幣的當地才能取出對應的該貨幣。 這個銀行和它所製定的規則,就是作用域對於變數的作用。

2. 詞法作用域與動態作用域

在JavaScript中,所使用的作用域是詞法作用域,也稱為靜態作用域。它是在編譯前就確定的。 JavaScript本質上是一門編譯型語言,只不過它編譯發生的時間節點是在程式碼執行前的幾微秒。有別於其他編譯型語言是在建置的過程中編譯,所以才看起來更像是解釋型語言。

關於編譯和解釋暫且不論,只需要理解到詞法作用域就是靜態作用域。理解靜態的意義就是當程式碼在書寫下定義時就已經確定了。也就是人所讀到程式碼中變數和函數被定義在什麼範圍,該範圍就是它們的作用域。

用一個簡單的例子來理解:

var a = 1function foo() {  console.log(a)
}function bar(b) {  var a = 2
  console.log(a)  foo()  function baz() {      console.log(b)
  }  return baz
}var c = bar(a)c()

對於定義的三個函數foo, bar, baz以及變數a,它們在書寫時作用域就已經定義。

因而在程式碼執行時, bar函數先呼叫傳入變數a的值, 當第一個輸出變數a值時,會先詢問自身作用域是否定義過變數a, 定義過則詢問是否存在a的值,存在輸出變數a為2.

然後開始呼叫foo函數, foo只有輸出變數a的值, 同樣也會詢問自身作用域是否定義過變數a, foo中未定義, 則會往上尋找自身定義時的作用域詢問是否定義過變數a, 全域作用域定義過且存在a值, 因而輸出a為1。其實這其中已經牽涉到了作用域鏈,但暫且不議。

之後進入c函數呼叫也就是baz函數, baz中輸出變數b的值,b會詢問自身作用域是否存在定義過變數b, baz未定義,則往上尋找自身定義時的作用域也就是bar函數作用域是否定義過變數b, bar實際隱含在參數中為變數b定義且賦值為1, 因而最終輸出為1。

這就是靜態作用域,只需要看變數和函數的書寫位置,即可確定它們都作用域範圍。

與之相對的是動態作用域, 在JavaScript中涉及到動態作用域的只有this指向,這在之後複習this時會涉及。

假設JavaScript是動態作用域,同樣看上述範例裡的程式碼執行過程。

bar先呼叫並傳入變數a, 當第一個輸出變數a值時, 完全取得到變數a因而輸出2。呼叫foo函數時, 由於自身作用域沒有變數a,則會從自身被呼叫的位置的作用域去往上查找,則此時為函數bar的作用域,因而輸出的a值為2。

c函數呼叫也就是baz函數呼叫時,也同樣是自身不存在變數b,去尋找自身被呼叫的位置的作用域,也就是全域作用域,全域作用域中同樣未定義過變數b, 則直接報錯。

3. 作用域的分類

作用域的分類可以依照上述所說的靜動分為:

  • 靜態作用域
  • 動態作用域

在靜態作用域也就是詞法作用域中還可以依照一定的範圍細分為:

  • 全局作用域
  • 函数作用域
  • 块级作用域

3.1 全局作用域

全局作用域可以理解为所有作用域的祖先作用域, 它包含了所有作用域在其中。也就是最大的范围。反向理解就是除了函数作用域和被{}花括号包裹起来的作用域之外,都属于全局作用域

3.2 函数作用域

之所以在全局作用域外还需要函数作用域,主要是有几个原因:

  • 可以存在一个更小的范围存放自身内部的变量和函数,外部无法访问
  • 由于外部无法访问,所以相当于隐藏了内部细节,仅提供输入和输出,符合最小暴露原则
  • 同时不同的函数作用域可以各自命名相同的变量和函数,而不产生命名冲突
  • 函数作用域可以嵌套函数作用域,就像俄罗斯套娃一样可以一层套一层,最终形成了作用域链

用一个例子来展示:

var name = 'xavier'function foo() {  var name = 'parker'
  var age = 18

  function bar() {    var name = 'coin'
    return age
  }  return bar()
}foo()console.log(age) // 报错

当代码执行时, 最终会报错表示age查找不到。 因为变量age是在foo函数中定义, 属于foo函数作用域中, 验证了第一点外部无法访问内部。

而当代码只执行到foo函数调用时, 其实foo函数有执行过程, 最终是返回了bar函数的调用,返回的结果应该是18。 在对于编写代码的人来说,其实只需要理解一个函数的作用是什么, 然后给一个需要的输入,最后得出一个预期所想的输出,而不需要在意函数内部到底是怎么编写的。验证了第二点只需要最小暴露原则。

在这代码中, 对name变量定义过三次, 但每次都在各自的作用域中而不会产生覆盖的结果。在那个作用域里调用,该作用域就会返回相应的值。这验证了第三点规避命名冲突。

最终bar函数是在foo函数内部定义的,foo函数获取不到bar内部的变量和函数,但是bar函数可以通过作用域链获取到其父作用域也就是foo里的变量与函数。这验证了第四点。

3.3 块级作用域

块级作用域在ES6之后才开始普及,对于是var声明的变量是无效的,仅对let和const声明的变量有效。以{}包裹的代码块就会形成块级作用域, 例如if语句, try/catch语句,while/for语句。但声明对象不属于。

let obj = {  a: 1,   // 这个区域不叫做块级作用域}  

if (true) {  // 这个区域属于块级作用域
  var foo = 1
  let bar = 2}console.log(foo)  // 1console.log(bar)  // 报错

用一个大致的类比来形容全局作用域,函数作用域和块级作用域。一个家中所有的范围就称为全局作用域,而家中的各个房间里的范围则是函数作用域, 甚至可能主卧中还配套有单独的卫生间的范围也属于函数作用域,拥有的半开放式厨房则是块级作用域。

假设你要在家中寻找自己的猫,当它在客厅中,也就是全局作用域里,你可以立马找到。但如果猫在房间里,而没发出声音。你在客厅中是无法判断它在哪里,也就是无法找到它。这就是函数作用域。但是如果它在半开放式厨房里,由于未完全封闭,它是能跑出来的,所以你还是能找得到它。 反之你在房间里,如果它也在,那么可以直接找到。但如果你在房间而它在客厅中,则你可以选择开门去客厅寻找,一样也能找到。

4. 执行上下文和作用域的关系

上述的过程过于理论化,因而现在通过对于实质的情况也就是内存中的情况来讨论。

之前上一篇说过在ES3中执行上下文都有三大内容:

  • 变量对象
  • 作用域链
  • this

实际在内存中,对于全局作用域来说,它所涵盖的范围就是全局对象GO。因为全局对象保存了所有关于全局作用域中的变量和方法。

而对于函数来说,当函数被调用时所创建出的函数执行上下文里的活动对象AO所涵盖的范围就是函数作用域, 并且函数本身存在有一个内部属性[[scope]], 它是用来保存其父作用域的,而父作用域实际上也是另一个变量对象。

对于块级代码来说,就不能用ES3这套来解释,而是用ES6中词法环境和变量环境来解释。块级代码会创建出块级执行上下文,但块级执行上下文里只存在词法环境,不存在变量环境,因而这词法环境里的环境记录就是块级作用域。

相同的解释对于全局和函数也一样,对于ES6中,它们执行上下文里的词法环境和变量环境的环境记录涵盖的范围就是它们的作用域。

用一段代码来更好的理解:

var a = 'a'function foo() {    let b = 'b'
    console.log(c)
}if (true) {    let a = 'c'
    var c = 'c'
    console.log(a)
}foo()console.log(a)

对于这段代码刚编译完准备开始执行,也就是代码创建时,此刻执行上下文栈和内存中的图为:

当开始进行到if语句时,会创建块级执行上下文,并执行完if语句时执行上下文栈和内存图为:

当if语句执行完后, 就会被弹出栈,销毁块级执行上下文。然后开始调用foo函数,创建函数执行上下文,此时执行栈和内存图为:

当foo执行时,变量b被赋值为'b',同时输出c时会在自身环境记录中寻找,但未找到,因而往上通过自身父作用域,也就是全局作用域的环境记录中寻找,找到c的值为'c',输出'c'。

5. 作用域链

通过上文阐述的各个知识点,作用域链就很好理解了,在ES3中就是执行上下文里其变量对象VO + 自身父作用域,然后每个执行上下文依次串联出一条链路所形成的就是作用域链。

而在ES6中就是执行上下文里的词法环境里的环境记录+外部环境引用。外部环境引用依次串联也会形成一条链路,也属于作用域链。

它的作用在于变量的查找路径。当代码执行时,遇到一个变量就会通过作用域链不断回溯,直到找到该值又或者是到了全局作用域这顶层还是不存在,则会报错。

以及之后关于闭包的产生,也是由于作用域链的存在所导致的。这会在之后的复习里涉及到。

6. 一些练习

6.1 自己设计一道简单的练习题

var a = 10let b = 20const c = {  d: 30}function foo() {  console.log(a)  let e = 50
  return b + e
  a = 40}function bar() {  console.log(f)  var f = 60
  let a = 70
  console.log(f)  return a + c.d}if (a <= 30) {  console.log(a)  let b = foo()  console.log(b)
} 

console.log(b)
c.d = bar()console.log(a)console.log(c.d)

【相关推荐:JavaScript视频教程web前端

以上是JavaScript詳細解析之作用域鏈的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.im。如有侵權,請聯絡admin@php.cn刪除