最近學了很多東西,發現 js 的很多內容只是憑藉着我的 主觀意識,或者說 感覺。所以這需要系統地學習 以及整理一下
1、變量提升
首先要明確的一點是,在 es6 出現以前,js 只有五種作用域 ,
- 全局作用域
- 函數作用域
- try/catch 作用域
- eval 作用域
- with 作用域 (在 vue 模板語法的編譯之中,就廣泛地使用到了 這個)
後面三種 不談,就談談 前面兩個
console.log(name) // function name
console.log(age) // undefined
function name() {}
var name = 'dong'
var age = 24
可以看到,在這種情況下,就出現了 變量提升
- 在很多文章中,都提到了 變量提升是爲了解決 函數 調用的問題,但是 var 又是怎麼回事呢?
- 首先 js 在執行之前,會先進行一遍 編譯
- 編譯的過程中,並不會執行代碼
- 像上面的代碼所示,會 解析到 name ,那麼會 將 name 保存的環境變量中,但是 當前是一個函數,有 較高的優先級,會覆蓋掉 var 的undefined 定義,函數體 也被放到了 環境變量中
- 第二個 var name 就如 第四點 提到的,優先級 比函數低,所以內部的存儲 依舊是 function
- 到了 age ,則是將 age 放到環境變量中,並且默認賦值 爲 undefined
- 然後開始執行,輸入如代碼中所示
2、let 和 const
正是因爲 var 的變量提升以及 缺少 作用域 ,所以出現了 let 和const
- let 和 const 在 { } 中是存在作用域的,哪怕 那只是一個 空的 { }
- 在 上一點 提到的 編譯過程之中,其實 let 和 const 在 執行之前也被 編譯到了,那麼 這個是如何做到 暫時性死區 和 作用域的呢?
- 在 編譯到 let 和 const 之後,對應的值 就會被 放入 詞法環境(相當於單獨的環境變量)中,而不是 通常所說的 環境變量中,然後 引擎 在 執行的過程之中,會 特別 注意這一點,禁止 代碼在 定義之前 進行訪問
- 所以下面代碼的執行結果 就很清晰了
- 當 爲 var 的時候,由於 沒有作用域的影響,var 變量提升了,變成了 全局作用域,5 個 i 指向了 同一個
- 當 爲 let 的時候,由於 作用域的 影響,每一個 {} 都是一個單獨的作用域,所以 出現了 5 個 不同 的 i
3、作用域鏈
講作用域鏈,就肯定會談到 閉包,以及 閉包的原理了
- 閉包的定義,只和 詞法作用域 有關,而和在哪裏執行無關
- 也就是說,一個函數的閉包,只和 你在哪裏定義了 這個函數有關係
- 然後 引用 第二大點 的 let 和 const, 一個 作用域會先 從 詞法環境 裏面找,然後 再到 環境變量中找,一層一層得找下來,直到 匹配 或者 到全局環境 中發現未定義爲止
- 這裏又要講到函數 調用棧的 概念了,也就是 先進後出 的 關係
- 在上文中提到的編譯階段,遇到函數的時候,會快速地 對這個函數 做一次 詞法掃描,然後 將 函數中 用到的值保存到 堆空間中
- 所以 下面 輸出 的是 outer 也是一目瞭然的事情了
3、this
由於 閉包的缺陷(不能 手動 指定 作用域 到底是什麼 ),js 又引入了 this 的概念
- 嚴格模式下,函數調用的時候,內部指向的是 undefined ,而在非嚴格模式下,指向的是 window
- 嵌套函數中的 this 不會繼承 外層函數的 this,這裏就引出了 () => {}, 就像 下圖 Vue 中的 mounted 一樣,內部定義一個函數,結果 this 指向的是 undefined。。。這裏其實就和 第一點說的一樣,內部調用的時候,是 undefined
- 各種操作 this 的方法中,this 的指向 是有 權重的,總的來說,是 obj.fn < fn.bind(obj) / apply / call < new fn()
- 要注意的是,事件綁定(指向 當前 dom),以及 setTimeout 中的this 指向(指向 window)
4、那麼 js 是怎麼被解析的呢?
這裏面就涉及到了 v8引擎的底層實現,我肯定是不知道的,只講講表面
- 正如上文所講的,V8引擎 在執行之前 會編譯一遍代碼,
- 編譯代碼的時候,會將 你的代碼 分成 一個又一個的 token,也就是 最小的不可分割的部分,例如 var 、 = 、 name 等等
- 再將 token 轉化爲 AST 抽象語法樹, 閉包、變量提升 就是在這個階段完成的(大名鼎鼎的 babel、ESlint 就是 基於 AST 語法樹 來進行 編譯的)
- 如果 成功便成 AST ,那麼自然相安無事,如果 變不成,那麼就是 你寫的代碼有問題,出現了語法錯誤,這個時候就會拋出一個錯誤
- 但是 AST 依舊處於高層,所以 V8 會將 語法樹 編譯成爲 字節碼
- 事實上 字節碼 依舊不能直接在 計算機中運行,而是需要 解釋器 便成 機器碼 才行
- 那麼 爲什麼 不直接轉爲 機器碼呢?
- 因爲 機器碼 佔用的內存實在是太多,在 移動端的場景之中,很快就會 把內存給吃掉
- 然後 在執行的 過程之中,V8 引擎 發現有一段代碼執行了多次,那麼 就會把這段代碼 單獨提出來,轉化成 字節碼,保存進內存中
- 這樣 你會發現,一段 js 代碼 會越來越快,瀏覽器佔用的 內存也會越來越高
5、把抽象的理論轉到可視化
前端就是可視化的直接呈現着,那麼有沒有辦法把上面的那些東西可視化呢?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
function n() {
var na = 'inner';
return function () {
debugger
console.log(na)
}
}
var fn = n()
fn()
</script>
</body>
</html>