JavaScript 基礎介紹(執行環境、作用域)
JavaScript(通常縮寫為JS)是一種進階的、直譯的程式語言。JavaScript是一門基於原型、函式先行的語言,是一門多範式的語言,它支援物件導向編程,指令式程式設計,以及函式語言程式設計。
編譯式語言 & 直譯式語言
我們寫的原始碼是無法直接被電腦或是瀏覽器閱讀的,在被電腦運行之前須經過“解譯”成電腦看得懂的代碼。
而其中又可分為編譯式語言&直譯式語言。
編譯式語言:編譯式語言編寫完原始碼後會經由編譯器編譯,並在預先編譯過程中除錯,確定無錯誤後再將代碼生成並於執行環境中運行。
Example:C、C++、bjective-C、Visual Basic等等。
直譯式語言:直譯式語言編寫完原始碼後會經由直譯器編譯,並直接將代碼生成並於執行環境中運行。
錯誤直接反映在環境中,像是 console 出現錯誤紅字。
Example:JavaScript、Python、Ruby等等。
JavaScript 是如何運行的
JavaScript是直譯式語言的一種,直譯語言在執行時會一行一行的動態將程式碼直譯(interpret)為機器碼,並執行。
以下是JavaScript經過直譯器轉換的過程。
1. 語法基本單元化(Tokenizing)。
將一串字元拆解成(對該語言)有意義的組塊,這些組塊就叫做語法基本單元化。
2. 組成抽象語法樹AST(Abstract Syntax Tree)。
將token流轉化為一個有元素層級巢狀所組成的代表程式語法結構的樹,這個樹被叫做抽象語法樹AST。
3. 代碼生成。
可透過編譯網站來了解語法單元化(Tokenizing)。
編譯網站
執行的錯誤情境 LHS, RHS
此錯誤與取值與賦予值有相當的關係。
- RHS:將值從右側的變數中取出,當無法取值時,會丟出’ReferrenceError’的錯誤訊息。
- LHS:將值賦予至左側的變數,如果左側不為’identifier’ or 無法被賦予時,會丟出’Invalid left-hand side’的錯誤訊息。
如有出現錯誤需立即修正,否則錯誤後方的程式碼會無法運行。
語法作用域(Lexical scope)
作用域分為靜態作用域以及動態作用域,不同的程式語言可能有不同的作用域,而同一語言內也可能存在多種作用域。
靜態作用域 :在原始碼寫好的時候,作用域就已經被訂下來了,且不會再被改變。
動態作用域 :在函式調用時才決定作用域。
JS採用靜態作用域(又稱語法作用域)。
在JS的作用域中
JS的作用域為一層一層向內,最外層有一全域,再向內包覆Function的作用域,Function的作用域是獨立的。
如Function的作用域內需要調用變數,但此作用域內無此變數時,會向外查找變數,
查找到便會調用此變數,
但如果查找不到此變數時就會出現錯誤 ReferenceError: 變數 is not defined。
EX:以下這段程式碼,會印出什麼結果呢?
var value = 1 ; |
首先,我們宣告了一個變數value, 他是一個全域變數。接著宣告兩個函式,最後執行fn2。
執行fn2時,我們把變數value賦予了另外一個值2,接著執行fn1。fn1因為要印出value,因此向外查找value這個變數,在全域中找到value,值等於1。最後console.log的結果會是1。
JS為語法作用域,console.log的結果會是1。
但如果是動態作用域的話,結果就不一樣了。在執行到console.log(value)時,他會向上一層調用的函式來查找value的值,因此找到值等於2。
執行環境與執行堆疊
執行環境 Execution Context
當函式被執行時,會產生“執行環境 Execution Context”。
每執行一次便會產生一個新的執行環境。
除了函式之外,全域也有全域執行環境,在網頁被瀏覽器開啟或是後端Node.js被啟動時,就被建立了。全域執行環境被建立時會同時宣告window或是global變數。
(在瀏覽器是window, Node.js是global,而在全域執行環境中的this就等同於這兩個變數。this是會隨著執行環境不同而改變的)。
執行堆疊 Execution stack
執行堆疊跟函式在宣告的時候的時候"沒有關聯",而是與"呼叫的位置"有關。
從以下程式碼來看:
function sayHi(name){ |
- 首先,在瀏覽器開啟或是Node.js啟動時,全域執行環境就會被建立。
- 接著我們執行doSomething函式,所以doSomething的執行環境被生成,且堆疊在全域執行環境之上。
- 在doSomething函式中我們又執行了sayHi函式,因此sayHi的執行環境被生成,且堆疊在doSomething執行環境之上。
- 當離開執行堆疊的時候也會一層一層離開,sayHi執行環境先離開並回到doSomething執行環境,最後會回到全域執行環境。
我們可以從chrome 開發者工具中的source tab,一步一步觀察執行堆疊的運作。
範圍鍊 Scope chain
每個變數都有自己的作用範圍,若使用前未宣告,就會變成全域變數,若是在函式內宣告的變數,則只能在該區域內使用,也就是說 JavaScript 在查找變數時,會循著範圍鏈一層層往外尋找。
範圍鍊取決於函式的作用域,與執行環境並無關聯性。
因JS為語法作用域,在原始碼寫好的時候,作用域就已經被訂下來了。
上方的程式碼中,無論是fn1或是fn2,因為fn1函式內沒有value變數,因此向外尋找,找到全域的value “1”,這就是範圍鍊的概念。
var person = ‘老媽’ |
執行 sayHi()
console.log顯示"hi 老媽"。
因為sayHi函式內沒有person變數,因此向外尋找,找到全域的person “老媽”。
var person = ‘老媽’ |
執行 doMorningWork()
console.log仍然顯示"hi 老媽"。
這是因為sayHi()
函式的範圍鍊依然指向全域,雖然他在doMorningWork()
函式內。
var person = ‘老媽’ |
執行 doMorningWork()
console.log顯示"哈囉~ 漂亮阿姨"。
因為meetAuntie()
函式內有宣告變數,因此console.log會印出"哈囉~ 漂亮阿姨"。
var person = ‘老媽’ |
執行 doMorningWork()
console.log顯示"哈囉~ 老爸"。
因為meetAuntie()
函式內變數var person = ‘漂亮阿姨’
已經被註解掉了,這時便會向外尋找,找到上一層的變數 “老爸”,因此console.log會印出"哈囉~ 老爸"。
提升 Hoisting
執行程式碼時先將所有變數取出,並在記憶體中分配空間給這些變數,但目前階段尚未賦予值而顯示undefined的情況下,稱為提升。
執行環境其實可細分為兩個階段:創造環境及執行。
- 創造環境:先將程式碼中的變數挑出來,在記憶體中分配空間給這些變數,但目前階段尚未賦予值,如果在此時去取用這些變數的話,會顯示undefined。
- 執行:此階段下才會將值賦予至變數。
變數的拆解
var Ming = '小明'; //宣告變數,可拆解成如下方所示 |
函式陳述式中在創造階段就會優先載入
函式陳述式與一般變數不太一樣,函式陳述式在創造環境階段會被優先載入,記憶體在此階段就載入函式的完整內容。所以函式在創造環境階段就已經可以被運行。
函式的拆解
//創造環境 |
函式表達式的拆解
var callName = function() { |
- 範例一:
function callName() { |
因上面所提及在創造階段時,函式優先下,不論函式先宣告,還是後宣告,結果皆為"呼叫小明 2",可拆解成如下
//創造環境 |
- 範例二:
callName(); //undefined |
拆解如下
//創造環境 |
- 範例三:
function callName() { |
拆解如下
//創造環境 |
- 測驗:
whosName() |
拆解如下
//創造環境 |
- 目前來說,程式碼還是會透過 Babel 編譯而編譯後的結果依然是 var 做執行,所以這樣的觀念還是要有。
(可以參考各大框架實際執行的程式碼) - function 的提升依然存在,這也是部分開發者的實作技巧之一將宣告的函式放在後方,呼叫放在前方。
(並非建議這樣的寫法,ESLint 中就不推薦此寫法)。 - let, const 雖沒有 hoisting,但還是有類似的概念(暫時性死區),其運作與 var hoisting 非常接近,同樣會影響原始碼的運行。
Not Defined VS undefined
Is not defined:
console.log(a) //a is not defined |
undefined:
var a |
執行緒與同步/非同步
學習 JS 時,必須要知道它是單執行緒(single thread)的語言,瀏覽器只分配給 JS 一個主執行緒,用來執行任務(函式),但一次只能執行一個任務,這些任務形成一個任務佇列排隊等候執行。
名詞解釋:
- 程式( Program ):Program 意旨軟體工程師所寫的程式碼(code),但還尚未load入記憶體的 code,我們稱之為Program。
- 程序(Process ):Process 意旨已經執行並且 load 到記憶體中的 Program ,程序中的每一行程式碼隨時都有可能被CPU執行。
- Process 是電腦中已執行 Program 的實體。
- Process 本身不是基本執行單位,而是 Thread (執行緒)的容器。
- Process 需要一些資源才能完成工作,如 CPU、記憶體、檔案以及I/O裝置。
- 執行緒:Thread 是 Process 的一個實體,也是CPU排程和分派的基本單位,它是比 Process 更小的能獨立執行的基本單位。
在同一個 Process 中會有很多個 Thread ,每一個 Thread 負責某一項功能。- 同一個 Process 會同時存在多個 Thread。
- 同一個 Process 底下的 Thread 共享資源,如 記憶體、變數等,不同的Process 則否。
- 單執行緒:有一個特性就是順序執行,當遇到比較耗時的任務時,還未執行的任務就會處於等待狀態,一定要等到前面的任務完成了,才會往後執行。
- 多執行緒:能夠在同一時間執行多於一個執行緒,進而提升整體處理效能。
JS程式語言中的同步與非同步
JS在執行時依舊是依照同步的概念,按照順序一個一個任務執行,但遇到非同步任務時,會把他往後放,放到事件佇列(Event Queue)中。
待所有任務執行完成後再執行事件佇列內的任務。
在所有非同步事件如 addEventListener、setTimeout、ajax…,都不會立即執行這些任務,而是將這些任務放到事件佇列(event queue)中,並將所有的事件堆疊完成後,才會開始讓事件佇列(event queue)內的事件被觸發。
名詞解釋:
- 同步( Scynchronous ):同步處理就像是一般的程式邏輯的話,都是按照你所寫的一行一行去執行,而順序必定都是前一個動作執行完才會去接著執行下一個動作。
- 非同步( Ascynchronous ):等待前面步驟執行完之前可以先去執行後面的步驟。
- 事件佇列( event queue )
從上面的描述,可以統整出兩個重點:
- 瀏覽器內核除了 js 引擎的執行緒,還有其他同步執行的執行緒。
- addEventListener、setTimeout、ajax,或其他無法預期執行時間的操作,都會以非同步處理。也就是會先被丟到事件佇列(event queue),等到同步執行的程式碼執行完,才會去處理那些被放到佇列中的任務。