聲明 本系列文章內容全部梳理自以下幾個來源: 《JavaScript權威指南》 "MDN web docs" "Github:smyhvae/web" "Github:goddyZhao/Translation/JavaScript" 作為一個前端小白,入門跟著這幾個來源學習,感謝作者的分享,在其基 ...
聲明
本系列文章內容全部梳理自以下幾個來源:
- 《JavaScript權威指南》
- MDN web docs
- Github:smyhvae/web
- Github:goddyZhao/Translation/JavaScript
作為一個前端小白,入門跟著這幾個來源學習,感謝作者的分享,在其基礎上,通過自己的理解,梳理出的知識點,或許有遺漏,或許有些理解是錯誤的,如有發現,歡迎指點下。
PS:梳理的內容以《JavaScript權威指南》這本書中的內容為主,因此接下去跟 JavaScript 語法相關的系列文章基本只介紹 ES5 標準規範的內容、ES6 等這系列梳理完再單獨來講講。
正文-作用域
在 ES5 中,變數的作用域只有兩類:
全局作用域
函數作用域
只要不是在函數內部定義的變數,作用域都是全局的,全局的變數在哪裡都可以被訪問到,即使跨 js 文件。
函數作用域是指在函數體定義的變數,不管有沒有在函數體的開頭定義,在函數體的任何地方都可以被使用,因為 JavaScript 中的變數有聲明提前的行為。
函數作用域需要區別於 Java 語言中的塊級作用域:
var i = 0;
function A() {
console.log(i); //輸出undefined
for (var i = 0; i < 1; i++) {}
console.log(i); //輸出1
}
在 Java 中,類似的代碼,在 for 迴圈前後輸出的 i 都會是 0,因為都會使用成員變數 i,for迴圈內定義的 i 由於塊級作用域限制,只在for 迴圈的 {} 大括弧中的代碼有效。
但在 JavaScript 中,變數作用域只分函數作用域,而且變數有聲明提前的特性,所以在函數體內部第一次輸出 i 時,此時變數已經提前聲明,但還沒初始化,所以會是 undefined。而函數內定義的變數的作用域或者說生命周期是整個函數內,所以即使 for 迴圈體語句結束,仍舊可以訪問到 i 變數。
由於允許變數的重覆定義,所以全局變數很容易起衝突,因為無法確保多份 js 文件中是否已經在全局中定義了該變數,一旦起衝突,瀏覽器行為僅僅是將後定義的覆蓋掉前定義的而已,這對於瀏覽器角度沒什麼大問題,但對於程式而已,很容易出現不可控的問題。而且,極難排查。
所以,實際編程中,建議不要過多的使用全局變數,有多種方法可以避免:
- 使用一個全局對象來作為命名空間,將其餘不在函數體內部定義的變數,作為該全局對象的屬性來定義使用。
- 使用一個立即執行的函數來作為臨時命名空間,函數執行結束釋放臨時命名空間。
- 如果臨時命名空間內的部分變數需要供外部使用,一可以將這部分變數添加到作為命名空間的全局對象上的屬性,二可以利用閉包的特性,返回一個新建的對象,為該對象添加一些介面可訪問這部分變數。
全局對象作為命名空間
var DASU = {};
DASU.num = 1;
function a() {
console.log(DASU.num);
}
這裡的全局對象意思是說,數據類型為對象的全局變數,簡稱全局對象,與前端里說的全局對象window是兩個不同概念,區分一下。
其實也就是一種思想,將所有函數外需要定義的變數,都替換成對指定對象的屬性來操作。
立即執行的函數作為臨時命名空間
(function () {
var num = 1;
function a() {
console.log(num);
}
a();
}())
當引入 js 文件到 HTML 時,js 文件中的代碼就會被執行,或者聲明瞭 <script> 標簽後,在標簽內的代碼也會立馬被執行。但函數只有被調用的時候才會執行,所以,如果我們使用一個立即執行的函數,那這個函數體內部的代碼行為就跟正常的 js 文件代碼被執行的行為一致了。
而且,還可以利用函數內作用域這一特點,來保證,在這個立即執行的函數內部定義的變數不會影響到全局變數。
缺點就是函數內部代碼執行結束後,這些在函數內定義的變數就被回收了。所以,如果有些信息需要跨 js 文件通信,此時要麼通過全局對象方式,要麼通過閉包特性來輔助實現。
臨時命名空間內的變數共用方式
全局變數可以在任何地方被訪問,所以可以將那些需要共用給外部使用的臨時命名空間內的變數賦值給全局對象的屬性,即結合第一種:全局對象做命名空間方式。
或者,通過閉包的特性,作為臨時命名空間的立即執行的函數需要有一個返回值,當外部持有這個返回值時,這個函數內的變數就不會被回收。
然後,返回值可以是一個對象,公開一些介面來獲取這些需要共用的變數,如:
var model = (function () {
var num = 1;
function a() {
console.log(num);
}
return {
getNum: function () {
return num;
}
}
}());
model.getNum();
//或者:
var model = (function () {
var num = 1;
function a() {
console.log(num);
}
return {
num:num
}
}());
model.num;
變數的聲明提前原理
看個例子:
var i = 0;
function A() {
console.log(i); //輸出undefined
for (var i = 0; i < 1; i++) {}
console.log(i); //輸出1
}
A();
函數內第一個輸出 undefined 是因為變數的聲明提前,第二個輸出 1 是因為變數作用域為函數作用域,而不是塊級作用域。
那麼,有想過,這些似乎理所當然的基礎常識原理是什麼嗎?
我們先來看些理論,再結合理論返回來分析這個例子,但只分析變數的聲明提前原理,至於作用域的原理留著作用域鏈一節分析。
理論
我們之前有介紹過執行上下文 EC,和變數對象 VO,執行上下文分全局執行上下文和函數執行上下文。在全局執行上下文中,VO 的具體表現是全局對象;在函數執行上下文中,VO 的具體表現是 AO,AO 存儲著函數內的變數:形參、局部變數、函數自身引用、this、arguments。
不管是執行函數代碼還是全局代碼,js 解釋器會分兩個過程,有的文章翻譯成:進入執行上下文階段、執行代碼階段(我不怎麼喜歡這個翻譯)。
進入執行上下文階段:其實本質上就是創建一個執行上下文,這個階段會解析當前上下文內的代碼,將聲明的變數都保存到 VO 對象上。
執行代碼階段:就是代碼實際運行期,當運行到相對應的變數的賦值語句時,就會將具體的屬性值寫入 VO 對象上保存的對應變數。
也就是說,在執行代碼階段,代碼實際運行時,js 解釋器已經解析了一遍上下文內的代碼,並創建了執行上下文,且為其添加了一個 VO 屬性,在 VO 對象上添加了上下文內聲明的所有變數,這就是變數的聲明提前行為。而之後函數體內對各變數的操作,其實是對 VO 上保存的變數進行操作了。
我看過一篇文章對這兩個過程的翻譯是:解析階段、執行階段。
我比較喜歡這種翻譯,解析階段主要的工作就是解析上下文內的代碼,創建執行上下文,創建變數對象 VO 等,為執行階段做準備;而執行階段就是代碼實際運行過程。
分析
var i = 0;
function A() {
console.log(i); //輸出undefined
for (var i = 0; i < 1; i++) {}
console.log(i); //輸出1
}
A();
再回過頭來看這個簡單的例子,假設這段代碼放在一份單獨的 js 文件中,解釋器第一次執行這份代碼,那麼當執行全局代碼時,首先進入全局執行上下文的解析階段:
- 解析代碼創建全局執行上下文
- 創建VO,併為其添加屬性 i、A
- 省略該過程其他工作
- 將創建的全局EC放入ECS棧內
當實際開始執行第一行全局代碼時,js解釋器經過瞭解析階段已經做瞭如上的工作,得到了一些基本的信息。之後便是執行全局代碼,如果執行的代碼是訪問全局變數,那麼直接讀取全局 EC 中 VO 里的對應變數;如果是對全局變數賦值操作,那麼寫入全局 EC 中的 VO 里對應變數的屬性值。
如果執行的代碼是調用某個函數,此時就會為這個函數的執行創建一個函數執行上下文,那麼這個過程同樣需要兩個階段:解析階段和執行階段。
所以當代碼執行到最後一行 A()
時,此時新的函數執行上下文的解析階段做的工作:
- 解析 A() 函數內代碼,並創建函數執行上下文 A函數EC
- 創建 AO,併為其添加屬性
- 省略其他工作介紹
- 將創建的A函數EC放入ECS棧內
所以當執行函數 A 內的代碼時,第一行輸出才會輸出 undefined,因為變數的聲明提前特性在調用函數時創建函數執行上下文的過程中,已經解析了函數內的聲明語句,並將這些變數添加到函數上下文 EC 的 AO 中了。
AO 就是變數對象 VO 在函數執行上下文中的具體表現。
而當執行完 for 迴圈語句,A 函數 EC 中的 AO 里的i屬性已經被賦值為 1 了,而 A 函數 EC 是直到函數執行結束才銷毀,所以即使在 for 語句內定義的 i 變數也可以在後面繼續使用。
以上,就是變數聲明提前的原理,當然,創建執行上下文的過程中,還涉及到其他很多工作,用來實現例如作用域鏈等機制,留待後續來說。
大家好,我是 dasu,歡迎關註我的公眾號(dasuAndroidTv),公眾號中有我的聯繫方式,歡迎有事沒事來嘮嗑一下,如果你覺得本篇內容有幫助到你,可以轉載但記得要關註,要標明原文哦,謝謝支持~