博客 / 詳情

返回

【JS】作用域-1

讀《你不知道的JS》的筆記,有問題請指出。

簡述作用域

每個編程語言的一個最基本的功能,就是可以聲明變量,在變量裏儲存值,更改值,訪問值。
隨之一系列問題產生,這些變量存儲在哪裏,將來需要使用他們的時候如何獲取他們?
這代表我們需要有一套設計良好的規則知道如何存儲這些變量,並如何獲取到他們,而這套規則,被稱為作用域(Scope)。

編譯和執行流程

不僅是在執行時會用到作用域,編譯時也會用到作用域

編譯

我們用 JS 寫的代碼稱為源代碼,是一種人類能看懂的語言,由於計算機只能讀懂 0 和 1(二進制/機器語言),所以如果我們要在計算機上執行我們的代碼,在執行代碼之前,有個編譯過程,目的是將源碼編譯成機器可以理解的機器碼。

一般來説,編譯分為 3 個步驟

  1. 分詞/詞法分析(Tokenizing/Lexing)
    將我們寫的代碼(字符串),分解成單獨的、有意義的代碼塊,這種代碼塊也稱為詞法單元(token),只抽取有意義的部分


    注:分詞≠詞法分析,他們是有細微區別的,區分他們的一個最直接的方式就是,比如 var a = 2這行代碼,當詞法單元生成器(tokenizer)在判斷 var 是單獨的一個詞法單元還是屬於其他詞法單元的一部分時,如果調用的是有狀態(stateful)的解析規則,那麼字符串被轉換成詞法單元的這個過程就被稱為詞法分析,否則,就是分詞。
    待解決:這裏面説的有狀態是什麼是什麼意思?
  2. 解析/語法分析
    將上面已經轉換好的詞法單元轉換成抽象語法樹(AST)
  3. 代碼生成
    將 AST 轉換成機器可以識別的指令,讓機器執行 var a = 2 這一系列任務。

編譯完之後,就是執行代碼了,對於 JS 來説,即使是 var a = 2 這行很簡單的代碼,在經過編譯和執行這兩個步驟時,也會涉及到作用域。

JS 的編譯過程和其他語言的編譯過程有點不同,在編譯的第三步,也就是將抽象語法樹(AST)轉換成機器指令時,當編譯器碰到聲明操作時,如 var a = 2,編譯器會詢問作用域,在當前的作用域內,是否聲明過 a,如果沒有,就會讓作用域在當前作用域內聲明一個 a,否則,忽略該聲明。

從上面可以看出,在 JS 引擎真正執行代碼之前,編譯過程中,編譯器就會將變量先在作用域內聲明好。

待解決:上面用的是 var,那麼我用 let 聲明變量的話,他也會幫我提前在作用域內聲明好嗎?

執行

代碼編譯完成後,就是執行步驟了,執行是由 JS 引擎執行,在執行 var a = 2 這行代碼時,也會涉及到作用域:

便於理解,接下來我們把它們擬人化,每個人工作上都有自己的工作職責,他們也是一樣,對於作用域來説,他的職責就是管理他這塊區域的變量,這個區域裏,存在哪些變量,變量分別存儲的值是多少他都知道,也是他應該知道的,所以在之前的編譯過程中,編譯器在聲明 a 之前,首先跑去問了作用域確認這塊區域裏還沒有變量 a 後,才讓作用域在這塊區域內聲明瞭變量 a。

而編譯完成後,也就是代碼執行過程,JS 引擎這時看到 var a = 2 後,也會先去問作用域,在當前作用域下,是否已經存在變量 a 了呢?

作用域檢查了下當前的他這塊區域內的變量,回答説,“嗯嗯,已經有了,是剛剛編譯的時候編譯器聲明的”。

JS 引擎:“好嘞,既然有了,那我就不用重複聲明瞭,我就賦個值就行了。”

LHS && RHS
JS 引擎在向作用域詢問變量的時候,查詢的方式還可以細分為 LHS 和 RHS,也就是讓作用域查詢這個變量是否存在,還是讓作用域查詢這個變量的值。

var a = 2 
console.log(a)

代碼如上,還是以對話的形式

JS 引擎:作用域大哥,幫我看看你那裏有沒有變量 a 啊,我得給他賦個值。(查看變量容器本身是否存在,屬於 LHS 查詢方式)

作用域:找到了!這傢伙在我這

JS 引擎: 謝謝了,再幫我看看 console 變量的值呢,我找找他裏面有沒有 log 這個方法(查詢 console 變量的值,屬於 RHS 查詢方式)

作用域:有的,console 的值給你了,你看看吧

JS 引擎:好嘞,謝謝,我看一下。。有 log 這個方法!作用域,再幫我看下 a 變量的值呢,雖然我剛剛給他賦值了一個 2,但是我還是得確認一下(查詢變量 a 的值,屬於 RHS 查詢方式)

作用域:嗯嗯,我看了下,他的值還是2

JS 引擎:好的,謝謝!

作用域鏈

有的時候,作用域是嵌套的

// 最外層為全局作用域
var a = 2 

function test() {
// 對於 JS 來説,一個函數會生成一個作用域(先不提 let 生成的塊級作用域)
    console.log(a)
}

test()

對於 test 函數來説,他自己內部就有一個作用域 A,最外層有一個全局作用域。

JS 引擎在調用 test 函數的時候,由於需要打印變量 a,因此向 test 函數內部的作用域 A 求助,問他有沒有看到變量 a,作用域 A 説沒有看到,於是 JS 引擎向作用域 A 外層的作用域(全局作用域)求助,最終在全局作用域裏找到了變量 a。

有時候並不一定當前作用域的外層作用域就是全局作用域,可能還嵌套有其他作用域,但是查詢方式都是一樣的,當前作用域找不到,就沿着嵌套的作用域往外找,直到找到全局作用域。

如果在全局作用域內也找不到這個變量呢?

那就可能會報錯了,但是具體的報錯信息還是有區別的:

  1. 查詢變量本身(LHS 查詢方式)

    • 在非嚴格模式下,找到全局作用域都沒有找到這個變量,全局作用域就會直接幫我們在全局作用域內聲明這個變量,也就是全局變量;
    • 在嚴格模式下,會拋出 referenceError 的錯誤,代表我們想引用的變量不存在。
  2. 查詢變量的值(RHS 查詢方式)
    如果我們對變量的值使用方式有錯,比如 console.log 是一個函數,我們卻想從裏面獲取一個不存在的屬性,如 console.log.test.sss,從一個 undefined 的數據類型上獲取 sss 屬性,就會拋出 typeError 錯誤
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.