--- title: '克服 JS 奇怪部分' disqus: Pai --- # 目錄及代辦事項 ### 在課程完成後,試著自己做出一個自己的 JS 小框架 ## Table of Contents [TOC] ## 待學習項目 - [ ] - - [ ] - - [ ] - - [ ] - # 壹、前言 老師在一開始提到,課程中可能有需多我們認為用不到的概念,他鼓勵我試著去理解它,它會是我們開發時進步的基礎。 許多人是先使用框架後才接觸到 JavaScript 的,有點本末導致,老師建議我打開 JQuery 或是其他框架的原始碼看他是怎麼寫的,並希望在這個課程結束後開發出一個自己的小框架。 # 貳、執行環境與詞彙環境 ## 一、名詞解釋 ### Syntax Parsers 語法解析器 將人寫的程式語言編譯成電腦看得懂的話 ![](https://i.imgur.com/vhFHTCl.jpg) ### Lexical Environments 詞彙環境 指的是你把程式碼寫在什麼地方 ![](https://i.imgur.com/bbOO99s.jpg) ### Execution Contexts 執行脈絡 "執行脈絡" 指的是管理 "詞彙環境" 執行的前後順序 通常是別人寫好的程式,把你寫的程式包裹起來,用來驗證及執行你的程式碼。像是 JavaScript 之於瀏覽器 (執行脈絡) ![](https://i.imgur.com/Cub6g7Q.jpg) ### 物件的定義 物件是名稱/值的配對組合,值裡面又可以包含另一個名稱/值 (物件) 的組合 ![](https://i.imgur.com/PyLzNuv.png) ## 二. JavaScript 在瀏覽器的 Execution Contexts ![](https://i.imgur.com/OrbzFvg.png) ## 三、 創造與提升 (Hoisting) JaveScript 並不會直接被執行,而是透過 Syntax Parsers 解析後才讓電腦讀懂。而解析分為兩階段, - 將變數和函式寫入記憶體 - 執行詞彙環境的程式 現在來看這段程式碼, ```javascript= console.log(a); // undefined b(); // 'b' var a = 'Hello World!' function b() { console.log('b') } ``` 變數和函式明明是在宣告前執行的 為什麼函式能執行?變數為什麼會是有被宣告的 undefined 值呢? 主要因為在第一階段,函式會先被完整的寫入記憶體中,而變數也會被寫入記憶體,但這個階段程式還看不懂等號後面的值,所以一率先給 undefined 的值。 在第二階段程式執行,並把變數的值賦予上去。 因此在寫程式的時候要儘量不要讓 hoisting 的情況發生,因為難保你的變數的值就是等於 undefined。 ## 四、 JavaScript 與 undefined undefined 是屬於 JS 的特殊值,在第一階端會將所有變數賦予 undefined 值,所以永遠不要把值宣告為 undefined,因為你不知道這個 undefined 是 JS 設定的還是你設定的。 ```javascript= var a; console.log(a); // undefined console.log(b); // Uncaught ReferenceError: b is not defined ``` undefined 代表已經寫入記憶體,但還沒定義的變數。 not defined 指的是在記憶體裡面找不到這個變數。 ## 五、單執行緒與同步執行 - 單執行緒 一次只執行一個指令 但瀏覽器不止由 JavaScript 組成,因此我們說說 JavaScript 是單執行緒時,是由我們的角度,不是瀏覽器。 - 同步執行 一次一個程式碼,並且照順序執行 ## 六、function invocation(函數呼叫)與 execution stack(執行堆) - function invacation 在 JavaScript 中透過()呼叫函數 ![](https://i.imgur.com/Dzgux0M.png) ```javascript= function b() { } function a()` { b() } a() ``` 現在透過上面的程式範例來了解 function invocation - 首先被創造的是 Global Ececution Context > this > 全域物件 > 韓式被放進記憶體 >>>>> 執行程式碼 - 當程式碼執行到函式 a,新的執行環境被創造,被放進執行堆(execution stack)中 - 新的執行環境會有自己的記憶體空間給變數和函數,他會經歷創造階段,然後逐行執行函數中的程式 - 當 a() 執行時,遇到函式中的另一個函式,他會先中止執行,先執行 b(),這時 b() 在執行堆的最上面,因此他會先執行,當執行完後,b() 的執行環境就會離開程式堆的最上面 ![](https://i.imgur.com/FGyxCy8.png) 每個函數都會創造一個執行環境 >:bulb:執行堆 被一個一個堆起來,最上面東西的會先被執行 ## 七、函式(function)、環境(context)與變數環境(variable environment) >:bulb: 變數環境(variable environment) 描述你創造變數的位置及他在記憶體中和其他變數的關係。你可以想成變數在哪裡? ```javascript= function b() { var myVar; } function a() { var myVar = 2; b(); } var myVar = 1; a(); ``` 上面這個範例中,myVar 的值會是什麼呢? ![](https://i.imgur.com/tctAlfl.png) 全域的變數環境與函式的變數環境是獨立分開的,彼此之前不會互相影響 ## 八、範圍鏈 ```javascript= function b() { console.log(myVar) } function a() { var myVar = 2 b(); } var myVar = 1; a() // 1 ``` 這個範例會得到全域變數中的 myVar=1 ![](https://i.imgur.com/sQmXM1K.png) 因為每一個被建立的函數執行環境都有一個外部參照, a() 和 b() 的外部參照都是 Global Execution Context,當你需要某個執行環境內的變數,但你卻找不到他,他會到外部環境找,直到他找到或沒找到。 這一整條向外部找的順序稱為範圍鍊。"範圍"代表我能夠取用這變數的地方,"鏈"是外部環境參照的連結 這裡就跟程式碼所在的詞彙環境有關了, ![](https://i.imgur.com/3OChpID.png) 例如 b() 的詞彙環境在全域環境下,而不是在 a() 裡面,他與最後一行的 var myVar=1 同級別。 假如果我們改變 b() 的詞彙位置像這樣: ```javascript= function a() { function b() { console.log(myVar) } var myVar = 2 b(); } var myVar = 1; a() // 2 b() // b is not defined ``` 這時 b() 的詞彙環境改在 a(),因此他會向外部參照a() 函式找 myVar, 同時代表我們不能在全域環境下呼叫 b() ## 九、範圍、ES6、Let >:bulb: 範圍 範圍是變數可以被取用的地方 let 允許一個和 var 不一樣的地方是取用的限制與區塊範圍。 假如你在 let c = true 前取用 c ,你會你會得到一個 error,雖然他仍在記憶體中,但引擎不讓你用。 另外就是 let 如果在區塊中被宣告,每次進行迴圈時,你的變數在記憶體中都是不同的,他就只能在區塊內被取用。 ## 十、關於非同步回呼 ```javascript= function waitThreeSeconds() { var ms = 3000 + new Date().getTime(); while(new Date() < ms){} // 使用 while 等待,當 ms > new Date() 時,執行下列 console.log console.log('finished function') } function clickHandler() { console.log('click event') } document.addEventListener('click',clickHandler); waitThreeSeconds(); console.log('finished exection') // finished function // finished exection // click event ``` >:bulb: 非同步 Asynchronous 在同一個時間點不止一個 但 JavaScript 引擎是同步的,他會一次只執行一行程式碼。 而 JavaScript 在瀏覽器中不只是只有他而已,他可能需要和其他事件溝通。這時就會使用到非同步去溝通。 ![](https://i.imgur.com/Er8gBwa.jpg) 當出現非同步事件時,會被擺放至 Event Quene,等待執行堆為空時,才會檢查 Event Quene 是否有事件要執行 => continue check ![](https://i.imgur.com/wKr1GE1.png) # 貳、型別與運算子 ## 一、型別與 JavaScript >:bulb: 動態型別 你不需要告訴 JavaScript Engine 你的變數是什麼型別,在執行程式的時候會自動判斷型別 >:bulb: 靜態型別 在宣告變數時就告訴編譯器變數的型別,如果變數中放入其他型別就會出錯 ## 二、純值 >:bulb: 純值 是一種資料的類別表示一個值。換句話說,不是一個物件,因為物件是一個名稱/值配對的組合。 ### 1. undefined undefined 表示還不存在,JavaScript 給所有變數的初始值。所以不該設定任何值為 undefined ### 2. null null 表示一個東西還不存在,可用來設定一個變數沒有值。 ### 3. Boolean true Or False ### 4. Number 浮點數,表示一定有小數點在後面。 ### 5. String 一連串字符所組成,單引號或雙引號都可以來表示字串 ### 6. Symbol 在 ES6 中被使用。 ## 三、運算子(Operater) 運算子都是函數 ```javascript= var a = 3 + 4; // 會等於 var a = +(3,4); ``` 但因為運算子使用的是中綴表示法。所以可以表示為 3+4 ## 四、運算子的優先性與相依性 運算子會先看優先性再去看相依性 優先性高的運算子會優先被執行 如果優先性都相同,則看是左相依性或右相依性 ![](https://i.imgur.com/4Kfk0tA.png) ![](https://i.imgur.com/s18FOU0.png) [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence) ## 五、強制轉型(Coercion) 轉換一個值的型態成另外一個 ## 六、比較運算子 先看一個例子 ```javascript= console.log(1<2<3) // true console.log(3<2<1) // true ``` - 為什麼會造成這樣的結果呢? 因為在<的運算子上,優先性是相等,使用的是由左到右相依性 因此先看 3<2 會得到 false 的值 接下來看 false<1 則是得到 true 的值 因為 false 會被強制轉型為 0 - 但不是所有值都可以被強制轉型的 ```javascript= Number(undefined) // NaN ``` 會得到 NaN,代表 not a Number 當得到這個值時,可以想成是想要把某個值轉型為數字,但無法轉型所得到的值。 ```javascript= Number(null) //0 ``` 但不是每次的轉型都會如你預期,像是 null 轉型為數值就會得到 0。 因此建議不要使用 == 運算子,而是使用 === 運算子 === 不會強制轉型,他會比較兩者是否是相同的東西。 [當使用 == 和 === 會發生什麼事](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness) ## 七、存在與布林 我們可以透過強制轉型來檢查變數是否有值 ```javascript= var a; if(a) { console.log('something is there') } // ``` 如果 a 值是 undefined, null, '' 都會被強行轉型為 false 這邊要特別注意 0 也會被強制轉型為 false 因此如果需要檢查 0 的情況可以寫成這樣 ```javascript= var a; a = 0; if(a || a ==== 0) { console.log('something is there') } // ``` ## 八、預設值 ```javascript= function greet(name) { console.log('Hello ' + name); } greet(); // Hello undefined ``` 當執行上面程式碼時,未定義的變數會被記憶體設為 undefined, 因為這邊我們加了運算子+,因此 undefined 會被強行轉成字串 因此我們可以設定預設值去防止如果沒有輸入值的情況 ```javascript= function greet(name) { name = name || '<this is name>' console.log('Hello ' + name); } greet(); // Hello undefined ``` ## 九、框架小叮嚀:預設值 ```javascript= window.libraryName = window.libraryName || "Lib 2" ``` 如果我們在某框架看到這樣的程式碼, 這其實是在設定 定義框架的物件或函數 這樣如果那個名稱已經存在, 也不會影響到其他東西 這是在檢查全域命名空間(global namespace) # 參、物件與函數 在其他程式語言中,物件與函數是不一樣的東西,但在 JS 裡面,是非常相關的 ## 一、物件與「點」 複習一下 物件是一群 "名稱/值" 的組合, 而這些值又可以是一群 "名稱/值的組合" ### 1. 物件是如何被存在記憶體 物件有"屬性"和"方法" ![](https://i.imgur.com/g4vMuKU.png) 在記憶體中,主要的核心物件會有一個記憶體的位置,他有可以參考到這些屬性和方法的位址。 ![](https://i.imgur.com/K0atYRB.png) ### 2. JavaScript 是如何找到物件方法與屬性的記憶體位置 - 計算取用成員的運算子(computed member access) object['name'] 通常用在物件名稱會改變時 ```javascript= var person = new Object(); person["firstname"] = "Tony" person["lastname"] = 'Alicea' var firstNameProperty = "firstname" console.log(person[firstNameProperty]) ``` - 點運算子(Member Access) 一般取用物件成員,使用點運算子 待物件名稱可能需要字串改變時,就可以使用計算取用成員運算子 ```javascript= console.log(person.firstname) peoson.address = new Object(); person.address.street = "111 Main St." ``` ## 二、物件與物件實體 ## 三、框架小叮嚀:偽裝命名空間 (Faking Namespaces) 命名空間就是變數與函數的容器,用來維持變數和函數的名稱分開。 但是 JavaScript 沒有命名空間,那要怎麼做呢? ```javascript= var greet = 'Hello'; var greet = 'Hola'; console.log(greet) // 同樣變數名稱,結果會被後面的變數覆蓋掉 ``` 我們可以用物件假裝有命名空間。 ```javascript= var english = {}; var spanish = {}; english.greet = 'Hello'; spanish.greet = 'Hola'; ``` 這樣就能達到同樣變數名稱卻可以做分離的目的。 - 但不能直接做變數的多層宣告 ```javascript= var english = {}; var spanish = {}; // english.greetings = {}; english.greetings.greet = 'Hello'; // undefined.greet X -> 因為在 english 的物件中找不到 greetings 這個命名 spanish.greet = 'Hola'; console.log(english) ``` - 或是直接使用物件實體化的方法 ```javascript= var english = { greetings: { greet: 'Hello'; } }; var spanish = {}; spanish.greet = 'Hola'; console.log(english) ``` ## 四、JSON 與物件實體 ### 1. JSON JavaScript Object Notation JSON 常常和物件實體被搞混,他們其實是不同的東西。 JSON 的屬性名稱一定要用引號包起來, 物件可以包也可以不包。 技術上來說,JSON 是物件實體語法的子集合 只要是 JSON 在物件實體語法就是有效的, 但不是所有的物件實體語法在JSON格式都是有效的。 ```javascript= // 物件實體 var objectLiteral = { firstname: 'Mary', isAProgrammer: true } // JSON { "firstname": "Mary", "isAProgrammer": true } ``` 我們可以透過 JavaScript 語法轉換兩者 ```javascript= var objectLiteral = { firstname: 'Mary', isAProgrammer: true } console.log(JSON.stringify(objectLiteral)) // 轉成JSON 字串 var jsonValue = JSON.parse('{"firstname": "Mary","isAProgrammer": true}') console.log(jsonValue) // 轉成物件 ``` ## 五、函數就是物件 - 一級函數(first class functions) - JavaScript 中非常強大的工具 - 你可以對別的型別,如:物件、字串、數值、布林做的事,你都可以對函數做 ex: 你可以指派一個變數的值為函數、你也可以將函數當成參數傳入另一個函數、你可以用實體語法立刻創造函數 - 函數就是物件 ![](https://i.imgur.com/e70l49x.png) - 函數有屬性及方法 - 純值 - 物件 - 函式 - 名字(也可以沒有) - 程式屬性(code property) - 程式屬性 - 可呼叫的,代表你可以執行這個程式碼 - 寫的程式碼是函數物件的特殊屬性,並非函數本身 ```javascript= function greet() { console.log('hi') } greet.language = 'english' console.log(greet.language); // english ``` ![](https://i.imgur.com/qnm3njR.png) >函數只是程式碼的容器 ## 六、函數陳述句與函數表示式 ### 1. 函數表示式(expression) 表示式是程式碼的單位,他會形成一個值,他不一定要被存入變數。 ```javascript= a = 3 //3 1+2 //3 a = { greeting: 'hi' } //Object {greeting: 'hi'} ``` 會回傳值的都算是表示式 ### 2. 函數陳述式(statement) 陳述式會做其他事 ```javascript= var a; if(a === 3) { } // 上面不會回傳任何值,他就是個陳述式 ``` ### 3. 函數表示式與函數陳述式的不同 ![](https://i.imgur.com/9c0FtHD.png) 函數表示式在環境被創造的階段,這個函數物件會被存入記憶體中, 當這個名字為 greet 函數被呼叫時,就會執行它。 ![](https://i.imgur.com/4LndgYi.png) 在環境被創造時,生成一個 anonymousGreet 的變數,在執行階段會把後面這段匿名函數的位址指向這個變數,所以我們可以透過這個變數來參考位址,不需要去命名這個函數。 ### 4.一級函式(First-class Function) 程式語言中的函數可以像其他變數一樣,我們稱它為一級函數。像是可以當成參數傳入函數中,可以被另一個函式作為回傳值且可以被當作值一樣指派到另一個變數 ```javascript= function log(a) { a() } log(function() { console.log('hi') }) ``` ## 七、觀念小叮嚀:傳值和傳參考 ### 1. 純值傳值 ![](https://i.imgur.com/ijRsrcx.png) 純值在 JavaScript 裡面傳值。 當變數是純值時,等號運算值會另外在記憶體中產生一個位址給這個變數。 所以兩個變數是存在不同的記憶體位置 ```javascript= // by value (primitives) var a = 3; var b, b = a; a=2 console.log(a); // 2 console.log(b); // 3 ``` ### 2. 物件傳參考 ![](https://i.imgur.com/DuDigCu.png) 當變數是物件時, 等號運算值會將兩個變數指向同一個位址。 像是別名一樣,兩個變數其實是同一個東西。 ```javascript= // by reference (all objects(including function)) var c = { greeting: 'hi'}; var d, c.greeting = 'hello' console.log(c); // { greeting: 'hello'} console.log(d); // { greeting: 'hello'} // by reference (even as parameters) function changeGreeting(obj) { obj.greeting = 'Hola'; //mutate } changeGreeting(d); console.log(c); // { greeting: 'Hola'} console.log(d);// { greeting: 'Hola'} // equals operater sets up new memory space (new address) c = { greeting: 'howdy' }; console.log(c); // { greeting: 'howdy' }; console.log(d); // { greeting: 'Hola'} // 等號運算子會設定一個新的記憶體空間,d 和 c 不會存在同一個記憶體位置 // 因為等號運算子看到的物件不存在記憶體內,這是物件實體化的語法 ``` ## 八、物件、函數與「this」 某些情況下,this 會依據函數如何被呼叫而改變 - 以上下這些呼叫都指到全域變數位址 ```javascript= console.log(this) // window function a() { console.log(this); } a() //window var n = function() { console.log(this) } b() //window ``` - 甚至可以直接創造全域變數裡面的 name/value ```javascript= function a() { this.newvariable = 'hello' } a() console.log(newvariable) // hello ``` - 當函數是連結到物件的方法時,this關鍵字成為裡面有方法的物件,也就是c ```javascript= var c = { name: 'The c object', log: function() { this.name = 'Updated c object' console.log(this) } } c.log(); //Updated c object ``` - 在物件中宣告函數,在函數裡面去用函數去改變this的值 ```javascript= var c = { name: 'The c object', log: function() { this.name = 'Updated c object' console.log(this) var setname = function(newname) { this.name = newname; } setname('Updated again! The c object') // 用函數呼叫的this指向全域變數,所以沒有去改變c物件裡面的name值 console.log(this); } } c.log(); //Updated c object //Updated c object ``` 在這種情況可以在物件內宣告一個變數指向物件內的 this ```javascript= var c = { name: 'The c object', log: function() { var self = this; self.name = 'Updated c object' console.log(self) var setname = function(newname) { self.name = newname; } setname('Updated again! The c object') // 用函數呼叫的this指向全域變數,所以沒有去改變c物件裡面的name值 console.log(this); } } c.log(); //Updated c object //Updated again! The c object ``` ## 九、觀念小叮嚀:陣列——任何東西的集合 陣列是任何東西的集合 他可以包含數字、布林、物件、函數、字串 ```javascript= var arr = [ 1, false, { name: 'Tony', address: '111 Main St.' }, function(name) { var greeting = 'Hello '; console.log(greeting + name); }, 'hello' ]; console.log(arr); arr[3](arr[2].name) ``` ## 十、'arguments' 與 spread ![](https://i.imgur.com/RmvwO1G.png) arguments 代表傳入的任何參數。 下面範例可以透過 arguments 檢查是否有正確傳入參數。 但 arguments 只是 array-like 像陣列而已,他並非陣列。 ```javascript= function greet(firstname, lastname, language) { language = language || 'en'; // 透過檢查 argument 的長度來看變數是否有被正確傳入 if (arguments.length === 0) { console.log('Missing parameters'); console.log('---------'); return } console.log(firstname); console.log(lastname); console.log(language); console.log(arguments); console.log('arg 0: ' + arguments[0]); console.log('---------'); } ``` - spread ...+變數名稱,代表其他沒有在函數種命名的變數。 他也是一個類陣列。 ```javascript= function greet(firstname, lastname, language, ...other) { console.log(other) } greet('a', 'b', 'c', 'd', 'e') // ["d", "e"] ``` ## 十一、框架小叮嚀:重載函數 重載函數,讓一個函數有不同數量的參數傳入 但在 JavaScript 中並沒有這樣處理函數的方法, 因為函數本身就是物件。 但我們可以透過函數中的邏輯來模擬重載函數 ```javascript= people.find = function () { switch (arguments.length) { case 0: return this.values; case 1: return this.values.filter((value) => { var firstName = arguments[0]; return value.indexOf(firstName) !== -1 ? true : false; }); case 2: return this.values.filter((value) => { var fullName = `${arguments[0]} ${arguments[1]}`; return value.indexOf(fullName) !== -1 ? true : false; }); } }; console.log(people.find()); // ["Dean Edwards", "Sam Stephenson", "Alex Russell", "Dean Tom"] console.log(people.find('Dean')); // ["Dean Edwards", "Dean Tom"] console.log(people.find('Dean', 'Edwards')); // ["Dean Edwards"] ``` ## 十二、觀念小叮嚀:語法解析器 我們寫的程式要透過語法解析器轉譯成電腦懂的語言 語法解析器我預知你要寫入語法,如果寫入的程式碼不符合規則他就會報錯 ## 十三、危險小叮嚀:自動插入分號 在 JavaScript 中,我們可以不用打分號,那是因為 JS 引擎看到我們在按下 Enter 鍵後出現的 carriage return,雖然他看不見,但他確實是一個字元,當 JS 引擎看到這個字元時,會自動幫我們補上分號,但這也可能造成問題 ```javascript= function getPerson() { return // js 在這自動幫我們加上分號了 { firstname: 'Tony' } } console.log(getPerson()) ``` 因此絕對不要讓js幫你加上分號,我們要自己打分號。 ## 十四、框架小叮嚀:空格 ## 十五、立即呼叫的函數表示式(IIFEs) 在函數表示式中才能使用立即函數 ```javascript= var greetFunc = function(name) { console.log('Hello' + name); }; greetFunc('John'); var greeting = function(name) { console.log('Hello ' + name); }('John') console.log(greeting) ``` - 那如果是原本是函數陳述式要如何使用立即函數呢? 如果是這樣就會噴錯 ```javascript= function() { console.log('greeting') } // Uncaught SyntaxError: Function statements require a function name ``` 我們只要確保程式碼的開頭不是 function 就不會有問題, 加上括號後,這個函式就會變成函數表示式, 再輸入這個函數後立即回傳一個函式 ![](https://i.imgur.com/CuIKa6q.png) ```javascript= (function() { console.log('greeting') }) ``` 透過這個函數表示式,我們可以再用一個括號呼叫他 就是一個 IIEF 了 ```javascript= (function() { console.log('greeting') })() ``` 而這背後發生什麼事? 當成是第一次載入時,我會有全域執行環境, 但什麼事都不會發生,因為沒有變數也沒有函數陳述式提升 ![](https://i.imgur.com/jGIoDv6.png) 然後當執行到這段 IIEF 程式碼, 他會創造函數物件記憶體,但他是匿名函數, 然後他看到括弧呼叫這個函數 ![](https://i.imgur.com/OckBVOz.png) 一個新的執行環境被創造 ![](https://i.imgur.com/3rTyajc.png) 然後 greeting 這個變數進到這個 IIEF 的執行環境 ![](https://i.imgur.com/TA1qut3.png) ## 十六、框架小叮嚀:IIFEs 與安全程式碼 在某些框架裡面,你可以看到程式碼會整個被包在 IIEF 裡面, 這是為了不和全域變數混淆。 當變數放在 IIEF 裡面,他就有屬於自己的執行環境,在裡面宣告的變數即使和全域變數相同,也不會衝突。 ![](https://i.imgur.com/fbvxuoN.png) ## 十七、瞭解閉包(一) 這邊使用一個函數內回傳一個函數 ```javascript= function greet(whattosay) { return function(name) { console.log(whattosay + ' ' + name) } } greet('Hi')('Tony') // 'Hi Tony' ``` 以上例子看起來很正常, 但假如將這個函數放入變數中呢? ```javascript= function greet(whattosay) { return function(name) { console.log(whattosay + ' ' + name) } } var sayHi = greet('Hi'); sayHi('Tony'); // 'Hi Tony' ``` 為什麼在 sayHi('Tony') 的函數中可以提取到 name 變數的值?? 因為他是 closure,所以是可能的。 來看看程式背後是如何運作的 ![](https://i.imgur.com/fEjh2yW.png) 在 greet('Hi')執行完後,離開執行堆 ![](https://i.imgur.com/05vKZW5.png) greet 的執行環境結束時,變數的記憶體空間仍在那, ![](https://i.imgur.com/BgmiCKT.png) 當我們回到全域變數呼叫 sayHi('Tony') 這時函數內的變數有使用到 whattosay, 這時他會透過範圍鏈到 sayHi 函數的外部環境 greet() 去找 whattosay, 雖然 greet 的執行環境已經結束,雖然 JS 引擎的垃圾回收機制會釋放不再使用的記憶體, 但因為閉包的機制,讓 JS 的垃圾回收機制沒有啟動,將 whattosay 變數的記憶體位置留下來。 ![](https://i.imgur.com/LJYOjuW.png) 這個包住所有可以取用的變數的現象稱為閉包 ## 十八、瞭解閉包(二) 閉包的經典案例 ```javascript= function buildFunctions() { var arr = []; for(var i = 0; i < 3; i++) { arr.push( function() { console.log(i) } ) } return arr; } var fs = buildFunctions(); fs[0](); //3 fs[1](); //3 fs[2](); //3 ``` 三個得到的結果都是 3,為什麼呢? ![](https://i.imgur.com/TCUdM5Q.png) 在執行 buildFunctions 這個函數時, arr 內被放入了 3 個函數,i 變數則是跑到了 3 離開了 for 迴圈。 執行完後,離開了 buildFunctions 的執行環境, ![](https://i.imgur.com/3tFdKTW.png) 但是變數和函數 在記憶體中會被留下來。 ![](https://i.imgur.com/qvz1NJ0.png) 這時 fs[0] 函數執行,他在函式裡面找 i,找不到,於是他透過範圍鏈到外部環境,也就是原本的 buildFunctions 的變數記憶體位置去找,於是找到 i 為 3。 其他 fs 函數以此類推,於是 console.log 出來的值都是 3 這種取用閉包外部的變數,也稱為自由變數。 那假如我們要按照順序輸出 0, 1, 2 要怎麼做呢 - 使用 let let 會將範圍限制在 {} 裡面, 因此每執行一次 for 迴圈,在記憶體裡面就是一個新的變數, 存在執行環境中的不同記憶體位置。 當 fs[0]() 執行時,就會被指向記憶體中的不同位置。 ```javascript= function buildFunctions() { var arr = []; for(var i = 0; i < 3; i++) { let j = i arr.push( function() { console.log(j) } ) } return arr; } var fs = buildFunctions(); fs[0](); //0 fs[1](); //1 fs[2](); //2 ``` - 使用 IIFE 立即函數創造執行環境 創造不同的執行環境,就可以把變數儲存到不同的記憶體位置。 因此我們透過立即函數來創造新的執行環境。 ```javascript= function buildFunctions() { var arr = []; for(var i = 0; i < 3; i++) { arr.push( (function(j) { return function() { console.log(j) } }(i)) ) } return arr; } var fs = buildFunctions(); fs[0](); //0 fs[1](); //1 fs[2](); //2 ``` ## 十九、框架小叮嚀:Function Factories 做出即使使用同一個函數,但每次執行他時,他會創造新的執行環境 ```javascript= function makeGreeting(language) { return function(firstname, lastname) { if(language === 'en') { console.log('Hello' + firstname + ' ' + lastname) } if(language === 'es') { console.log('Hola' + firstname + ' ' + lastname) } } } var greetEnglish = makeGreeting('en'); var greetSpanish = makeGreeting('es') greetEnglish('John', 'Doe'); greetSpanish('John', 'Doe'); ``` 第一次執行 greetEnglish 時,會到範圍鏈找到 language == en 第二次執行 greetSpanish 時,會到範圍鏈找到 language == es ![](https://i.imgur.com/rvHpA2L.png) ## 二十、閉包與回呼 以下這段程式碼用到一級函數和閉包 ```javascript= function sayHiLater() { var greeting = 'Hi'; setTimeout(function() { console.log(greeting) }, 3000); } sayHiLater(); ``` 在 setTimeout 裡面傳入一個一級函數去做 3 秒後要做的事, console.log(greet) 用到閉包去取用外層範圍鏈 greeeting 的變數 - 一級函數 click 裡面也包了一個一級函數(函數表示式) ```javascript= $("button").click(function() { }) ``` - 回呼函數 當你執行回呼函數時,當他結束前會執行你傳入作為參數的函數 ![](https://i.imgur.com/YaS8bXg.png ```javascript= function tellMeWhenDone(callback) { var a = 1000; var b = 2000; callback(); // 這個會執行傳入的函數 } tellMeWhenDone(function() { console.log('I am done'); }) tellMeWhenDone(function() { console.log('All done...'); }) ``` ## 二十一、call()、apply() 與 bind() 所有的函數物件都可以使用這三種方法 bind() 會拷貝一個函數出來並賦予 this 的指向 apply() 和 bind() 一樣會賦予 this 的指向,不過他們不會拷貝函示,而是直接執行重新指向 this 的函數 - bind 使用 bind 影響 JS 引擎決定 this 變數, 但 bind 不會執行函數,他只是創造一個函數的拷貝 ```javascript= var person = { firstname: 'John', lastname: 'Doe', getFullName: function() { var fullname = this.firstname + ' ' + this.lastname; return fullname; } } var logName = function() { console.log('Logged: ' + this.getFullName()); } var logPersonName = logName.bind(person); logPersonName(); ``` 當 logName.bind(person) 時,bind 會 "拷貝" 出一個 logName 的函數,並把 this 的指向從 window 改成 person。 logName = function() { console.log('Logged: ' + window.getFullName()); } logPersonName = function() { console.log('Logged: ' + person.getFullName()); } logName !== logPersonName - call 讓函數綁定 this 的指向,並且可以傳入參數且執行它。 ```javascript= var person = { firstname: 'John', lastname: 'Doe', getFullName: function() { var fullname = this.firstname + ' ' + this.lastname; return fullname; } } var logName = function(lang1, lang2) { console.log('Logged: ' + this.getFullName()); console.log('Arguments: ' + lang1 + ' ' + lang2) } logName.call(person, 'en', 'es'); // Logged: John Doe // Arguments: en es ``` - apply() apply() 會重新指向 this 後並執行函數, 但不一樣的事 apply 的 argument 必須傳述陣列。 ```javascript= var person = { firstname: 'John', lastname: 'Doe', getFullName: function() { var fullname = this.firstname + ' ' + this.lastname; return fullname; } } var logName = function(lang1, lang2) { console.log('Logged: ' + this.getFullName()); console.log('Arguments: ' + lang1 + ' ' + lang2) } logName.apply(person, ['en', 'es']); // Logged: John Doe // Arguments: en es ``` - 使用 call() 和 apply() 指定 this 指向並創造 IIFE ```javascript= var person = { firstname: 'John', lastname: 'Doe', getFullName: function() { var fullname = this.firstname + ' ' + this.lastname; return fullname; } } (function(lang1, lang2) { console.log('Logged: ' + this.getFullName()); console.log('Arguments: ' + lang1 + ' ' + lang2) }).apply(person, ['en', 'es']) ``` - fumction borrowing 函數借用 函數借用讓你可以用其他物件的方法結合自己本身的屬性。 下面範例 使用 person 的 function 執行,但 function 內的變數指向 person2 的 ```javascript= var person = { firstname: 'John', lastname: 'Doe', getFullName: function() { var fullname = this.firstname + ' ' + this.lastname; return fullname; } } var person2 = { firstname: 'Jane', lastname: 'Doe' } console.log(person.getFullName.call(person2)) // Jane Doe ``` - function currying funciton.bind(this) 會拷貝函數,但是不會執行, 但如果 function.bind(this, 1) 傳入參數的話,就會使這個新函數固定這個傳入值參數。 function currying: 建立一個函數的拷貝,並設定預設的參數 ```javascript= function multiply(a, b) { return a*b; } var multiplyTwo = multiply.bind(this, 2) multiplyTwo(2) // 4 multiplyTwo(100) // 200 ``` ## 二十二、函數程式設計(一) JS 引入一些方法讓你做一些在其他沒有一集函數的程式語言不能做的事 ```javascript= function mapForEach(arr, fn) { var newArr = []; for(var i = 0; i < arr.length; i++) { newArr.push( fn(arr[i]) ) }; return newArr; } var arr1 = [1, 2, 3]; console.log(arr1); var arr2 = mapForEach(arr1, function(item) { return item * 2; }) console.log(arr2); var arr3 = mapForEach(arr1, function(item) { return item > 2; }); console.log(arr3); var checkPastLimit = function(limiter, item) { return item > limiter; } var arr4 = mapForEach(arr1, checkPastLimit.bind(this, 1)); console.log(arr4); var checkPastLimitSimple = function(limiter) { return function(limiter, item) { return item > limiter }.bind(this, limiter); } var arr5 = mapForEach(arr1, checkPastLimitSimple(3)) console.log(arr5) ``` ## 二十三、函數程式設計(二) 瞭接這些程式庫的程式碼來增進自己的 JS 功力 [underscorejs](https://underscorejs.org/) [lodashjs](https://lodash.com/) # 肆、JavaScript 的物件導向與原型繼承 ## 一、觀念小叮嚀:古典和原型繼承 - 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 透過原型鏈可以向他的原型取用物件的屬性和方法 ![](https://i.imgur.com/mKYZmCA.png) 上面這張圖 obj.prop2 像是去 obj 取用 prop2, 但其實是去 obj 的物件原型取用 prop2 ![](https://i.imgur.com/26dPQtH.png) 而原型物件也有他的原型物件,因此他可以透過原型鏈往上取用 obj.prop3 ![](https://i.imgur.com/BjQeGhJ.png) obj2.prop2 也可以做取用。 因為他們可以指向同一個原型。 ```javascript= var person = { firstname: 'Default', lastname: 'Default', getFullName: function() { return this.first + ' ' +this.lastname; } } var john = { firstname: 'John', lastname: 'Doe' } //Don't do this EVER! for demo purpose only john.__proto__ = person; console.log(john.getFullName()); // John Doe console.log(john.firstname); // John ``` john 物件透過範圍鏈取用到 person 原型的 getFullName 的方法。 另外 john.firstname 因為已經在 john 物件中找到 firstname,因此就不會繼續往原型鏈找 firstname ```javascript= var person = { firstname: 'Default', lastname: 'Default', getFullName: function() { return this.first + ' ' +this.lastname; } } var jane = { firstname: 'Jane', } //Don't do this EVER! for demo purpose only jane.__proto__ = person console.log(jane.getFullName()) // Jane Default ``` 當 jane 呼叫 getFullName 時,他在 jane 的物件找到 'Jane',但 jane 的物件並沒有 lastname,因此他透過原型鏈向上找,找到了 lastname 為 'Default' ## 三、所有東西都是物件(或純值) ```javascript= var obj = {}; var fun = function(){}; var arr = []; obj.__proto__ // object fun.__proto__.__proto__ // object arr.__proto__.__proto__ // object ``` ## 四、Reflection 與 Extend - Reflection 一個物件可以看到自己的東西,然後改變自己的屬性與方法 ```javascript= var person = { firstname: 'Default', lastname: 'Default', getFullName: function() { return this.firstname + ' ' + this.lastname; } } var john = { firstname: 'John', lastname: 'Doe' } // don't do this Ever! for demo proposes only!!! john.__proto__ = person; // for in 不只在物件本身取用屬性與方法,還會到原型上找 for(var prop in john) { console.log(prop + ': ' + john[prop]) } // firstname: John // lastname: Doe /* getFullName: function() { return this.firstname + ' ' + this.lastname; }*/ // 使用 for in 做 reflection for(var prop in john) { if(john.hasOwnProperty(prop)) { console.log(prop + ': ' + john[prop]) } } // firstname: John // lastname: Doe ``` - extend 合併多個物件成為一個物件 使用 reflection 完成 extend 功能 參考 underscore.js 作法 ```javascript= var person = { firstname: 'Default', lastname: 'Default', getFullName: function() { return this.firstname + ' ' + this.lastname; } } var john = { firstname: 'John', lastname: 'Doe' } // don't do this Ever! for demo proposes only!!! john.__proto__ = person; var jane = { address: '111 Main St.', getFormalFullName: function() { return this.lastname + ', ' + this.firstname; } } var jim = { getFirstName: function() { return firstname; } } _.extend(john, jane, jim); ``` - underscorejs 的 extend 原始碼 ```javascript= function createAssigner(keysFunc, defaults) { return function(obj) { var length = arguments.length; if (defaults) obj = Object(obj); if (length < 2 || obj == null) return obj; // 如果物件長度小於 2,就回傳原物件 for (var index = 1; index < length; index++) { // 遍歷第二個開始的物件 var source = arguments[index], keys = keysFunc(source), // 當成去取用屬性或方法名稱的函數 l = keys.length; for (var i = 0; i < l; i++) { // 遍歷物件中的屬性與方法 var key = keys[i]; if (!defaults || obj[key] === void 0) obj[key] = source[key]; // 如果物件沒有這個名稱的話,才去指定的這名稱/值 } } return obj; }; } var extend = createAssigner(allKeys); ``` # 伍、建立物件 ## 一、函數建構子、「new」與 JavaScript 的歷史 - 函數建構子 - 可用來設定原型 - 一個函數用來建立物件,當你在呼叫函數前面放了 new 的關鍵字,在執行環境的創造階段被產生的 this 變數,會指向新的空物件。當函數結束執行時,該物件會被函數自動回傳 - new 是一個運算子,當使用 new 的時候,一個空的物件會被建立 - 只要函數用 new 這個運算子,他並不會回傳值,JavaScript 引擎會回傳 new 建立起的物件 ``` javascript= function Person() { console.log(this) this.firstname = 'John'; this.lastname = 'Doe'; } var john = new Person() // 第一個步驟 new 會產生一個空物件 // 第二步驟: 執行函數,但因為使用了 new 運算子,函數內的 this 指向會被導向新的空物件 // this.firstname this.lastname 就會在空物件上新增這兩個名字 console.log(john) /* person = {} person = { firstname = 'John'; lastname = 'Doe'; } */ ``` - 使用同樣的屬性和方法建立更多物件 ```javascript= function Person(firstname, lastname) { this.firstname = firstname; this.lastname = lastname; } var john = new Person('John', 'Doe') console.log(john); var jane = new Person('Jane', 'Doe') console.log(jane); // Person {firstname: "John", lastname: "Doe"} // Person {firstname: "Jane", lastname: "Doe"} ``` ## 二、函數建構子與「.prototype」 ![](https://i.imgur.com/lOTO7ZU.png) - 函數的 prototype 只用在使用函數建構子來建構物件這種特殊用途時才會用到 - 函數的原型屬性不是函數的原型,它是你用函數建構子創造的物件的原型 透過下面的程式碼解釋,Person 的 prototype 和 Person 的 __proto__ 不一樣,Person 的 prototype 是 John 用函數建構子 new 出來的物件原型。 Person 的 __proto__ 會指向 Function Prototype ![](https://i.imgur.com/DMqctUq.png) ```javascript= function Person(firstname, lastname) { this.firstname = firstname; this.lastname = lastname; } Person.prototype.getFullName = function() { return this.firstname + ' ' + this.lastname; } var john = new Person('John', 'Doe') console.log(john); var jane = new Person('Jane', 'Doe') console.log(jane); ``` 另外附上一張網路抓到的解析圖 ![](https://i.imgur.com/CukMXXZ.png) - 屬性在函數建構子內被設定,方法在原型內設定 在設置物件方法時,我們通常會屬性在函數建構子內被設定,方法在原型內設定,因為屬性通常都是不同值,每次函數建構子 new 出來的物件都不同。但方法是可以復用的。如果 new 1000 次物件,物件內的方法都是一樣的,這樣會讓記憶體大量地被佔用,因此建議將方法放到原型,當要取用方法時,透過原型鏈取同一個原型的方法即可 ## 三、危險小叮嚀:「new 」與函數 當使用 new 的時候會建立空物件,當執行函數時,this 會指向空物件,如果你不回傳任何東西的話,他會回傳新物件。 但他仍然是個函數,當你忘記加上 new 的時候,他仍然會執行。 ```javascript= function Person(firstname, lastname) { this.firstname = firstname; this.lastname = lastname; } var john = new Person('John', 'Doe') console.log(john); // undefined ``` 因為沒有回傳任何值,所以原本要被建立的物件會被設定為 undefined, 當你要取用屬性或方法時,會因為他不是物件而噴錯 - 確保不會出錯的方法 - 任何我們要作為函數建構子的函數,命名的第一個字母大寫, - 使用 linters 除錯 ## 四、觀念小叮嚀:內建的函數建構子 - 內建的函數建構子 表示已經存在的函數原型 - 一些已經存在的函數原型 ```javascript= var a = new Number(3); a // Number{[[PrimitiveValue]: 3]} // 將 3 包到物件內 a.toFixed(2); // "3.00" var b = new String('John'); b.indexOf('o'); // 1 'John'.length // 4 // 因為 JS 會直接把字串轉為物件 var c = new Date("3/22/2021") ``` - 在 JS 創造共用的函數建構子 ```javascript= String.prototype.isLengthGreaterThan = function(limit) { return this.length > limit; } console.log("John".isLengthGreaterThan(3)); Number.prototype.isPositive = function() { return this > 0; } var a = new Number(3); a.isPositive(); // true ``` ## 五、危險小叮嚀:內建的函數建構子 內建的函數建構子建立的純值並不是真正的純值,他是"物件"。 建議不要用 JS 內建的函數建構子, 用實體化語法,直接用純值會更加安全。 ## 六、危險小叮嚀:陣列與 for in 儘量不要使用 for in ,因為他會遍歷到原型的屬性。 使用 for(var i = 0; i < length; i++) ```javascript= Array.prototype.myCustomFeature = 'cool'; var arr = ['John', 'Jane', 'Jim']; // new Array('John', 'Jane', 'Jim') // 只是函數建構子的另一種寫法 for (var prop in arr) { console.log(prop + ': ' + arr[prop]); } // 0: John // 1: Jane // 2: Jim // myCustomFeature: cool ``` ## 七、Object.create 與純粹的原型繼承 - Object.create 透過 Object.create 可以創造一個繼承原型的物件 透過物件的點運算子,可以覆蓋屬性,或是隱藏方法。 ![](https://i.imgur.com/2FVSniL.png) ```javascript= var person = { firstname: 'Default', lastname: 'Default', greet: function() { return 'Hi ' + this.firstname; } } // 小提醒, greet function 內的 return this.firstname 的 this 是必須要加的,因為當執行 person.greet() 時,因為物件不會建立新的執行環境,所以他不會在 person 裡面找,他會到全域環境去找,但全域環境沒有這個變數,因此他會噴錯。 var john = Object.create(person); john.firstname = 'John'; john.lastname = 'Doe'; console.log(john); ``` - polyfill polyfill 將程式缺少的功能增加進去 ```javascript= if(!Object.create) { Object.create = function (0) { if(argument.length > 1) { throw new Error('Object.create implementation only accepts the first parameter.') } function F() {}; F.prototype = o; return new F(); } } ``` ## 八、ES6 與類別 - class 只是函數建構子的語法糖,其實做的事都一樣 ```javascript= class Person { constructor(firstname, lastname) { this.firstname = firstname; this.lastname = lastname; } const time = 'morning' greet() { return 'Hi ' + time + this.firstname; } } var john = new Person('John', 'Deo') ``` - extend extend 的用法就是 set prototype ```javascript= class InfornalPerson extends Person { constructor(firstname, lastname) { super(firstname, lastname); } greet() { return 'Yo ' + firstname; } } ``` # 陸、雜談 ## 一、「typeof」、「instanceof」與搞清楚這是什麼 ```javascript= var a = 3; console.log(typeof a); // number var b = "Hello"; console.log(typeof b); // string var c = {}; console.log(c) // object var d = []; console.log(typeof d) // object console.log(Object.prototype.toString.call(a)) // [object array] function Person(name) { this.name = name; } var e = new Person('Jane'); console.log(typeof e); // object console.log(e instanceof Person); // true // 確認 Person 是不是 e 的原型 console.log(typeof undefined); // undefined console.log(typeof null); // object <-- bug var z = function() {}; console.log(typeof z); // funciton // 可以檢查傳過來的參數是不是一級函數 ``` ## 二、嚴謹模式 - 這是額外的,但不能百分之百依賴他 - 嚴謹模式可以單獨放在函數內使用 ```javascript= function logNewPerson() { "use strict"; var person2; persom2 = {}; console.log(persom2) } var person; persom = {}; console.log(persom); logNewPerson() ``` - 注意:當引用數多不同的 js 檔案。如果第一個檔案在全域環境使用了嚴謹模式,那樣子全部的 js 就會是嚴謹模式,可能導致出錯。 - 每個 JavaSrcript 引擎嚴謹模式的規則可能不一樣 # 柒、檢驗知名的框架與資源庫 ## 一、從好的程式碼學習 講師鼓勵學習 JS 可以去 github 上看別人的原始碼, 又或者去看常用的 JS 框架原始碼。 ## 二、深入瞭解原始碼:jQeury(一) ## 三、 ## 四、 ## 五、 ## 六、 # 捌、來打造一個框架/資源庫! # 玖、BONUS 課程準備 ECMAScript 6 # 拾、準備 ECMAScript 6