老師在一開始提到,課程中可能有需多我們認為用不到的概念,他鼓勵我試著去理解它,它會是我們開發時進步的基礎。
許多人是先使用框架後才接觸到 JavaScript 的,有點本末導致,老師建議我打開 JQuery 或是其他框架的原始碼看他是怎麼寫的,並希望在這個課程結束後開發出一個自己的小框架。
將人寫的程式語言編譯成電腦看得懂的話
指的是你把程式碼寫在什麼地方
"執行脈絡" 指的是管理 "詞彙環境" 執行的前後順序
通常是別人寫好的程式,把你寫的程式包裹起來,用來驗證及執行你的程式碼。像是 JavaScript 之於瀏覽器 (執行脈絡)
物件是名稱/值的配對組合,值裡面又可以包含另一個名稱/值 (物件) 的組合
JaveScript 並不會直接被執行,而是透過 Syntax Parsers 解析後才讓電腦讀懂。而解析分為兩階段,
現在來看這段程式碼,
變數和函式明明是在宣告前執行的
為什麼函式能執行?變數為什麼會是有被宣告的 undefined 值呢?
主要因為在第一階段,函式會先被完整的寫入記憶體中,而變數也會被寫入記憶體,但這個階段程式還看不懂等號後面的值,所以一率先給 undefined 的值。
在第二階段程式執行,並把變數的值賦予上去。
因此在寫程式的時候要儘量不要讓 hoisting 的情況發生,因為難保你的變數的值就是等於 undefined。
undefined 是屬於 JS 的特殊值,在第一階端會將所有變數賦予 undefined 值,所以永遠不要把值宣告為 undefined,因為你不知道這個 undefined 是 JS 設定的還是你設定的。
undefined 代表已經寫入記憶體,但還沒定義的變數。
not defined 指的是在記憶體裡面找不到這個變數。
現在透過上面的程式範例來了解 function invocation
首先被創造的是 Global Ececution Context > this > 全域物件 > 韓式被放進記憶體 >>>>> 執行程式碼
當程式碼執行到函式 a,新的執行環境被創造,被放進執行堆(execution stack)中
新的執行環境會有自己的記憶體空間給變數和函數,他會經歷創造階段,然後逐行執行函數中的程式
當 a() 執行時,遇到函式中的另一個函式,他會先中止執行,先執行 b(),這時 b() 在執行堆的最上面,因此他會先執行,當執行完後,b() 的執行環境就會離開程式堆的最上面
每個函數都會創造一個執行環境
執行堆Image Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
被一個一個堆起來,最上面東西的會先被執行
變數環境(variable environment)Image Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
描述你創造變數的位置及他在記憶體中和其他變數的關係。你可以想成變數在哪裡?
上面這個範例中,myVar 的值會是什麼呢?
全域的變數環境與函式的變數環境是獨立分開的,彼此之前不會互相影響
這個範例會得到全域變數中的 myVar=1
因為每一個被建立的函數執行環境都有一個外部參照,
a() 和 b() 的外部參照都是 Global Execution Context,當你需要某個執行環境內的變數,但你卻找不到他,他會到外部環境找,直到他找到或沒找到。
這一整條向外部找的順序稱為範圍鍊。"範圍"代表我能夠取用這變數的地方,"鏈"是外部環境參照的連結
這裡就跟程式碼所在的詞彙環境有關了,
例如 b() 的詞彙環境在全域環境下,而不是在 a() 裡面,他與最後一行的 var myVar=1 同級別。
假如果我們改變 b() 的詞彙位置像這樣:
這時 b() 的詞彙環境改在 a(),因此他會向外部參照a() 函式找 myVar,
同時代表我們不能在全域環境下呼叫 b()
範圍Image Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
範圍是變數可以被取用的地方
let 允許一個和 var 不一樣的地方是取用的限制與區塊範圍。
假如你在 let c = true 前取用 c ,你會你會得到一個 error,雖然他仍在記憶體中,但引擎不讓你用。
另外就是 let 如果在區塊中被宣告,每次進行迴圈時,你的變數在記憶體中都是不同的,他就只能在區塊內被取用。
非同步 AsynchronousImage Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
在同一個時間點不止一個
但 JavaScript 引擎是同步的,他會一次只執行一行程式碼。
而 JavaScript 在瀏覽器中不只是只有他而已,他可能需要和其他事件溝通。這時就會使用到非同步去溝通。
當出現非同步事件時,會被擺放至 Event Quene,等待執行堆為空時,才會檢查 Event Quene 是否有事件要執行 => continue check
動態型別Image Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
你不需要告訴 JavaScript Engine 你的變數是什麼型別,在執行程式的時候會自動判斷型別
靜態型別Image Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
在宣告變數時就告訴編譯器變數的型別,如果變數中放入其他型別就會出錯
純值Image Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
是一種資料的類別表示一個值。換句話說,不是一個物件,因為物件是一個名稱/值配對的組合。
undefined 表示還不存在,JavaScript 給所有變數的初始值。所以不該設定任何值為 undefined
null 表示一個東西還不存在,可用來設定一個變數沒有值。
true Or False
浮點數,表示一定有小數點在後面。
一連串字符所組成,單引號或雙引號都可以來表示字串
在 ES6 中被使用。
運算子都是函數
但因為運算子使用的是中綴表示法。所以可以表示為 3+4
運算子會先看優先性再去看相依性
優先性高的運算子會優先被執行
如果優先性都相同,則看是左相依性或右相依性
轉換一個值的型態成另外一個
先看一個例子
為什麼會造成這樣的結果呢?
因為在<的運算子上,優先性是相等,使用的是由左到右相依性
因此先看 3<2 會得到 false 的值
接下來看 false<1 則是得到 true 的值
因為 false 會被強制轉型為 0
但不是所有值都可以被強制轉型的
會得到 NaN,代表 not a Number
當得到這個值時,可以想成是想要把某個值轉型為數字,但無法轉型所得到的值。
但不是每次的轉型都會如你預期,像是 null 轉型為數值就會得到 0。
因此建議不要使用 == 運算子,而是使用 === 運算子
=== 不會強制轉型,他會比較兩者是否是相同的東西。
當使用 == 和 === 會發生什麼事
我們可以透過強制轉型來檢查變數是否有值
如果 a 值是 undefined, null, '' 都會被強行轉型為 false
這邊要特別注意 0 也會被強制轉型為 false
因此如果需要檢查 0 的情況可以寫成這樣
當執行上面程式碼時,未定義的變數會被記憶體設為 undefined,
因為這邊我們加了運算子+,因此 undefined 會被強行轉成字串
因此我們可以設定預設值去防止如果沒有輸入值的情況
如果我們在某框架看到這樣的程式碼,
這其實是在設定 定義框架的物件或函數
這樣如果那個名稱已經存在, 也不會影響到其他東西
這是在檢查全域命名空間(global namespace)
在其他程式語言中,物件與函數是不一樣的東西,但在 JS 裡面,是非常相關的
複習一下
物件是一群 "名稱/值" 的組合,
而這些值又可以是一群 "名稱/值的組合"
物件有"屬性"和"方法"
在記憶體中,主要的核心物件會有一個記憶體的位置,他有可以參考到這些屬性和方法的位址。
命名空間就是變數與函數的容器,用來維持變數和函數的名稱分開。
但是 JavaScript 沒有命名空間,那要怎麼做呢?
我們可以用物件假裝有命名空間。
這樣就能達到同樣變數名稱卻可以做分離的目的。
JavaScript Object Notation
JSON 常常和物件實體被搞混,他們其實是不同的東西。
JSON 的屬性名稱一定要用引號包起來,
物件可以包也可以不包。
技術上來說,JSON 是物件實體語法的子集合
只要是 JSON 在物件實體語法就是有效的,
但不是所有的物件實體語法在JSON格式都是有效的。
我們可以透過 JavaScript 語法轉換兩者
函數只是程式碼的容器
表示式是程式碼的單位,他會形成一個值,他不一定要被存入變數。
會回傳值的都算是表示式
陳述式會做其他事
函數表示式在環境被創造的階段,這個函數物件會被存入記憶體中,
當這個名字為 greet 函數被呼叫時,就會執行它。
在環境被創造時,生成一個 anonymousGreet 的變數,在執行階段會把後面這段匿名函數的位址指向這個變數,所以我們可以透過這個變數來參考位址,不需要去命名這個函數。
程式語言中的函數可以像其他變數一樣,我們稱它為一級函數。像是可以當成參數傳入函數中,可以被另一個函式作為回傳值且可以被當作值一樣指派到另一個變數
純值在 JavaScript 裡面傳值。
當變數是純值時,等號運算值會另外在記憶體中產生一個位址給這個變數。
所以兩個變數是存在不同的記憶體位置
當變數是物件時,
等號運算值會將兩個變數指向同一個位址。
像是別名一樣,兩個變數其實是同一個東西。
某些情況下,this 會依據函數如何被呼叫而改變
在這種情況可以在物件內宣告一個變數指向物件內的 this
陣列是任何東西的集合
他可以包含數字、布林、物件、函數、字串
arguments 代表傳入的任何參數。
下面範例可以透過 arguments 檢查是否有正確傳入參數。
但 arguments 只是 array-like 像陣列而已,他並非陣列。
重載函數,讓一個函數有不同數量的參數傳入
但在 JavaScript 中並沒有這樣處理函數的方法,
因為函數本身就是物件。
但我們可以透過函數中的邏輯來模擬重載函數
我們寫的程式要透過語法解析器轉譯成電腦懂的語言
語法解析器我預知你要寫入語法,如果寫入的程式碼不符合規則他就會報錯
在 JavaScript 中,我們可以不用打分號,那是因為 JS 引擎看到我們在按下 Enter 鍵後出現的 carriage return,雖然他看不見,但他確實是一個字元,當 JS 引擎看到這個字元時,會自動幫我們補上分號,但這也可能造成問題
因此絕對不要讓js幫你加上分號,我們要自己打分號。
在函數表示式中才能使用立即函數
我們只要確保程式碼的開頭不是 function 就不會有問題,
加上括號後,這個函式就會變成函數表示式,
再輸入這個函數後立即回傳一個函式
透過這個函數表示式,我們可以再用一個括號呼叫他
就是一個 IIEF 了
而這背後發生什麼事?
當成是第一次載入時,我會有全域執行環境,
但什麼事都不會發生,因為沒有變數也沒有函數陳述式提升
然後當執行到這段 IIEF 程式碼,
他會創造函數物件記憶體,但他是匿名函數,
然後他看到括弧呼叫這個函數
一個新的執行環境被創造
然後 greeting 這個變數進到這個 IIEF 的執行環境
在某些框架裡面,你可以看到程式碼會整個被包在 IIEF 裡面,
這是為了不和全域變數混淆。
當變數放在 IIEF 裡面,他就有屬於自己的執行環境,在裡面宣告的變數即使和全域變數相同,也不會衝突。
這邊使用一個函數內回傳一個函數
以上例子看起來很正常,
但假如將這個函數放入變數中呢?
為什麼在 sayHi('Tony') 的函數中可以提取到 name 變數的值??
因為他是 closure,所以是可能的。
來看看程式背後是如何運作的
在 greet('Hi')執行完後,離開執行堆
greet 的執行環境結束時,變數的記憶體空間仍在那,
當我們回到全域變數呼叫 sayHi('Tony')
這時函數內的變數有使用到 whattosay,
這時他會透過範圍鏈到 sayHi 函數的外部環境 greet() 去找 whattosay,
雖然 greet 的執行環境已經結束,雖然 JS 引擎的垃圾回收機制會釋放不再使用的記憶體,
但因為閉包的機制,讓 JS 的垃圾回收機制沒有啟動,將 whattosay 變數的記憶體位置留下來。
這個包住所有可以取用的變數的現象稱為閉包
閉包的經典案例
三個得到的結果都是 3,為什麼呢?
在執行 buildFunctions 這個函數時,
arr 內被放入了 3 個函數,i 變數則是跑到了 3 離開了 for 迴圈。
執行完後,離開了 buildFunctions 的執行環境,
但是變數和函數 在記憶體中會被留下來。
這時 fs[0] 函數執行,他在函式裡面找 i,找不到,於是他透過範圍鏈到外部環境,也就是原本的 buildFunctions 的變數記憶體位置去找,於是找到 i 為 3。
其他 fs 函數以此類推,於是 console.log 出來的值都是 3
這種取用閉包外部的變數,也稱為自由變數。
那假如我們要按照順序輸出 0, 1, 2 要怎麼做呢
做出即使使用同一個函數,但每次執行他時,他會創造新的執行環境
第一次執行 greetEnglish 時,會到範圍鏈找到 language == en
第二次執行 greetSpanish 時,會到範圍鏈找到 language == es
以下這段程式碼用到一級函數和閉包
在 setTimeout 裡面傳入一個一級函數去做 3 秒後要做的事,
console.log(greet) 用到閉包去取用外層範圍鏈 greeeting 的變數
所有的函數物件都可以使用這三種方法
bind() 會拷貝一個函數出來並賦予 this 的指向
apply() 和 bind() 一樣會賦予 this 的指向,不過他們不會拷貝函示,而是直接執行重新指向 this 的函數
當 logName.bind(person) 時,bind 會 "拷貝" 出一個 logName 的函數,並把 this 的指向從 window 改成 person。
logName = function() {
console.log('Logged: ' + window.getFullName());
}
logPersonName = function() {
console.log('Logged: ' + person.getFullName());
}
logName !== logPersonName
function currying: 建立一個函數的拷貝,並設定預設的參數
JS 引入一些方法讓你做一些在其他沒有一集函數的程式語言不能做的事
瞭接這些程式庫的程式碼來增進自己的 JS 功力
underscorejs
lodashjs
inheritance 繼承
One object get access to the properties and methods of anthor object
一個物件可以取用另一個物件的屬性及方法
classic inheritance 古典繼承
verbose
friend
protected
provate
interface
prototype inheritance 原型繼承
simple
flexible
extensible
easy to understand
object 透過原型鏈可以向他的原型取用物件的屬性和方法
上面這張圖 obj.prop2 像是去 obj 取用 prop2,
但其實是去 obj 的物件原型取用 prop2
而原型物件也有他的原型物件,因此他可以透過原型鏈往上取用 obj.prop3
obj2.prop2 也可以做取用。
因為他們可以指向同一個原型。
john 物件透過範圍鏈取用到 person 原型的 getFullName 的方法。
另外 john.firstname 因為已經在 john 物件中找到 firstname,因此就不會繼續往原型鏈找 firstname
當 jane 呼叫 getFullName 時,他在 jane 的物件找到 'Jane',但 jane 的物件並沒有 lastname,因此他透過原型鏈向上找,找到了 lastname 為 'Default'
使用 reflection 完成 extend 功能
參考 underscore.js 作法
函數的 prototype 只用在使用函數建構子來建構物件這種特殊用途時才會用到
函數的原型屬性不是函數的原型,它是你用函數建構子創造的物件的原型
透過下面的程式碼解釋,Person 的 prototype 和 Person 的 proto 不一樣,Person 的 prototype 是 John 用函數建構子 new 出來的物件原型。
Person 的 proto 會指向 Function Prototype
另外附上一張網路抓到的解析圖
當使用 new 的時候會建立空物件,當執行函數時,this 會指向空物件,如果你不回傳任何東西的話,他會回傳新物件。
但他仍然是個函數,當你忘記加上 new 的時候,他仍然會執行。
因為沒有回傳任何值,所以原本要被建立的物件會被設定為 undefined,
當你要取用屬性或方法時,會因為他不是物件而噴錯
內建的函數建構子
表示已經存在的函數原型
一些已經存在的函數原型
內建的函數建構子建立的純值並不是真正的純值,他是"物件"。
建議不要用 JS 內建的函數建構子,
用實體化語法,直接用純值會更加安全。
儘量不要使用 for in ,因為他會遍歷到原型的屬性。
使用 for(var i = 0; i < length; i++)
講師鼓勵學習 JS 可以去 github 上看別人的原始碼,
又或者去看常用的 JS 框架原始碼。