# +_忍者2 # Ch1 ### 1.1 瞭解 JavaScript 程式語言 JS 是一個更加程序導向([functionally oriented](https://www.geeksforgeeks.org/software-engineering-function-oriented-design/))的程式語言,在本質上跟其他語言有很大的區別: #### 函式是頭等物件(first-class object) 這是一個 functionally programming 的特性 >Chris 補充:物件導向(封裝、繼承、動態連結)處理複雜名詞關係(ex: Java)、function programming 處理複雜動詞關係(處理的問題大多是運算,沒有到很複雜,寫 code 本身是數學證明,像數學邪教) 區別:解決的問題不同 在 JS 裡面就是看情境使用。 [design pattern](https://ithelp.ithome.com.tw/articles/10337622) 可以看一下,是基於物件導向存在的,MVVM用觀察者模式。 #### 函式閉包(function closure) 體現了 function 對 JS 有多重要。 若一個 function 保持了它本體中所使用的外部變數,這就是一個閉包。 #### 可見範圍(scope) JS 還沒有像其他 C 語言般的 block-level variable,因此只好依賴全域和函式變數。 #### 基於原型的物件導向 不同於 C#、Java、Ruby,JS 使用原形來實作物件導向概念,非使用類別。 #### JS 是由 ==物件、原型、函式、閉包==之間的密切關係組成,特別關注以下幾點,可以寫得更優雅、更高效能的 JS: - 生成器(generator):根據不同的請求,產生出多個值,且可以在請求與請求之間暫停其執行的函式。 - 約定(promise):更好控制非同步。 - 代理(proxy):允許控制特定物件存取。 - 進階的陣列方法 - 對應表(map):字典般的資料集合,集合表(set)則用來處理具有唯一性的數值集合。 - 正規表達式:簡化程式。 - 模組:分解程式,便於管理。 #### 使用轉譯器(transplier)嘗試未來的新功能 JS 更新,但還在使用舊的瀏覽器,怎麼辦? 可以使用轉譯器(轉換+編譯),可以將程式碼轉換成現有瀏覽器可正常工作的等效程式碼。 - popular transplier:Traceur、Babel ### 1.2 了解瀏覽器 瀏覽器提供各種概念和 API,可以關注以下幾點: #### 文件物件模型(DOM:Document Object Model) 指客戶端應用程式使用者介面的結構化表示。 #### 事件 JS 絕大多數都是事件驅動,代表程式碼都是在回應特定事件。ex:點擊、滑鼠移動、按下鍵盤等等。 #### 瀏覽器 API 瀏覽器提供了可以跟世界互動的方式 - API,可以存取設備資訊,在本地端儲存資料、或遠端伺服器通訊。 ### 1.3 使用當前的最佳實踐 #### 程式除錯 - 程式除錯技巧 - 測試 - 效能分析 主要的瀏覽器都提供了除錯工具: - Firebug - Chrome DevTools - FireFox Developer Tools - F12 Developer Tools - WebKit Inspector #### 測試 主要工具:自建 assert 函式,目的是確定一個假定到底是真還是假。 ```javascript! var a = 0; assert(a === 1, "Disaster! a is not 1!"); // a 不 === 1 就會跑出後面訊息 // 下方是使用 console.assert(,) 實作 ``` ![image](https://hackmd.io/_uploads/rkBKSjQO0.png) #### 效能分析 ```javascript! console.time("My operation"); for(var n = 0; n < maxCount; n++){ // 需要進行量測的動作 } console.timeEnd("My operation"); ``` ![image](https://hackmd.io/_uploads/H1o5ehX_0.png) #### 提高技能轉移性 # Ch2 需要了解完整的 Web 應用程式生命週期,特別是 ==JS 如何適應這個生命週期==。 ### 2.1 生命週期概述 典型的客戶端 Web 應用程式的生命週期:使用者輸入網址 ~ 使用者離開網頁,由**頁面建立**和**事件處理**這兩階段組成。 ![image](https://hackmd.io/_uploads/HkTxSnXdC.png) > 1. 輸入網址(或點擊超連結) > 2. 產生請求並送往伺服器 > 3. 執行動作或取得個資,將回應送往客戶端 > 4. 處理 HTML、CSS、JS 並建立頁面 > 5. 監看事件佇列,一次處理一個事件 > 6. 與頁面元素互動 > 7. 關閉 Web 應用程式 - 頁面建立:設置使用者介面 - 事件處理:進入一個等待事件發生 (6) 的迴圈 (5),並開始啟用事件處理器 #### 一個具備 GUI、可回應事件的簡單 Web 應用程式 ... 程式碼沒筆記,總之是用 DOM 去處理東西 ### 2.2 頁面建立階段 目標是設置 Web 應用程式的生命週期 UI,有以下兩步: 1. 解析 HTML 並建立文件物件模型(DOM) 2. 執行 JS 程式 HTML 和 DOM 不是同個東西:HTML 是瀏覽器在建立最初 DOM (UI頁面)的時候遵循的藍圖,甚至可以修復在此藍圖發現的錯的東西。 例如:在 head 中放了 p 瀏覽器會去校正,把 p 放到 body 裡面。 #### JS 中的全域物件 window -> 代表頁面所屬的瀏覽視窗,可以藉由他來存取所有其他的全域物件、全域變數,及瀏覽器 API,它擁有的最重要屬性是 document,也就是頁面的 DOM 結構,可以藉此新增刪除修改元素。 >ex:document.getElementById("first") 是一個用來操控 DOM 的方法,它是屬於瀏覽器全域物件 window 中的屬性 document 的方法。這個方法可以讓你透過指定元素的 ID 來選取該元素,從而對該元素進行操作,從而改變頁面的內容和樣式。 #### 全域型、函式型的程式碼 函式型:程式碼處於函式裡面,呼叫函式才會執行 全域型:程式碼會被 JS 引擎自動以逐行方式直接執行 #### 在頁面建立階段執行 JS 程式碼 當瀏覽器在介面建立時,==遇到 script 節點,會暫時停止建立 DOM 結構==(但會繼續維持它的全域狀態) ```javascript! // 問GPT: // 繼續維持它的全域狀態:之前定義的全域變數、函數和全域對象(如 window 和 document)仍然可用,並且事件處理程序和計時器回調等仍然會正常執行。這確保了腳本可以對當前已解析的 DOM 進行操作,並在後續腳本中繼續訪問和修改全域狀態。 // 所以是說 如果我 html 結構中是這樣 <!DOCTYPE html> <html> <head> <script> // A </script> </head> <body> <script> // B </script> </body> </html> // 在執行到 B 的 script 的時候,瀏覽器會暫停繼續解析 DOM 但是 JS 還是可以維持全域狀態,也就是說可以取用之前在 A 命名的全域變數或是一些用 window 定義出來的東西 ``` JS 可以任意修改 DOM 結構,但是不能選取和修改尚未建立的元素(因此也是大家都常把 script 寫在最底的原因) ```javascript! <li id="first"></li> <script> ... ---> 像在這邊就不能選取修改 #second 元素 </script> <li id="second"></li> ``` 在已經執行過的 script 元素中,由使用者所定義的全域變數,能夠繼續使用,因為存放了所有全域變數的==window 全域物件在頁面的整個生命週期內都是有效且可存取的==。 瀏覽器處理完所有的 HTML 元素後頁面建立階段完成,就會開始下一階段的生命週期,事件處理。 ### 2.3 事件處理 可以影響應用程式的全域狀態、修改 DOM 結構和註冊事件監聽器。 >Chris:頁面建立重點在瀏覽器註冊 JS 是單一執行緒, is like 銀行排隊,一次就處理一位客人,同時處理櫃檯會炸裂。 #### 為啥有事件佇列:因為要處理已發生但尚未處理的事件。 所有已發生的事件都按照順序放置在同一個事件佇列中,交由瀏覽器進行檢測。 #### 事件處理過程: 1. 瀏覽器檢查事件佇列頂部 2. 沒有事件,繼續檢查 3. 如果頂部有一個事件,瀏覽器會取得它,執行相關程式,剩餘事件會耐心的在事件著列中等待,直到自己被處理。 - 注意:==將事件放入佇列==的瀏覽器工作機制(判斷事件何時發生、將其送入事件佇列所需的處理工作):不在事件處理的執行緒(頁面建立、事件處理階段)裡面。 #### 事件是非同步的 事件的處理、對事件處理函式的呼叫是非同步的。 可能會發生的事件類型: - 瀏覽器事件,例如網頁處於已載入和未載入的狀態 - 網路事件,例如來自伺服器的回應(Ajax 事件、伺服器端事件) - 使用者事件,例如點擊滑鼠、移動滑鼠和按下鍵盤 - 計時器事件,例如等候逾時或間隔性的觸發條件 事件處理的概念是 Web 應用程式的核心,程式碼是事先設置的,以便之後執行 #### 註冊事件處置器 讓瀏覽器知道我們對某些事件感興趣,可以用以下方式註冊事件: 1. 將函式指定給特殊屬性 (缺點:對一個事件只能指派一個事件處置器) ```javascript! window.onload = function() {} // 將一個函式指派給 window 物件特殊的 onload 屬性 document.body.onclick = function() {} // 為 document 主體的 click 事件註冊一個處置器 Chris 補充: 如果用這種方法 window.onload 完只能做一件事 (但若我有這些情境:點按鈕後送出表單,打API、回到上一頁、跑 loading,就需要多做幾件事) ``` 2. 使用內建的 addEventListener 方法(可以註冊多個事件處理函式) ```javascript! <script> document.body.addEventListener("mousemove", function() { var second = document.getElementById("second"); addMessage(second, "Event: mousemove"); -> 為 mousemove 註冊事件處置器 }); document.body.addEventListener("click", function(){ var second = document.getElementById("second"); addmessage(second, "Event: click"); -> 為 click 註冊事件處置器 }); </script> // 事件迴圈頂部會先取得 mousemove 事件,並執行相關的事件處理器,處理完成後,會將它移出事件佇列,而此時著列頂端是 click 滑鼠事件。 ``` - GPT 給的優劣比較 ```html! <!-- 將函式指定給特殊屬性 --> <!DOCTYPE html> <html> <head> <title>事件屬性範例</title> </head> <body> <button id="myButton">點擊我</button> <script> const button = document.getElementById('myButton'); // 指定第一個事件處理程序 button.onclick = function() { alert('按鈕被點擊了!(第一次)'); }; // 再次指定事件處理程序,將會覆蓋前一個 button.onclick = function() { alert('按鈕被點擊了!(第二次)'); }; </script> </body> </html> <!-- 缺點:只有最後一個指定的處理程序會生效,前面的處理程序會被覆蓋。 --> ``` ```html! <!-- 使用內建的 addEventListener 方法 --> <!DOCTYPE html> <html> <head> <title>addEventListener 範例</title> </head> <body> <button id="myButton">點擊我</button> <script> const button = document.getElementById('myButton'); // 添加第一個事件處理程序 button.addEventListener('click', function() { alert('按鈕被點擊了!(第一次)'); }); // 添加第二個事件處理程序 button.addEventListener('click', function() { alert('按鈕被點擊了!(第二次)'); }); </script> </body> </html> <!-- 可以為同一事件添加多個處理程序,兩個處理程序都會依次執行,輕鬆添加和移除事件處理程序,並且支持事件捕獲和冒泡。 --> ``` #### 處理事件 當事件發生時,瀏覽器呼叫相關的事件處理器。 由於單執行緒,所以一次只能處理一個事件,處理完才會處理其他事件。 1. 事件迴圈檢查佇列 2. 發現佇列前面有一個 mousemove 事件 3. 執行相關處理器 4. click 事件在佇列中等待 5. 當 mousemove 事件被處理完成 6. 事件迴圈會再次檢查佇列 7. 事件迴圈會在佇列找到 click 事件並開始處理它 8. click 處置器執行完畢 9. 佇列沒有新的事件,事件迴圈繼續循環等待處理新的事件,直到使用者關閉程式 非同步:是因為 JS 要適應瀏覽器 call 是主動呼叫 invoke 是被動處理 call function 去把 secTimeout 放到佇列,在佇列裡要執行時 "被"瀏覽器 invoked # Ch3 ### 3.1 使用函式與否的差異為何? function 作為頭等物件是 JS 一大優勢,強健穩固的 function 會讓我們在開發時候有很大的靈活性和控制權。 JS 的 物件 特性: 1. 可以由實值建立:`{}` 2. 可以被指派給變數、陣列資料或其他物件的屬性: ```javascript! var ninja = {}; ninjaArray.push({}); minja.data = {}; ``` 3. 作為引數傳遞給函式 ```javascript! function hide(ninja) { ninja.visibility = false; } hide({}); // 將新建立的物件作為引數傳遞給函式 ``` 4. 可以作為函式的回傳值 ```javascript! function returnNewNinja() { return {} } // 從函式回傳一個新物件 ``` 5. 擁有可動態建立和指派的屬性 ```javascript! var ninja = {}; ninja.name = "Hanzo"; // 從物件上新增一個屬性 ``` #### 函式做為頭等物件 函式擁有物件的所有功能,是個可以被呼叫的函式,它可以: 1. 由實值建立: ``` function ninjaFunction() {} ``` 2. 可以被指派給變數、陣列資料或其他物件的屬性: ```javascript! var ninjaFunction() {} ninjaArray.push(function(){}) ninja.data = function(){} ``` 3. 作為引數傳遞給函式 ```javascript! function call(ninjafunction) { ninjaFunction(); } ``` 4. 可以作為函式的回傳值 ```javascript! function returnNinjaFunction(){ return function() {} } ``` 5. 擁有可動態建立和指派的屬性 ```javascript! var ninjaFunction = function() { ninjaFunction.ninja = "Hanzo"; } ``` 函式做為頭等物件是邁向 functional proggramming 的第一步,這種風格的重點在於:透過撰寫函式來解決問題。 #### 回呼函式 將函式做為引數給另外一個函式,並在稍後某個時機點呼叫這個函式,就是回呼 call back。 ```javascript! function useless(ninjaCallback) { return ninjaCallback(); } ``` #### 一個簡單的回呼範例 ```javascript! <script> var text = 'Domo arigato!'; report("Before defining functions"); function useless(ninjaCallback) { report("In useless function"); return ninjaCallback(); } // 定義一個以回呼函釋為傳入參數的函式,在這邊會立即呼叫被傳入的 ninjaCallback() function getText() { report("In getText function"); return text; } // 定義一個回傳某個全域變數值的簡單函式 report("Before making all the calls"); assert(useless(getText) === text, "The useless function works! " + text); // 直接定義一個作為引數的回呼函式 report("After the calls have been made"); </script> // 這整段:getText 作為 useless 的引數,然後在 useless 內又直接呼叫傳入的函式,所以直接執行了 getText(所以 call back 了 getText) //也可以改寫為:直接定義一個作為引數的回呼函式 assert(useless( function () { return text; }) === text, "The useless function works! " + text); ``` JS 可以在任何地方建立函式,此特性可以讓程式碼更緊湊、也避免全域污染,例如 GPT 的舉例: ```javascript! // 全域範圍內定義的函式 function greet(name) { return `Hello, ${name}!`; } console.log(greet('Alice')); // 輸出: Hello, Alice! console.log(greet('Bob')); // 輸出: Hello, Bob! function createGreeting(name) { // 在這裡定義的函式 function greet(name) { return `Hello, ${name}!`; } return greet(name); } console.log(createGreeting('Alice')); // 輸出: Hello, Alice! console.log(createGreeting('Bob')); // 輸出: Hello, Bob! // 兩者就有作用域的差別,greet 函式是定義在 createGreeting 函式內部的。因此 greet 只在 createGreeting 函式內部可見和可用。 ``` #### 使用比對器進行排序 我們有陣列想要進行排序,可以使用 sort 方法,並且定義一個拿來進行比較的回呼函式,當需要進行比較的時候,就會呼叫這個回呼函式。 ```javascript! var values = [0, 3, 2, 5, 7, 4, 8, 1] values.sort(function(value1, value2){ return value1 - value2; }) // 細節:如果傳入的值應該反轉順序,預期要 return 一個正整數;如果不需要就返回負數;兩個值相等就返回零,把傳入的值相減就可以依照 return 的值來排列順序。 ``` ### 3.2 函式作為物件的有趣之處 - 建立物件指派屬性 ```javascript! var ninja = {}; ninja.name = "hitsuke"; ``` - 建立函式指派屬性 ```javascript! var weildSword = function(){}; weildSword.swordType = "katana"; ``` #### 3.2.1 儲存函式 ==不太懂這個範例== ```javascript! var store = { nextId: 1, // 紀錄下一個可以被指派的 id cache: {}, // 建立被當作快取的物件,可以把多個函式儲存其中 add: function(fn) { if (!fn.id) { // 檢查函式是否已有 id 屬性 fn.id = this.nextId++; // 如果沒有,指派一個 id 並增加 nextId this.cache[fn.id] = fn; // 將函式添加到快取中 return true; // 返回 true 表示函式被添加 } return false; // 返回 false 表示函式已存在,不需要再添加 } }; function ninja() {} assert(store.add(ninja), "Function was safely added."); // 這應該返回 true,因為 ninja 函式是第一次添加 assert(!store.add(ninja), "But it was only added once"); // 這應該返回 false,因為 ninja 函式已經存在 // 如果 ninja 被加到 catch 裡面會長這樣: // this.cache[ninja.id] = ninja ; // -> catch : { // 1: function ninja() {} // } ``` #### 3.2.2 自我記憶函式(self-moemoizing function) 建立一個函式,使其能夠記住之前的計算值。(將結果和函式參數一起儲存起來,下次要用這組參數進行呼叫的時候,就不用重新計算) ```javascript! function isPrime(value) { // 檢查是否已經建立該屬性了,如果沒有就建立它 if (!isPrime.answers) { isPrime.answers = {}; } // 針對該傳入的值,檢查屬於該值的計算結果是否已經被暫存在 answers 中,如果屬性存在,return isPrime.answers[value]; 會直接返回這個值(true 或 false) if (isPrime.answers[value] !== undefined) { return isPrime.answers[value]; } // 初始化 prime 為 true,並排除 0 和 1 這兩個非質數 var prime = value !== 0 && value !== 1; // 從 2 開始檢查到 value 的前一個數字 for (var i = 2; i < value; i++) { if (value % i === 0) { prime = false; break; } } // 將計算結果儲存在緩存中,並返回結果 return isPrime.answers[value] = prime; } // 測試 assert(isPrime(5), "5 is prime!"); assert(isPrime.answers[5], "The answer was cached!"); // 在此 我們的暫存是函式本身的一個屬性,所以他的生命週期等於函式本身的生命週期。 ``` ==我是有點不太懂這邊想要表達什麼== >此種做法有兩個優點: 藉由取得之前計算過的值,讓終端使用者在呼叫函式時得到效能上的改善。 他不動聲息地在幕後運行,無論是終端使用者,還是頁面製作者,都不需要執行任何特殊的請求或做任何額外的初始化,來使一切發生。 完美並不美,考慮一下缺點: 任何類型的暫存都是花費記憶體空間來換取效能。 純粹主義者可能認為暫存是一個不應該與業務邏輯放在一起的事物,一個函式或一個方法應該只做一件事,並起把它做好,不過先不要擔心,第八章他會再講。 很難對這樣的演算法進行附載測試或測量其效能,因為我們的結果取決於之前對函式提供的輸入值。 ### 3.3 定義函式 無論你有沒有意識到,但你其實一直都在把函式跟像字串、數字這樣的值一樣拿來實質定義,因為函式是什麼?——頭等物件。 #### JS 提供了四種定義函式的方法: > 必須瞭解這之間的差異,因為會影響到:函式可以被呼叫的時機、函式的行為,以及哪些物件上的函式可被呼叫。 1. 函式宣告(declaration)、函式表達式(expression):了解這兩者的差異可以幫助知道:函是在什麼時候可以被呼叫。 ```javascript! function myFun() { return 1; } ``` 2. 箭頭函式:ES6新功能,利用更簡潔語法定義函式,解決的回呼函式的常見問題 ```javascript! myArg => myArg*2 ``` 3. 函式建構式(constructor):一種不太常見的方法,可使我們從一個可以動態產生的字串中,動態地建立一個新的函式,底下的例子動態的建立具有兩個參數(a 和 b)的函式 ```javascript! new Function('a', 'b', 'return a + b') ``` 4. 生成器(generator)函式:ES6新功能,可以在應用程式執行時退出和重新進入他們,同時在這些不同的進入點之間保持他們的變數值,我們可以用生成器來定義函式宣告、函式表達式和函是建構式 ```javascript! function* myGen() { yeild 1; } ``` #### 3.3.1 函式宣告和函式表達式 1. 函式宣告 - 必要:function 開頭 + 函式名稱(函式名稱是必要的,因為必須要有呼叫他的方式,唯一的方式就是藉由他的名稱) - 括號內用逗號分隔的參數名稱列表 - 需要被包在 {} 裡(函式主體是零個或多個敘述句) - 一個函式宣告需獨立放置,作為一個單獨的 JS 敘述句。 ![image](https://hackmd.io/_uploads/H1Kp0UkYA.png) ```javascript! function samurai() { return "samurai here"; } function nunja() { function hiddenNinja() { return "ninja here"; } } ---> 展示了把函式在函式中宣告 ``` 2. 函式表達式:總是作為敘述句內容的函式(例如:作為指派表達式的右側,或作為另一個函式的參數) - 函式名稱可有可無 - 可以用變數呼叫 - 可以用參數名稱呼叫 - (小補充:函式只有在執行到那一行時才會被定義。) 函式可以被當作如同其他表達式來使用,像使用數字實值一樣。 ```javascript! var a = 3; myFunction(4); var a = function(){}; myFunction(function() {}); ``` 兩者比較 ```javascript! function myFunctionDeclaration() { function ineerFunction() { } } var myFunc = function() {}; myFunc(function(){ return function() {}; ---> 作為另外一個函式呼叫的參數 }) (function namedFunctionExpression () { })() ---> 具名的函式表達式作為立即函式的一部分 +function(){}(); -function(){}(); !function(){}(); ~function(){}(); ---> 作為一元運算子的引述,而會被立即呼叫的函式表達式 ``` ```javascript! // 用變數呼叫 var doNothing = function() {}; doNothing(); // 用參數名稱呼叫 function doSomething(action) { action(); } ``` 3. 立即函式 ```javascript! // 標準函式呼叫 myFunctionName(3); // 一個執行函式識別項的表達式(GPT:函數識別項是指函數的名稱,用於引用並調用該函數) // 對一個函式表達式的立即呼叫 (function() {})(3); // 一個執行函式表達式的表達式 // GPT 舉例: (function(x) { console.log(x * 2); // 假設這是我們的函數內容 })(3); // 立即執行並傳入參數 3 ``` ==我的感覺啦:所以是說當我們要呼叫一個函式的時候,除了使用函式識別項,也可用變數、參數,或是一個立即呼叫的函式表達式,只要可以代表函式識別項即可== #### 圍繞函式表達式的括號 為什麼會需要它們? ```javascript! function(){}(3); // 例如這樣,function 關鍵字開頭的敘述句,代表一個函式宣告,但是這邊我們又沒有給他命名,所以會拋出一個錯誤,因此我們需要將其括號: (function(){})(3); // 這麼一來:JS 就會解析為一個表達式,不是敘述句。 (function(){}(3)); // 或是:直接都括號起來 ``` ```javascript! // 也可以這樣直接告訴 JS 我這是一個表達式不是敘述句: +function(){}(); -function(){}(); !function(){}(); ~function(){}(); ``` #### 箭頭函式 ```javascript! // 語法: param => expression // 此箭頭函式接受一個參數 param 並返回一個表達式 expression 的值。 ``` - 必要:() - 零個或多於一個參數時 / 一個參數的話可有可無 - 必要:箭頭運算子 => - 如果函式主體只有一個表達式,表達式的值就是函式回傳值 - 如果函式主體是一個程式區塊,如果沒有 return 敘述句,回傳值就是 undefined,反之,回傳值就是 return 敘述句所回傳的值 ![image](https://hackmd.io/_uploads/Hy-bzKyYC.png) ```javascript! // 一般寫法 var values = [0, 3, 2, 5, 7, 4, 8, 1]; values.sort(function(value1, value2) { return value1 - value2; }); // 箭頭函式寫法: var values = [0, 3, 2, 5, 7, 4, 8, 1]; values.sort((value1, value2) => value1 - value2); // 屏除了大括號、function 關鍵字、return 敘述句,並增加了一個運算子:=> 即是定義一個箭頭函式的關鍵組成物。 ``` ### 3.4 引數與函式參數 - 參數是在函式中定義的變數 - 引數是呼叫函式時傳遞給他的值 ![image](https://hackmd.io/_uploads/Hkf3RK1YA.png) 第一個引數會分配給第一個參數,第二個給第二個,以此類推,但是我們給參數不同數量的引數的時候,不會報錯,只是多餘的引數不會指派給任何參數(但仍然有辦法存取它們,下一章會講)。 #### 3.4.1 不定參數 只有最後一個參數可以作為不定參數! ==這邊有點混雜== ```javascript! function multiMax(first, ...remainingNumbers) { var sorted = remainingNumbers.sort(function(a,b){ return b - a; }); return first * sorted[0]; } assert(multiMax(3, 1, 2, 3) == 9, "3*3=9 (First arg, by largest.)"); // ...remainingNumbers 是剩下的所有參數,收集到一個陣列中。 ``` #### 3.4.2 預設參數 ==這邊在幹嘛也不太懂== ```javascript! function performAction(ninja, action) { action = typeof actioin === "undefined" ? "skulking" : "action"; // 如果參數 action 是 undefined 就使用預設值 slulking 如果有給值,就使用傳入的值 return ninja + "" + action; } assert(performAction('Fuma') === "Fuma skulking", "The default value is used for Fuma"); assert(performAction('') === "Yoshi skulking", "The default value is used for Yoshi"); assert(performAction('Hattori') === "Hattori skulking", "The default value is used for Hattori"); // 由於 performAction 沒有傳入第二個引數,所以 action 參數會保持預設值,也就是 skulking assert(performAction('Yanyu', 'sneaking') === "Yanyu sneaking", "Yanyu can do whatever he oleasesm even for sneak!"); // 傳入一個字串作為參數 action 的值,這個值便會為函式所用 ``` 這是一種令人厭煩????的做法,所以 ES6 新增了以下預設參數功能 ```javascript! // ES6 中可以指定對某個參數給值 function performAction(ninja, action = "skulking"){ return ninja + " " + action; } // 沒有傳入值,就使用預設參數值 assert(performAction("Fuma") === "Fuma skulking"), "The default value is used for Fuma"); assert(performAction("Yoshi") === "Yoshi skulking"), "The default value is used for Yoshi"); assert(performAction("Hattori") === "Hattori skulking"), "The default value is used for Hattori"); // 使用傳入的值 assert(performAction("Yanyu", "sneaking") === "Yanyu sneaking", "Yanyu can do whatever he oleasesm even for sneak!"); ``` 可以為預設參數指派任何值,舉凡數字、字串、物件、陣列、函式。 每次函式被呼叫時,會從左到右取值,再將值指派給後面的預設參數時,我們可以引用之前的參數,如下面的程式列表所示。 ```javascript! function performAction(ninja, action = "skulking", message = ninja + " " + action) { retturn message; } --> 這邊的第三個參數就使用了第一個參數 ninja 和 action assert(perfornAction("Yoshi") === "Yoshi skulking", "Yoshi is skulking"); // GPT 舉例 function calculatePrice(basePrice, tax = basePrice * 0.1, discount = 0) { return basePrice + tax - discount; } undefined calculatePrice(100) // 110 只有第一個引數 calculatePrice(100, 20) // 120 有第二個就以第二個傳入的為主 calculatePrice(100, 20, 10) // 110 ``` #### 習題 ```javascript! 2) 根據函式的類型(函式宣告、函式表達式或箭頭函式)進行分類 numbers.sort(functioin sortAsc(a,b){ return a - b; }) // 作為引數傳遞給其他函式的函式表達式 numbers.sort((a,b) => b - a); // 作為引數傳遞給其他函式的箭頭函式 (function(){})(); // 在呼叫表達式裡作為被呼叫方的一個函式表達式 functioin outer(){ function inner(){ return inner; } } // 兩個都是函式宣告 (function(){}()); // 被包裹在表達式中的一個函式表達式呼叫 (()=> "Yoshi")() // 作為被呼叫方的一個箭頭函式 4) 在測試函式的主體內,參數 a、b、c 在兩次函式呼叫時的值會是什麼? function test(a, b, ...c) { /*a, b, c*/ } test(1, 2, 3, 4, 5); // a = 1, b = 2, c = [3, 4, 5]; test(); // a = undefined; b = undefined; c = [] ``` # Ch4 *p.67* 函式的隱含參數: - this:==函式所處的背景空間(context)== 即 ==函式被呼叫時所屬的物件==,是 JS 物件導向的其中一項基本要素。 - argument 參數:代表在==進行函式呼叫時所傳遞的所有引數==。 > 呼叫函式的方式對如何判定函式的隱含參數有很大影響。 *p68* #### 你知道嗎? 為什麼 this 這個參數被稱為函式背景空間?函式和方法之間有什麼區別?如果一個建構器函式明確地回傳一個物件,此時會發生什麼事? ### 4.1 使用函式隱含引數 所謂隱含,是指這些參數沒有明確列在==函式署名(function signature 中),而是默默地傳給函式==,但我們可以在函式內存取它們。 (補充:函式署名 >> 函式名稱或引數) #### 4.1.1 arguments 參數 允許我們存取所有傳遞給函式的引數,而不用去管==相對應的參數是否有明確定義==。 這讓我們可以實現 JS 本身不支援的函式重載(function overloading),以及能夠接受可變參數數量的可變函式(variadic function)。 ##### 自己補充:函式重載/可變函式 ```javascript! // 函式重載:指的是可以在"同一個作用域"內定義多個具有相同名稱但參數不同的函式。例如在 C++ 中,可以這樣做: void print(int i); void print(double d); void print(string s); // 在 JS 中是不支援的。如果在同一個範疇內定義多個同名函式,後面定義的會覆蓋前面定義的函式。 --- // 可變函式(variadic function):指可以接受不定數量參數的函式。 // 在 JS 中,我們可以使用 arguments 物件或 ES6 引入的剩餘參數語法來實現這種函式。 ``` arguments 物件有 length 的屬性,可以用 arguments.length 檢查傳遞給函式的引數數量。 單個引數值可以藉由使用==陣列索引符號==獲得,例如,arguments[2] 將取得第三個引數。(但它不是一個陣列喔!) ```javascript! function whatever(a, b, c) { assert(a === 1, "Disaster! a is not 1!"); assert(b === 2, "Disaster! b is not 2!"); assert(c === 3, "Disaster! c is not 3!"); assert(arguments.length === 5, 'We have passed in 5 parameters'); assert(arguments[0] === a, 'The first argument is assigned to a'); assert(arguments[1] === b, 'The second argument is assigned to b'); assert(arguments[2] === c, 'The third argument is assigned to c'); assert(arguments[3] === 4, 'We can access the fourth argument'); assert(arguments[4] === 5, 'We can access the fifth argument'); } whatever(1, 2, 3, 4, 5); // 呼叫了五個引數,但實際上我們只宣告了三個參數。這就是使用 arguments 來實現可變函式(也就是實現:可以接受"不定數量參數"的函式)。 ``` > 偷看到第四章了,好快好扯好厲害!!! > [name=Jeremy] > 第一頁而已 XD 剛剛看不下去又收起來了,颱風天在家真的是效率極低,有非常多讀一讀跑去吃餅乾或睡覺的可能✌🏻 > [name=Jamixcs] > 肯定是要有人在旁邊鞭策的 > [name=Jeremy] > 🥹🥲🥲🥲 > [name=Jamixcs] *p70* ```javascript! function sum() { var sum = 0; for (let i = 0; i < arguments.length; i++) { sum += arguments[i]; } return sum; } assert(sum(1, 2) === 3, "We can add two numbers"); assert(sum(1, 2, 3) === 6, "We can add three numbers"); assert(sum(1, 2, 3, 4) === 10, "We can add four numbers"); ``` *p71* #### 注意:很多情況下可以用不定參數來取代 arguments 參數,不定參數是一個真正的陣列。 ```javascript! // 用不定參數改寫 function sum(...args) { var sum = 0; for (let i = 0; i < args.length; i++) { sum += args[i]; } return sum; } assert(sum(1, 2) === 3, "We can add two numbers"); assert(sum(1, 2, 3) === 6, "We can add three numbers"); assert(sum(1, 2, 3, 4) === 10, "We can add four numbers"); ``` ##### 自己補充:宣告函式時不定義參數呢? ```javascript! // 當在定義函式時不宣告參數,但在呼叫函式時傳入了"引數",這些引數會自動成為 arguments 物件中的元素。 function example() { console.log(`argument list: ${arguments[0]}, ${arguments[1]}, ${arguments[2]}`); } example(10, 20, 30); // argument list: 10, 20, 30 ``` *p71* #### arguments 物件作為函式參數的別名(alias) 作者稱它為古怪功用🫣 ex:如果我們對 arguments[0] 設定一個新的值,==第一個參數的值==也會被改變。 ```javascript! function infiltrate(person) { assert(person === 'gardener', 'The person is a gardener'); assert(arguments[0] === 'gardener', 'The first arguments is a gardener'); arguments[0] = 'ninja'; assert(person === 'ninja', 'The person is a ninja now'); assert(arguments[0] === 'ninja', 'The first arguments is a ninja'); person = 'gardener'; assert(person === 'gardener', 'The person is a gardener once more'); assert(arguments[0] === 'gardener', 'The first arguments is a gardener again'); } infiltrate("gardener"); ``` ==別名的作用是雙向的== 用 arguments 物件作為函式參數的別名,可能會有造成混淆的風險,因此 JS 有提供一種方法來禁用它:使用==嚴格模式==。 *p73* ```javascript! "use strict"; ---> 啟用嚴格模式 function infiltrate(person) { assert(person === 'gardener', 'The person is a gardener'); assert(arguments[0] === 'gardener', 'The first arguments is a gardener'); arguments[0] = 'ninja'; assert(arguments[0] === 'ninja', 'The first arguments is a ninja now'); assert(person === 'gardener', 'The person is still a gardener'); ---> person 的值未改變 infiltrate("gardener"); // 此時 arguments 不再是 參數的別名! ``` #### 自己小練習 ```javascript! function whoIAm(name) { console.log(`name is:${name}`); arguments[0] = 'FangFang'; console.log(`arguments[0] is:${arguments[0]}\nname is:${name}`); name = 'Watson'; console.log(`arguments[0] is:${arguments[0]}\nname is:${name}`); } whoIAm('Jami'); // name is:Jami // // arguments[0] is:FangFang // name is:FangFang // arguments[0] is:Watson // name is:Watson ``` ```javascript! // 嚴格模式 function whoIAm(name) { "use strict" console.log(`name is:${name}`); arguments[0] = 'FangFang'; console.log(`arguments[0] is:${arguments[0]}\nname is:${name}`); name = 'Watson'; console.log(`arguments[0] is:${arguments[0]}\nname is:${name}`); } whoIAm('Jami'); // name is:Jami // arguments[0] is:FangFang // name is:Jami // arguments[0] is:FangFang // name is:Watson ``` *p74* #### 4.1.2 this 參數:介紹函式背景空間(context) 當函式被呼叫的時候,代表==呼叫時引數值的參數==(GPT:在呼叫函式的那一刻,引數的值會被傳遞給函式定義中的參數),並會將名為 this 的隱含式參數傳遞給函式。 this 是 JS 在物件導向上的一個重要組成部分,指的是==與函數呼叫相關聯的物件==。因此,它通常被稱為函式背景空間(function context)。 this 參數所指向的東西會由==函式定義的方式和位置==來決定,==也會受到函式呼叫方式==的影響。 > 不同函式呼叫的方式,會影響如何決定 this 的值。 ### 4.2 呼叫函式 呼叫函式的時候,到底是發生啥事??你曾想過,呼叫函式對程式運作有什麼影響嗎,this 參數將會如何被建立? #### 四種呼叫函式的方式 1. 作為函式 - skulk() 這是一般的呼叫形式 2. 作為一個方法 - ninja.skulk() 把呼叫綁定到一個物件上,以提供物件導向的程式設計方式 3. 作為一個建構器函式 - new Ninja() 來創造新的物件 4. 經由函式的 apply 或 call 方法 - skulk.call(ninja) 或 skulk.apply(ninja) *p75* #### 4.2.1 作為函式來呼叫 一個函式本來就要作為函式呼叫啊,這邊說的是和 .call/.apply/建構器函式的區別。 ```javascript! function ninja(){}; ninja(); // 被當作函式來呼叫的函式宣告 var samurai = function(){}; samurai(); // 被當作函式來呼叫的函式表達式 (function(){})(); //被當作函式來呼叫的立即函表達式 ``` 在這幾種呼叫方式的時候 this 是全域的背景空間(window 物件),嚴格模式下是 undefined。 *p76* ```javascript! // 將函式作為函式呼叫,this 預設會是 window 物件,用嚴格模式才是 undefined function ninja() { return this } console.log(ninja()) // Window {window: Window, self: Window, document: document, name: 'Jami', location: Location, …} function samurai() { "use strict"; return this; } console.log(samurai()) // undefined ``` *p77* #### 4.2.2 作為方法呼叫 當函式被指派為物件的屬性,並且使用屬性來呼叫函式的時候,就是被作為==物件的方法==來呼叫。 物件導向關鍵機制:在這個方法的主體中,我們可以用 this 來取得這個方法所屬的物件。 > 當函式作為物件的方法呼叫的時候,這個物件就會成為函式的背景空間,因此可以用 this 參數去存取。 ```javascript! var ninja = {}; ninja.skulk = function(){}; // 將一個匿名函式賦值給 ninja 的 skulk 屬性 ninja.skulk(); // 作為方法呼叫 ``` ##### 我自己舉例 ```javascript! const ninja = { name: 'Jami', greet: function() { console.log(`Hi I am ${this.name}`); } } ninja.greet(); // Hi I am Jami // 當 greet 函式被當作 ninja 方法來呼叫的時候,this 參數會指向 ninja,ninja 物件會成為 greet 函式的背景空間。 // 因此可以用 this 參數存取,所以 this.name 可存取到 ninja 背景中的 name: 'Jami' ``` #### 函式呼叫與方法呼叫間的差異 程式範例在講:即便都在使用同一個函式,但是函式的背景空間是什麼碗糕,取決於==如何呼叫它==。 ```javascript! function whatsMyContext() { return this; } assert(whatsMyContext() === window, "Function call on window"); // whatsMyContext() 被當作函式呼叫,背景空間會在 window var getMyThis = whatsMyContext; // getMyThis 取得了一個指向 whatsMyContext 的參照 assert(getMyThis() === window, "Another function call in window"); // 用變數呼叫函式,但還是被當作用函式呼叫,背景空間依然是 window 物件 var ninja1 = { getMyThis: whatsMyContext }; // 建立一個名為 ninja1 的物件,他有一個 getMyThis 屬性,是參照到 whatsMyContext 函式 assert(ninja1.getMyThis() === ninja1, "Working with 1st ninja"); // 此時 ninja1.getMyThis() 被用 ninja1 的 getMyThis() 方法來呼叫,因此!背景空間是在 ninja1,這便是物件導向作法。 var ninja2 = { getMyThis: whatsMyContext }; // ninja2 物件也有一個參照到 whatsMyContext 的 getMyThis 屬性 assert(ninja2.getMyThis() === ninja2, "Working with 2nd ninja"); // 透過 ninja2 物件上的 getMyThis() 方法呼叫,此函式的背景空間為 ninja2 ``` *p79* >不需要為了不同的物件,建立不同的函式複本進行相同的操作,這是物件導向的基本原則。 #### ??? GPT 舉例 ```javascript! // 相對錯誤的方式:為每個物件建立不同的函式複本 const person1 = { name: 'Alice', introduce: function() { console.log(`Hi, my name is ${this.name}.`); } }; const person2 = { name: 'Bob', introduce: function() { console.log(`Hi, my name is ${this.name}.`); } }; person1.introduce(); // Hi, my name is Alice. person2.introduce(); // Hi, my name is Bob. // 正確的方式:利用方法來進行操作 function introduce() { console.log(`Hi, my name is ${this.name}.`); } const person1 = { name: 'Alice', introduce: introduce }; const person2 = { name: 'Bob', introduce: introduce }; person1.introduce(); // Hi, my name is Alice. person2.introduce(); // Hi, my name is Bob. ``` ##### 我自己的小結論 >建立一個共用的 method 並且指向各個物件,當我以 method 來呼叫這些函式的時候,背景空間自然會成為==呼叫的該物件本身==,因此 this 是不會混淆到的,會正確的被建立在當下的那個物件。 *p80* #### 4.2.3 作為建構器來呼叫 建構器函式宣告就像任何函式一樣,可以使用函式宣告和函式表達式來建立新物件。 ==唯一的例外:箭頭函式。== 要使用建構器函式來呼叫函式,只要在呼叫前加上關鍵字 new ```javascript! function whatMyContext(){ return this; } new whatMyContext(); ``` #### 注意 函式建構器 / 建構器函式 - 函式建構器讓我們能夠用動態建立的字串來建立樣式。 - 建構器函式,用於建立和初始化物件實例的函式。 > A function constructor enables us to create functions from dynamically created strings. On the other hand, constructor functions, the topic of this section, are functions that we use to create and initialize object instances. *p81* #### 建構器的超能力 ```javascript! function Ninja() { this.skulk = function() { return this; } } // 初始化一個名為 Ninja 的建構器函式,並在 skulk 方法中設定一個函式會回傳 this 背景空間 var ninja1 = new Ninja(); ---> 用關鍵字 new 呼叫的時候,會建立一個空的物件實例,並將其物件作為函式背景空間(this 參數)傳遞給函式。 var ninja2 = new Ninja(); assert(ninja1.skulk() === ninja1, "The 1st ninja1 is skulking"); assert(ninja2.skulk() === ninja2, "The 1st ninja2 is skulking"); // 當呼叫 ninja1.skulk() ninja2.skulk() 的時候,this 的背景空間就會變成是 ninja1 . ninja2 ``` new 呼叫函式的時候會: 1. 建立一個新的空物件。 2. 此物件被當成 this 參數傳遞給建構器,因此成為建構器的函式背景空間。 `(ninja1.skulk() -> ninja1 被當成 this 參數)` 3. new 運算子會回傳新建立的物件(有例外,之後會談)。 *p82* ![image](https://hackmd.io/_uploads/BkYoe0tYA.png) ```javascript! 1. 創造一個新的空物件 ninja1 2. ninja1 被當成 this 參數 3. 添加屬性到這個物件 ninja1 = { skulk: function() { return this; } } 4. 因此 skulk 的背景空間就是 ninja1 ``` >建構器的目的是:建立一個物件、設置好它,將它作為建構器的值回傳。 任何會干擾這個意圖的事物都不應該出現在建構器裡。 > >我自己翻譯的白話文:new 一個建構器函式後,會創建並初始化一個新物件,並自動返回這個新物件本身。 *p83* ### 建構器的回傳值 回傳基礎型值(primitive value)的建構器函式 ```javascript! function Ninja() { this.skulk = function() { return true; } return 1; } assert(Ninja() === 1); // true var ninja = new Ninja(); assert(typeof ninja === "object"); // true, assert(typeof ninja.skulk === "function"); // true // 此測試證明了 new 一個建構器函式之後,被創立的這個物件會回傳本身,也就是上面的白話文。 ``` *p84* #### 以==特定物件==作為回傳值的建構器函式 會回傳另一個物件的建構器函式。 ```javascript! var puppet = { rules: false }; function Emperor() { this.rules = true; return puppet; } var emperor = new Emperor(); assert(emperor === puppet, "The emperor is merely a puppet!"); assert(emperor.rules === false, "The puppet does not know how to rule!"); // 在這個例子中,Emperor 建構器函式返回了外部定義的 puppet 物件 ``` ![image](https://hackmd.io/_uploads/HJZD-X5F0.png) 測試證明:被指派給變數 emperor 的物件,是建構器函式所回傳的物件,而非 new 表達式所建立的物件。 登登登!總結發現: 1. 如果建構器返回一個物件,那麼該物件將作為整個 new 表達式的值回傳,並且另一個被當成 this 傳遞給建構器的新物件將被丟棄。 2. 如果從建構器回傳的並不是一個物件,則回傳的值會被忽略,並且最終會回傳那個新建立的物件。 > 我自己的總結: new 一個建構器 -> 會產生一個新物件並回傳它本身 但!如果建構器裡面 return 一個物件,那新產生的這個物件,就會被忽略,而是回傳建構器裡面 return 的那一個。 *p85* #### 建構器的設計考量 建構器的目的:對呼叫函式所建立的新物件進行初始化,使其符合初始條件。 ==雖然這樣的函式可以一樣用以函式方式呼叫==,甚至被指派給物件屬性以便作為方法來呼叫,但這樣沒什麼意義🤣。 ```javascript! function Ninja() { this.skulk = function() { return this; } } var whatever = Ninja(); // 此時 skulk 屬性會建立在 window 物件上,如果在嚴格模式下,會變 undefined ``` *p86* 命名差異: - 函式和方法:通常以一個描述它們在做什麼的動詞開始,並以小寫字母開頭。 `EX: skulk, creep, sneak, doSomethingWonderful` - 建構器函式:通常以一個描述物件本身的名詞來命名,並以大寫字母開頭。 `EX: Ninja, Samurai, Emperor, Ronin` ### 4.2.4 使用 apply 和 call 方法來進行呼叫 先用一個事件處理的常見錯誤例子來看! *p87* ```javascript! <button id="test">Click Me!</button> <script> // 這個建構器用來保留按鈕的狀態資訊 function Button() { this.clicked = false; this.click = function() { this.clicked = true; assert(button.clicked, "The button has been clicked"); } } var button = new Button(); var elem = document.getElementById("test"); elem.addEventListener("click", button.click); // 用來為指定的 DOM 元素註冊事件監聽器的一種方法。這裡表示:當用戶點擊 elem 元素時,會觸發 "click" 這個事件類型、調用 button.click 方法。 </script> ``` *p88* 在這個例子裡,瀏覽器的事件處理系統會將函式呼叫的背景空間定義為事件的目標元素,這使得背景空間將是 button 元素,而非 button 物件。 >我的理解啦: >當 button.click 作為方法被呼叫時,this 的上下文將指向 elem 元素,而不是 button。 > >因此 this.clicked = true 實際上是設定 elem.clicked = clicked,而不是 button.clicked = clicked。 #### 使用 apply 和 call 方法 可以在呼叫函式時,==明確指定==任何我們想要作為函式背景空間的物件。讓我們利用所有函式都具備的兩種方法實踐這一點。 - 使用 apply 方法呼叫函式:需要傳遞兩個參數:要用來當做函式背景空間的物件、用來當作呼叫引數的陣列值。 `juggle.apply(ninja1,[1,2,3,4])` - 使用 call 方法呼叫函式:引數直接在引述列表中傳遞,而不是一個陣列。 `juggle.call(ninja2, 5,6,7,8)` ```javascript! // 宣告一個函式,內容是把所有引數值做加總,將結果儲存在函式的背景空間 function juggle() { var result = 0; for(var n = 0; n < arguments.length; n++) { result += arguments[n]; } this.result = result; } // 此兩個物件將作為:函式以 apply . call 呼叫時的背景空間 var ninja1 = {}; var ninja2 = {}; juggle.apply(ninja1, [1,2,3,4]); juggle.call(ninja2, 5,6,7,8); assert(ninja1.result === 10, "juggeld via apply"); assert(ninja2.result === 26, "juggeld via call"); ``` ![image](https://hackmd.io/_uploads/H1p6hZAK0.png) *p90* #### 強制指定回呼時的函式背景空間 強制將函式背景空間設為一個指定的物件,將使用一個簡單函式,對陣列的元素執行一項操作。 *91* 這邊介紹了命令式和函式式的設計: - 命令式(Imperative)的函式設計:通常會將陣列傳遞給一個方法,並使用 for 迴圈對每個元素執行操作。 > 命命式 > [name=Jeremy] > 好 謝謝 > [name=Jamixcs] ```javascript! function(collection) { for(var n = 0; n < collection.length; n++) { ... } } ``` - 函式型(Functional):建立一個==只處理一個元素==的函式,並將每個元素傳遞給該函式。 ```javascript! function(item) { ... } ``` 差別在於,函式在程式邏輯中的角色為何??? > 錯字喔 > [name=Jeremy] > 🥲 > [name=Jamixcs] ##### 有請 GPT 補充:假設你有一個數字陣列,並且你想要將每個數字加倍。 命令式風格: ```javascript! function doubleNumbers(numbers) { var doubled = []; for (var i = 0; i < numbers.length; i++) { doubled.push(numbers[i] * 2); // 對每個數字進行加倍操作 } return doubled; } var numbers = [1, 2, 3, 4]; var result = doubleNumbers(numbers); console.log(result); // 輸出: [2, 4, 6, 8] // 在這個例子中,明確地指定了如何遍歷陣列(使用 for 迴圈),以及如何處理每個元素(將它們加倍)。這種方法具體地控制了流程和操作。 ``` 函式式風格 ```javascript! function double(number) { return number * 2; } var numbers = [1, 2, 3, 4]; var doubled = numbers.map(double); // 使用 map 函式(一次只處理一個元素)將 double 應用於每個元素 console.log(doubled); // 輸出: [2, 4, 6, 8] ``` ##### GPT 另一個比喻 > 假設你要讓每個人加薪 10%。 > > 命令式風格:就像是你逐個檢查每個人的薪水,計算新的薪水,然後更新記錄。 > 函式式風格:你專注於計算薪水的邏輯,然後使用一個工具(比如表格公式)來自動地對所有人的薪水進行計算和更新。 > > 總結 > 命令式風格:詳細描述了如何做(遍歷、操作每個元素)。 > 函式式風格:描述了要做什麼(處理單一元素),然後使用工具來自動處理整個集合。 *p92* 所有陣列物件都可以用 forEach,比傳統的 for 敘述句來得更受歡迎,講閉包的時候會講。 #### 建立一個 forEach 函式,來示範函式背景空間的設定 ```javascript! function forEach(list, callback) { for(var n = 0; n < list.length; n++) { callback.call(list[n], n) // 呼叫回呼函式時,會用目前迭代中的元素作為函式背景空間 } } var weapons = [ { type: 'shuriken' }, { type: 'katana' }, { type: 'nunchucks' }]; forEach(weapons, function(index) { assert(this === weapons[index], "Got the expected value of " + weapons[index].type); }) // forEach 函式每次迭代會做:將目前的元素指定為目前元素的背景空間(例如 n = 0; 跑到 list[0] 就會以 list[0] 作為背景空間,因此我們我們用去驗證 this 是不是等於當下的物件) ``` *p93* 兩個要用哪一個? 如果我們有許多彼此不相關的變數,call 可以讓我們直接在引數列出他們,如果要已經有了一個欲操作的元素的陣列了,就用 apply🤓 ### 修復函式背景空間的問題 前面的例子讓我們見識到事件處理時,函式的背景空間可能會有問題,所以我們用 apply 和 call 來繞過,現在我們有更優雅的作法,箭頭函式和 bind🌹 *p94* #### 4.3.1 使用箭頭函式來繞過函式背景空間 箭頭函式有一個特性讓它很適合作為回呼函式:==沒有自己的 this 值,而是於定義時記住 this 參數值==。 >我自己翻白話文:箭頭函式從哪邊生出來,那邊就是它的背景空間。 ```javascript! <button id="test">Click Me!</button> <script> // 這個建構器用來保留按鈕的狀態資訊 function Button() { this.clicked = false; this.click = () => { ---------> 這邊用了箭頭函式 this.clicked = true; assert(button.clicked, "The button has been clicked"); } } var button = new Button(); var elem = document.getElementById("test"); elem.addEventListener("click", button.click); // button.click 執行箭頭函式,而箭頭函式的背景空間指向 Button 實例,即 button 變數,斷言會成立。 // button.click 被綁定到 elem 上,箭頭函式的 this 仍然指向 button 對象(如果是一般匿名函式就會綁到事件指向的元素上)。 ``` 在這個例子裡,瀏覽器的事件處理系統會將這個==箭頭函式呼叫的背景空間==定義為箭頭函式被宣告的那個物件,這使得背景空間將是 button 物件。 >我的理解啦: >當 button.click 作為方法被呼叫時,this 的上下文將指向 var button = new Button(); 這裡的 button 物件。 *p95* #### 箭頭函式和物件實值(object literal) 只有一個按鈕,不要用建構器函式的方式建立,那可能會想用一個簡單的物件實值取代它: *p96* ```javascript! <button id="test">Click Me!</button> <script> assert(this === window, "this === window"); var button = { clicked: false, click: () => { //直接賦予物件屬性一個箭頭函式 this.clicked = true; } var elem = document.getElementById("test"); elem.addEventListener("click", button.click); </script> ``` click 箭頭函式會是建立於物件實值上的一個屬性,這邊==物件實值是建立在全域 window 上==,所以 this 的值將會是全域程式的 this 值。 *p97* #### 4.3.2 使用 bind 方法 每個函式可以用 bind 這個方法建立出一個新的函式,這個函式具有相同的主體,但它的背景空間總是會綁定到指定的物件上,無論我們用什麼方式呼叫它。 *p98* ```javascript! <button id="test">Click Me!</button> <script> var button = { clicked: false, click: function() { this.clicked = true; assert(button.click, "The button has been clicked;") } }; var elem = document.getElementById("test"); elem.addEventListener("click", button.click.bind(button)); ------> 直接在呼叫的時候 bind(要指向的物件) var boundFunction = button.click.bind(button); assert(boundFunction != button.click, "Calling bind creates a completely new function"); // 後面這邊證明了是建立一個新函式,而非修改原本的 </script> ``` 每個函式都可以使用 bind 方法,他會建立並回傳一個==新函式,並綁定了指定物件,而 this 參數值會始終設定為該物件,不論綁定的函式呼叫方式為何。 #### 習題 1. 以下函式使用 arguments 物件計算傳入引數的總和: ```javascript! function sum() { var sum = 0; for(var i = 0; i < arguments.length; i++) { sum += arguments[i]; } return sum; } assert(sum(1,2,3) === 6, 'Sum of first three numbers is 6') assert(sum(1,2,3,4) === 10, 'Sum of first four numbers is 6') ``` 請用第三章介紹的不定參數重寫。 ```javascript! function sum(...numbers) { var sum = 0; for(var i = 0; i < numbers.length; i++) { sum += numbers[i]; } return sum; } ``` 2. 執行以下程式碼後,變數 ninja 和 samurai 的值分別會是什麼? ```javascript! function getSamurai(samurai) { "use strict" arguments[0] = "Ishida"; return samurai; } function getNinja(ninja) { arguments[0] = "Fuma"; return ninja; } var samurai = getSamurai("Toyotomi"); var ninja = getNunja("Yoshi"); ``` >samurai 會是 Toyotomi(因為使用嚴格模式) >ninja 會是 Fuma 3. 當執行以下程式碼時,哪項檢查會通過? ```javascript! function whoAmI1() { "use strict"; return this; } function whoAmI2() { return this; }; assert(whoAmI1() === window, "Window?"); assert(whoAmI2() === window, "Window?"); ``` > whoAmI2(因為在普通模式下,this 的值都會是 window) > whoAmI1 會變成 undefined(因為嚴格模式) 4. 當執行以下程式碼時,哪項檢查會通過? ```javascript! var ninja1 = { whoAmI: function() { return this; } } var ninja2 = { whoAmI: ninja1.whoAmI } var identify = ninja2.whoAmI; assert(ninja1.whoAmI() === ninja1, "ninja1?"); assert(ninja2.whoAmI() === ninja1, "ninja1 again?"); assert(identify() === ninja1, "ninja1 again?"); assert(ninja1.whoAmI.call(ninja2) === ninja2, "ninja2 here?") ``` > 通過。whoAmI() 被當成 ninja1 的方法來呼叫,背景空間為 ninja1 > 不通過。ninja2,whoAmI() 被當成 ninja2 的方法來呼叫,背景空間為 ninja2 > 不通過。whoAmI() 在普通模式下呼叫,背景空間為 window > 通過。call()的參數直接可以指定背景空間,背景空間為 ninja2 5. 當執行以下程式碼時,哪項檢查會通過? ```javascript! function Ninja() { this.whoAmI = () => {}; } var ninja1 = new Ninja(); var ninja2 = { whoAmI: ninja1.whoAmI }; assert(ninja1.whoAmI() === ninja1, "ninja1 here?") assert(ninja2.whoAmI() === ninja2, "ninja2 here?") ``` > 通過。whoAmI() 這個箭頭函式會在被創造時創建背景空間,因此背景空間是 Ninja 這個建構器函式,而 new 一個建構器,所以 this 被指向 ninja1,而由於是箭頭函式,所以會永遠指向 ninja1 > 不通過。 6. 當執行以下程式碼時,哪項檢查會通過? ```javascript! function Ninja() { this.whoAmI = function() { return this; }.bind(this); } var ninja1 = new Ninja(); var ninja2 = { whoAmI: ninja1.whoAmI } ``` >通過。 >ninja1 為 Ninja 建構器函式指向的物件,其中的背景空間被 bind 在該物件底下,因此每次調用 ninja1.whoAmI 時,this 都會指向 ninja1。 > >不通過。 >ninja2 做的事:創建了一個新物件 ninja2。將 ninja2 的 whoAmI 方法設置為 ninja1.whoAmI。 >但!當下的 this 已經被 bind 在 ninja1,就會永遠指向 ninja1。 >`console.log(ninja1.whoAmI === ninja2.whoAmI`) // true # Ch5 ### 5.1 瞭解閉包 定義:讓函式能夠存取及操作在函式外部的變數,函式將可以存取函式在定義時於作用域範圍內的所有變數和其他函式。 ==我的理解:閉包是 JS 在宣告函式時,函式可以記住在宣告時的作用域,也就是它可以記住在此時可使用的變數等等,即便作用域是一個函式(在這個函式從 heap 清除之後,還是可以用它內部宣告的變數)== #### 我的理解範例 ```javascript= function outerFunction(initialValue) { let count = initialValue; // 這是 outerFunction 作用域內宣告的變數,照理來講在 outerFunction() 執行完就要清除的變數 function innerFunction() { count += 1; // 但是 JS 讓 innerFunction 可以記住宣告時作用域中的變數 return count; } return innerFunction; } const counter = outerFunction(5); // 因此 outerFunction() return innerFunction 時依舊可以記住 count 變數,所以在繼續被執行的話是可以記住之前的值的! counter() // 6 counter() // 7 counter() // 8 counter() // 9 ``` ```javascript! var outerValue = "ninja"; function outerFunction() { assert(outerValue === "ninja", "I can see the ninja."); } outerFunction(); // 這邊的例子是表示 outerValue 和 outerFunction 都是在全域範圍內宣告的,而且這個範圍(一個閉包)永遠不會消失(只要我們的程式還在運行) ``` ```javascript! var outerValue = "samurai"; var later; function outerFunction() { var innerValue = "ninja"; // 可見範圍侷限在函式內,函式以外的地方無法存取它 function innerFunction() { // 可見範圍侷限在函式內,函式以外的地方無法存取它,對 innerFunction 來說 innerValue 是可見的 assert(outerValue === "samurai", "I can see the samurai."); assert(innerValue === "ninja", "I can see the ninja."); later = innerFunction; // 把一個指向 innerFunction 的參照存在 later 變數中,他是個全域變數,所以可以在 outerFunction 之外以 later 呼叫他。 } } outerFunction(); // 呼叫 outerFunction,這時會建立 innerFunction() 並把指向他的參照指派給 later 變數 later(); // 藉由 later 來呼叫 innerFunction 由於 innerFunction (和 innerValue) 可見範圍僅在 outerFunction 函式內 所以我們無法直接呼叫他,但用 later 就可以,因為later 是全域變數。 ``` 分析: 1. outerValue 是在全域範圍,因此對所有東西來說他是可見的 2. 把函式參照複製到 全域變數 later 上,在執行 outerFunction 之後執行 innerFunction 3. 執行 innerFunction 時,outerFunction 的作用範圍已經消失 >> 就是說 innerFunction 是在全域執行的 早就已經脫離 outerFunction 這個執行環境了 4. 預期應該失敗 看不到 ninja,因為 innerValue 應該是 undefined 但是結果卻是 true。 所屬的作用域範圍早已經消失了,為什麼還是活的?因為閉包。 當我們在 outerFunction 裏宣告 innerFunction 時,不僅定義了一個函式宣告,同時也“建立了一個閉包”,其內==包含函式定義,以及在建立函式時存在於作用範圍內的所有參數==。 這就是所謂的閉包。在函式定義時為函式及所屬範圍內的變數建立了一個『安全氣泡』,讓函式具有執行時所需要的所有東西。這個氣泡,含有函式和它的變數,並且擁有與函式相同的存活時間。 要記住每一個經由閉包來存取資訊的函式都有一個『球與鏈』附加到它上頭,以攜帶這些資訊。因此,儘管閉包非常有用,但他們並不是沒開銷。所有資訊都需要保存在記憶體中,直到 JS 引擎完全確定不再需要它了,或直到畫面卸載。 ### 5.2 開始使用閉包 ### 摹擬私有變數 JS 本身不支援私有變數,但是可以透過閉包做出類似的功能。 ```javascript! function Ninja() { var feints = 0; this.getFeints = function() { return feints; } this.feint = function() { fients++; } } var ninja1 = new Ninja(); ninja1.fients(); assert(ninja1.fients === undefined, "And the private data is inaccessible to us."); assert(ninja1.getFeints() === 1, "We' re able to access the internal feint count."); var ninja2 = new Ninja(); assert(ninja2.getFeints() === 0, "The second ninja object gets its own feints variable."); // 他這邊是要說用ninja1. 是拿不到裡面的變數fients的,只能透過建構器函式.方法去拿到裡變的變數。 ``` 我自己理解的小範例 ```javascript! function Ninja() { var feints = 0; this.getFeints = function() { return feints; } this.feint = function() { return ++feints; } } const ninja1 = new Ninja ninja1.getFeints() // 0 ninja.feint() // 1 ninja1.getFeints() // 1 ``` ### 伴隨回呼來使用閉包 ```javascript! <div id="box1">First Box</div> <script> function animateIt(elementId) { var elem = document.getElementById(elementId); var tick = 0; var timer = setInterval(function() { if(tick < 100) { elem.style.left = elem.style.top = tick + "px"; tick ++; } else { clearInterval(timer); assert(tick === 100,"Tick accessed via a closure."); assert(elem, "Element also accessed via a closure.") assert(timer, "Timer reference also obtained via a closure.") } }) } ``` 如果將三個變數移動到全域,也是可以用,但是在不同動畫中就會有互相影響的狀況。 目前這樣是可以在呼叫函式的時候,每個呼叫都可以形成一個自己的作用域不會互相影響?? ### 5.3 使用執行背景空間追蹤程式執行 JS 程式有兩種主要類型:全域程式(被包含在程式外)、函式程式(被包含在函式內) 執行背景空間,是可以用來追蹤函式的執行,也有兩種:全域執行背景空間、函式執行背景空間。 PS:函式背景空間可以用 this 存取,和這邊講的執行背景空間不一樣喔。 這邊提到了呼叫堆疊(執行背景空間堆疊)call stack JS 是單執行緒的執行模型,也就是一次只能執行一段程式碼,每當一個函式被呼叫時,都必須停止當前的執行背景空間,然後再成立一個新的函式執行背景空間 函式執行完任務後,他的執行背景空間就會被丟棄,恢復到原本呼叫者的執行背景空間,因此所有的執行背景空間都需要進行追蹤,因此書中說最簡單的方法就是使用一個堆疊,被稱為執行背景空間堆疊、呼堆堆疊(call stack)。 堆疊就是一種資料結構,像放紙張一樣。 建立執行背景空間 ```javascript! function skulk(ninja) { report(ninja + "skulking"); } function report(message) { console.log(message) } skulk("kuma"); skulk("Yoshi") ``` ![IMG_0478 (1)](https://hackmd.io/_uploads/S1eVoo8Nye.jpg) 除了追蹤應用程式的執行位置,執行背景空間在解析識別項也非常重要,解析識別項是找出識別項所參照之變數的過程,執行背景空間是透過字彙環境來解析識別項。 ==公尛== ### 使用字彙環境來追蹤識別項 字彙環境 -> 作用域、scope 每一次程式碼被執行時,每一個程式碼的結構都獲得相關的字彙環境。 強調:一個在內部的程式碼結構可以存取外部程式碼結構中所定義的變數。 每個字彙環境必須追蹤它的外部字彙環境(因為有時候會需要存外部作用域的變數),JS 就利用了『函式作為頭等物件』的方法達成這一點。 每當函式被呼叫時,都會建立一個新的函式背景空間,並將其推送到 call stack 中,還會建立一個相關連的字彙環境 scope,現在則是關鍵部分:JS 引擎會將被呼叫函式內部 [[Enviroment]] 屬性,參照到新字彙環境的外部環境,也就是目前被呼叫的函式在先前建立時所處的環境。 ```javascript! var ninja = "Muneyoshi" function skulk() { var action = "Skulking" function report() { var intro = "Aha!" assert(intro === "Aha!", "Local"); assert(action === "Skulking!", "Outer"); assert(ninja === "Muneyoshi!", "Global"); } report() } skulk() ``` 當 `assert(intro === "Aha!", "Local")` 執行時,必須解析 intro 這個識別項,JS 會先檢查目前運行的 report 執行背景空間,因為這個執行背景空間有 intro 這個識別項的參照,因此就會被解析出來。 `assert(action === "Skulking!", "Outer")` 必須解析 action 識別項,所以會檢查當前運行的執行背景空間,發現到 report 環境不包含對 action 識別項的參照,所以會去檢查 report 環境外部的環境,skulk 環境,所以就找到了,當嘗試解析 ninja 識別項的時候也是一樣。 ### 5.5 瞭解 JS 的變數類型 const 只能在一開始的時候設置值。 可以修改現有的值,但不能完全覆寫他。 ### 5.5.2 可以根據他們與字彙環境的關係,來分類,可分為 var/ let.const ## 讀書會 閉包就是為了解決 call stack 消失後,一個 function 要拿到另外一個 function 的 scope 中的變數 有兩派: 只要宣告 function 就是一個閉包環境 要寫一些機制才是閉包 阿傑:沒有私有變數,所以要有一個閉包去處理這個現象,區隔開全域變數,是一個工具。 識別項:例如這個東西不是字串、不是函式,他就要去找是什麼? 對應識別項:賦值 (已編輯) 像是 add 他不是字串也不是數字,console.log 的時候會去找他是啥,他就是識別項,那它是啥,是一個 function 印出來就是一個 function ![image](https://hackmd.io/_uploads/Hy9RaWdE1l.png) 函式宣告的環境(字彙環境)很重要! 閉包範例 ```javascript! let outerCount = 0; function createCounter() { let count = 0 function increment() { count++; outerCount++; console.log("innerCount", count); console.log("outerCount", outerCount); } return increment; } const counterA = createCounter(); const counterB = createCounter(); ``` # Ch7 原型 就是在尋找一個屬性時,可以被委派這項任務的物件。在定義一些自動成為可讓其他物件存取的屬性和功能時,使用原型是一中相當方便的做法。 ## 7.1 已經指派給物件的屬性,可以輕易地被修改或刪除。 ```javascript! let obj = { prop1: 1, prop2: function(){}, prop3: {} } ``` 可以這樣修改 ```javascript! obj.prop1 = 1 obj.prop1 = [] delete obj.prop2 obj.prop4 = "Hello" ``` 這個物件會變成: ![image](https://hackmd.io/_uploads/HJmoY3JSyx.png) 一種可以重用程式碼,並且幫助我們把程式組織好的方式就是繼承,他把存在於某個物件上的功能延伸到其他物件上。繼承是用原型實作出來的。 原型的基本概念:每個物件都可以有一個對自身原型的參照,就是在尋找某個特定屬性,==而物件本身沒有這個屬性時,可以被委派這項搜尋工作的另一個物件==。 ```javascript! const yoshi = { skulk: true } const hattori = { sneak: true } const kuma = { creep: true } ``` 原本三者都沒有什麼原型繼承關係,彼此也不能存取彼此的屬性。 當! ```javascript! Object.setPrototypeOf(yoshi, hattori) 等同於 yoshi.__proto__ = hattori ``` 以上為將 hattori 設置為 yoshi 的原型,因此 yoshi 可以存取 hattori 的屬性 ![image](https://hackmd.io/_uploads/ry3e7pJryx.png) 用 `"sneak" in yoshi` 可以檢測出這個屬性是否在 yoshi 裏 ![image](https://hackmd.io/_uploads/SJVK_pJSkg.png) 當存取物件本身沒有的屬性時,會在其物件原型搜尋該屬性。 因此可以透過 yoshi 取得 hattori 的 sneak 屬性,因為 hatoori 是 yoshi 的原型 每個物件可以有一個物件原型,原型也可以有自己的原型,依此類推,就能得到所謂的原型串鏈。 p.194 物件原型是物件的內部屬性,而且無法直接存取,也就是 [[ prototype ]],但在瀏覽器上可以用 `.__proto__` 存取 ![image](https://hackmd.io/_uploads/HJwg-rZryl.png) ## 7.2 有物件導向背景的人,可能會懷念類別建構器(Class)所帶來的物件封裝和良好的結構性。 Class 是一個用來對物件進行初始化的函式,讓它具有某種已知的初始狀態。 new 運算子是用在建構器函式上,會觸發一塊記憶體空間被撥出來,用於存放新物件。 每個函式其實都有一個原型物件,它會自動被設置為透過該函式所建立物件的原型 ### 建立一個具備原型方法的新物件實例 ```javascript! function Ninja() {} Ninja.prototype.swingSword = function() { return true } // 每個函式都有一個內建的原型物件,我們可以任意修改它 const ninja1 = Ninja(); console.assert(ninja === undefined, "No instance of Ninja created.") const ninja2 = new Ninja(); console.assert(ninja && ninja2.swingSword, ninja2.swingSword(), "Instance exists and method is callable") ``` 這邊用兩種方式呼叫,並將回傳結果存起來。 ninja1 是用一般函式方式呼叫 而 ninja2 是用 new 建構出來的,有一個新的物件被建立、此物件被設定為該函式的背景空間(可以用 this 關鍵字來存取它),這個 ninja2 也取得了 Ninja() 的函式原型的屬性以及方法 !而所有從 Ninja() new 出來的物件 Chris: 通常是我要知道 ninja2 傳來的物件是什麼型別的時候會用到! `ninja2.constructor.name` ![image](https://hackmd.io/_uploads/Hk10EBbrJl.png) ![11111111 (1)](https://hackmd.io/_uploads/S1Ck50JBke.jpg) 1. 每個函式都會有一個原型物件(這邊就叫做 Ninja prototype) 2. 這個 Ninja prototype 有一個 constructor 屬性會參照原來的函式 3. ==建構器物件的原型,會被設為新建立物件的原型(就是 Ninja prototype)== 所以用 constructor 時,這個 constructor 函式的原型,就會是新 new 出來的這個物件的原型 ![image](https://hackmd.io/_uploads/Hkao2AJS1x.png) ## 7.2.1 實例屬性 觀察初始化動作的優先順序 ```javascript! function Ninja() { this.swung = false; this.swingSword = function() { return !this.swung } } // 實例的方法 Ninja.prototype.swingSword = function() { return this.swung } // 創建一個同名的原型方法 const ninja = new Ninja(); console.assert(ninja.swingSword(), "Call the instance method, not the prototype method") ``` 以上是在講看是實例的方法還是同名的原型方法會被優先執行,結果回傳了 true 因此是「實例的方法」會先優先被執行。 實例會遮蔽定義在原型上的同名屬性。 我們用 constructor 時,會 new 出一個新物件,所以當我們在物件實例上要執行 swingSword() 時,會立即找到在 constructor 建立時建立的屬性,因此不用到原型鏈上面去找。 ### 有趣的副作用 當今天我們建立了三個實例,每個實例都透過 constructor 建立了各自版本的屬性,但他們的原型屬性都指向同一個原型物件。 如果是一般資料還 ok,但如果是方法,可能就會有問題!如果非常大量的物件,就會浪費記憶體。所以這時候讓物件方法只在原型上出現,或許比較合理。 Chris:重點是不要寫重複的 code,記憶體不是重點XD 如何確認是不是真的私有變數?就是那個變數在不在 this 身上。 如下 ![image](https://hackmd.io/_uploads/SyIcoSbHye.png) ## 7.2.2 JS 因其動態特性所產生的副作用 竄改掉原型,就會創造出新的原型 如果要改就是要用 setPrototypeOf() 來自橘子的實驗 ```javascript! function Ninja2(){} const ninja2 = new Ninja2(); ninja2 instanceof Ninja2 // true ninja2.constructor === Ninja2 // true // 把constructor換掉,換成Samurai function Samurai(){} ninja2.constructor = function Samurai() {} // 這個應該會是這樣 ninja2.constructor = Samurai ninja2 instanceof Ninja2 // 用instanceof還是可以知道原本是從哪裡建構來的 // true ninja2.constructor === Ninja2 // 但.constructor就不行 // false // 用ninja2的constructor建構一個實體samurai1 // 結果samurai1既不是從Ninja2建構而來 // 也不是從Samurai const samurai1 = new ninja2.constructor() samurai1 instanceof Ninja2 // false samurai1.constructor === Ninja2 // false samurai1 instanceof Samurai // false samurai1.constructor === Samurai // false // 超怪XD // 只知道他是從ninja2的constructor建構 samurai1 instanceof ninja2.constructor // true ``` ### 小補充 一個建構器函式的原型鏈應該是長這樣子 ![image](https://hackmd.io/_uploads/SJe0e1EByg.png) ```javascript! ninja -> [[ prototype ]] Ninja() -> [[ prototype ]] Object() ``` ## 7.3 實現物件繼承 ==p207== 繼承可以使程式碼重用。 例如:讓新物件皆去繼承舊有物件的屬性,就不需要在創造新物件的時候重複同樣的程式和資料。 ### 程式列表 7.7 嘗試用原型來實現繼承 instanceof:檢查右側函式的原型是否出現在左側物件的原型鏈上 ```javascript! function Person() {} Person.prototype.dance = function(){} function Ninja(){} Ninja.prototype = { dance: Person.prototype.dance } // 將 Person 原型的方法複製到 Ninja 上 const ninja = new Ninja(); ninja instanceof Ninja // true ninja instanceof Person // false ninja instanceof Object // true ``` ninja 這個實例的原型鏈上沒有 Person:![image](https://hackmd.io/_uploads/B1wDGJNBkg.png) 沒有辦法用這種方式去將 Ninja 繼承 Person 的原型,==這不是繼承,這是複製。== (原型鏈就像是:忍者可以是人,人可以是哺乳類,哺乳類可以是動物...) ==p208== 建立這個原型鏈的最佳方式就是使用物件實例,以它來作為其他物件的原型。 ```javascript! SubClass.prototype = new SuperClass(); // 例如: Ninja.prototype = new Person() ``` 就有了! ![image](https://hackmd.io/_uploads/r1r2RJES1g.png) ==new Person() 創造出來的實例,就會是 Ninja 的 protorype 因為這樣是讓 Ninja 的原型直接是 Person 的實例,因此就會擁有 Person 這個原型在他自己的原型身上== 但這時候 ninja 的原型鏈上就沒有 Ninja() 這個原型了!(看下圖 ninja 如果是由 Ninja new 出來的話 ninja 會先指向 new Ninja,然後他再指向 prototype) 我的理解是:==因為 new 出來的時候這個實例的原型會是參照 new 了誰(Person),所以這個 ninja 的原型是 Person 這個物件然後他有一個 constructor 的方法,因此與 Ninja() 會斷開魂結。== ![IMG_0718 (1)] (https://hackmd.io/_uploads/HJQA9k4ryg.jpg) 是改了這條 ![image (1)](https://hackmd.io/_uploads/HykA3_9HJx.png) ```javascript! function Person() {} 定義 Person 這個 constructor 函式時 一個藉由 constructor 屬性參照到 Person 函式的原型也會被建立出來! 所以也等同於有了一個 Person.protorype 這個原型,而在 new Person 的時候,新的實例就會指向 Person.protorype 這個原型 ``` <!-- 而 Ninja 有一個指向自己的 constructor 原型物件,因此我們可以這樣得到它: ![image](https://hackmd.io/_uploads/HyV9pJErke.png) --> ==p210== 再回到剛剛做的: ```javascript! function Ninja(){} Ninja.prototype = new Person() const ninja = new Ninja() ``` <!-- ![image](https://hackmd.io/_uploads/BktnRy4rkx.png) --> 所以如果我要 ninja 去存取原型鏈上的屬性 dance ![image](https://hackmd.io/_uploads/S1Jb1gEB1e.png) 會由自身開始找,再找到 Person 再找到 Person 的原型,就找到了。 <!-- 注意:`Ninja.prototype = Person.prototype` 兩者會是同一個物件,因此會被改到喔,如下: ![image](https://hackmd.io/_uploads/Hkeheg4r1e.png) --> ## 7.3.1 覆寫 constructor 屬性所造成的問題 .constructor 可以得知實例是由哪個 constructor 建構出來的! 我們以剛剛這個例子來看 ninja.constructor ![image](https://hackmd.io/_uploads/HJehKlEH1g.png) 會發現這不是我們要的 QQQQ ninja 是由 Ninja() new 出來的啦! 作者說這個 bug 要我們自己出來解決! ==p211== 但他說要先看一下 JS 怎麼設定屬性?! JS 物件的每個屬性都有一個屬性描述子,可以透過他們來設定以下鍵值。 - configurable:true 可以被修改、屬性可以被刪除,false 兩個動作都不能執行 - enumerable:true 屬性可以在物件的 for-in 迴圈操作裡出現 - value:設定屬性的值。 - writable:true 可以透過設值敘述來修改屬性值。 - get:定義取值(getter)函式,可以用來取得屬性值。不能和 value,writable 一起設定。 - set:定義取值(setter)函式,可以用來對屬性指派一個值,同樣的不能和 value 和 writable 一起設定。 小例子! ```javascript! ninja.name = "Yoshi" ``` 這個屬性會是可設定 configurable、可列舉 enumerable、可寫入 writable 的,而 get . set 函式會是 undefined。 ==p212== > MDN:[Object.defineProperty() ](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)靜態方法 Object.defineProperty() 會直接對一個物件定義、或是修改現有的屬性。執行後會回傳定義完的物件。 > 語法:Object.defineProperty(obj, prop, descriptor) > obj 要定義屬性的物件。 > prop 要被定義或修改的屬性名字。 > descriptor 要定義或修改物件敘述內容。 ### 7.9 程式列表屬性 ```javascript! const ninja = {} ninja.name = "Yoshi" ninja.weapon = "kusarigama" Object.defineProperty(ninja, "sneaky" { configurable : false, // 不可設定 enumerable: false, // 不可列舉 value: true, writable: true }) for(let prop in ninja) { assert(prop !== undefined, "An enumerated property" + prop) } ``` 將 ninja 設置一個屬性叫 sneaky,他不可設定、不可列舉、值是 true,可寫入(可修改值) 然後我們用 for in 迴圈去看 sneaky 應該不會出現在裡面!因為他是 enumerable: false 不可列舉的。 ### 解決覆寫 constructor 屬性所造成的問題 這邊要解決的事 ```javascript! Ninja.prototype = new Person() ``` 進而發生 `ninja.constructor` 會是 Person() 的問題 ==213== Chris:defineProperty 超級重要 寫一個 class 叫 property 要 return 值出去的時候是否需要怎麼樣?key 是什麼,要不要寫在裡面?要不要被 set? 我們可以這樣做: ```javascript= function Person() {} Person.prototype.dance = function(){} function Ninja(){} Ninja.prototype = new Person(); // 設定 constructor 的 屬性!!!!!!! Object.defineProperty(Ninja.prototype, "constructor", { enumerable: false, value: Ninja, writable: true }) const ninja = new Ninja() ninja.constructor // ƒ Ninja(){} ``` ![image](https://hackmd.io/_uploads/B1zk9Z4Syl.png) ==p214====p215====p216== ### 7.3.2 instanceof 運算子 運作在物件的原型鏈上,例如: ```javascript! ninja instanceof Ninja // 實例 instanceof 建構器函式 ``` 會檢查 Ninja 函式目前的原型,是否出現在 ninja 物件實例的原型串鏈上(對右側函式的原型做檢查,看看他是否出現在運算子左測物件的原型鏈上) ==p217== ## 7.4 在 ES6 裡使用 JS 的類別 JS 天生並不支援傳統的物件繼承。 ES6 出現了 class 模擬物件導向,是模擬喔!底層依然是以原型的方式實現繼承。 ### 7.4.1 使用 class 關鍵字 class 可讓我們手動地、更優雅地建立物件及實現繼承。 ```javascript! class Ninja{ constructor(name) { this.name = name; } // 定義一個建構器函式,當我們用關鍵字 new 來呼叫 class 的時候,就是建立新的實例的時候,他會被執行 swingSword(){ return true; } // 定義一個額外的方法,所有 Ninja 物件實例都可以存取他 } var ninja = new Ninja("Yoshi") // new 一個新的 ninja 物件 ``` ==p218== class 中:我們可以定義一個 constructor,他會在建立新的實例的時候被呼叫,在這個 constructor 中,我們可以用 this 存取新的物件實例(this 就是實例本身!==在 constructor 中,this 代表新建立的物件實例==)、也可以增加新屬性。 ninja 這個實例可以存取 class 中定義的屬性、方法,並且 Ninja 這個 class 的原型會在 ninja 的原型鏈上 ![image](https://hackmd.io/_uploads/rk1QBHrrkg.png) ==p219== ### class 是語法糖 >//< 就是讓我們在模擬原型繼承的時候可以方便一點的語法。 剛剛那段在 ES5 是可以這樣寫的,一模一樣的東西 ```javascript! function Ninja(name) { this.name = name } Ninja.prototype.swingSword = function() { return true } ``` 以上是:將 Ninja 的原型添加了一個 swingSword 的方法,讓實例都可以在原型鏈上用這個方法! ### 靜態方法 這邊在講人家 Java 也有的靜態方法(定義在 Class 層級的方法),ES6 可以怎麼模擬: ```javascript! class Ninja{ constructor(name, level) { this.name = name; this.level = level; } swingSword() { return true } static compare(ninja1, ninja2){ return ninja1.level - ninja2.level } // 使用關鍵字 static 來建立靜態方法(在方法名稱之前加上 static) } var ninja1 = new Ninja("Yoshi", 4) var ninja2 = new Ninja("Hattori", 3) ``` ==p220== compare 的靜態方法是==定義在類別的層級上==(而不是在實例的層級上)用途是來比較兩個 ninja 實例誰的技術等級比較高,這表示 compare 方法不能透過 ninja 實例來存取,==只能夠過 Ninja 類別==。 ![image](https://hackmd.io/_uploads/BkXsuPBBke.png) 用 Ninja 類別存取。 ![image](https://hackmd.io/_uploads/SyS9OvHS1g.png) ==p221== ### 7.4.2 實現繼承! 作者說要在 ES6 前實現繼承很痛苦,如果要讓實例都能存取,必須把方法定義在建構器函式的原型上。 提到了前面講的破壞原型鏈,==將想要繼承的 Person 物件的實例設置為 Ninja 物件的原型,又會讓 instanceof Ninja 是 false、constructor 指向錯誤,因此又要用 Object.defineProperty 修復它== ### 在 ES6 裡的繼承 ```javascript! class Person { constructor(name){ this.name = name } dance(){ return true } } class Ninja extends Person { // 使用關鍵字 extends 來繼承其他類別 constructor(name, weapon){ super(name); // 使用關鍵字 super 來呼叫基礎類別的建構器???? this.weapon = weapon } wieldWeapon(){ return true } } var person = new Person("Bob") ``` ==p222== Ninja 繼承的是 Person 這個類別,他可以用其中的 constructor 他可以存取 name、有 dance 方法可以用,他也可以用自己的。 可以看一下~! ![image](https://hackmd.io/_uploads/ryUJIw5Hyx.png =600x) ==class Ninja 的原型是 Person== ==p223== 而!在這邊用 super 可以呼叫基礎類別的 constructor 存取到 name????? 而這邊可以做相關檢查: ![image](https://hackmd.io/_uploads/B1NkiFrByg.png) ![image](https://hackmd.io/_uploads/BkRHsKHBkl.png) ninja 的原型鏈上有 Person 所以可以存取 Person 中的變數和方法,但 Person 反之就不能。 #### ok 下個結語 所以終於真的知道為什麼大家會一直討論 :啊 JS 有物件導向嗎? :他其實不是真正的物件導向。。。 :他就不是!!!!! >現在我可以回答,對,JS 不是,只是用 class,就能以原型的底層邏輯去模擬、實現物件導向的概念。 (但以前不是 Java、C# 背景的菜雞,不會懂這種歸屬感,但至少比以前懂了) 所以也可知道原型這個概念蠻重要的,雖然不知道實作可以用在哪裡,不過至少知道這是一種 JS 的身份認同? ### Chris:物件導向三元素: >- 繼承性(Inheritance) >- 具備封裝性(Encapsulation) >- 動態連結(Polymorphism,最主要要做的事) JS 沒有 private 所以不是!有 #private https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_properties 有什麼 ES6 不支援的套件,就可以想到他: (右邊可以看 under node 4 時 class 做了什麼) https://babeljs.io/repl#?browsers=defaults%2C%20not%20ie%2011%2C%20not%20ie_mob%2011&build=&builtIns=false&corejs=3.21&spec=false&loose=false&code_lz=MYGwhgzhAECiDeAoAkAIwBQEpr2sA9gHYT4gCmAdCPgOboAuAFgJYSYC-i7QA&debug=false&forceAllTransforms=false&modules=false&shippedProposals=false&evaluate=false&fileSize=false&timeTravel=false&sourceType=module&lineWrap=true&presets=env%2Creact%2Cstage-2&prettier=true&targets=Node-4&version=7.26.4&externalPlugins=&assumptions=%7B%7D > The Car -> class 抽象 > A Car -> object 實例 Chris:怪做法改來改去都不要學 ### 型別判斷 SOP 1. typeof 是不是基礎型別?是物件再用其他招 2. constructor.name 3. instanceof ```javascript! document 是一個物件 document.constructor.name // 'HTMLDocument' document instanceof HTMLDocument // true ``` ```javascript! typeof null // 'object' null.constructor // VM518:1 Uncaught TypeError: Cannot read properties of null (reading 'constructor') at <anonymous>:1:6 ``` Chris:用套件的時候,就需要判斷型別,文件寫下什麼給你一包東西,例如:你可以 `啥.什麼什麼`,就可以怎樣喔!這時你就要知道那個`啥`是什麼型別。 ### == / === 用法 `=== undefined || === null 等同於 == null` 要找 undefined、null 才用 ==,其他時候都用 === null 是被賦予的初值,undefined 是垃圾值,=== undefined 很可怕 = undefined 非必要不會寫,需要就要寫註解。 ### 語法觀念 mindset 為什麼要寫物件? >「抽象」-> 搜集特徵放在一起,概念凝聚、重複使用 為什麼要寫 class? > - 為了要得到別人的 funciton,但我不要重寫一次,我要繼承它 > - 大量產出 object(JS 閉包也做得到) > > 我是一個 class,他是一個 class,我繼承他(實現動態連結) object 是我要得到那個屬性(我要擁有他 has),class 是行為(我要成為他 is) > 我只要他的行為就好?我只要那個 funciton name?就寫 class > 繼承可以這樣想: > 我有`金萱`、`金萱+QQ`,我寫的時候要把`金萱+QQ`繼承`金萱`,還是兩杯都繼承`飲料` ### ### 習題 #### 4 第四題第二種就是節省記憶體的寫法?? ```javascript! function Person(firstName, lastName){ this.firstName = firstName; this.lastName = lastName; this.getFullName = function (){ return this.firstName + " " + this.lastName; } } function Person(firstName, lastName){ this.firstName = firstName; this.lastName = lastName; } Person.prototype.getFullName = function (){ return this.firstName + " " + this.lastName; } ``` #### 6 ```javascript! function Person() {} function Ninja () {} Ninja.prototype = new Person(); const ninja = new Ninja; ``` 這個要注意 Ninja 的原型是 Person 的實例 所以原型是 Person 的原型 ninja.constructor 時會先去找 Person 的原型,他這個原型會有 constructor 屬性指向 `function Person() {}` 這個函式。 ## 8 Proxy ```javascript! const user = { _name: "小明", _age: 25, get name() { console.log(`讀取屬性 name: ${this._name}`); return this._name; }, set name(value) { console.log(`設定屬性 name 為 ${value}`); this._name = value; }, get age() { console.log(`讀取屬性 age: ${this._age}`); return this._age; }, set age(value) { console.log(`設定屬性 age 為 ${value}`); this._age = value; }, }; console.log(user.name); // 讀取屬性 name: 小明 user.age = 30; // 設定屬性 age 為 30 ``` ```javascript! const user = { name: "小明", age: 25, }; const proxyUser = new Proxy(user, { get(target, prop) { console.log(`讀取屬性 ${prop}: ${target[prop]}`); return target[prop]; }, set(target, prop, value) { console.log(`設定屬性 ${prop} 為 ${value}`); target[prop] = value; return true; }, }); console.log(proxyUser.name); // 讀取屬性 name: 小明 proxyUser.age = 30; // 設定屬性 age 為 30 ``` # 8 嚴格模式 的 set 可以拿來做 常數不想被改的時候,報錯的敘述 get set 想法就是取資料的時候如果不是我們要的,可以包一個去轉態,或轉為千分位數轉為字串等等行為,再渲染到畫面上,像是 computed 抽象就是拿到什麼我們不用管他,只要管我們的get set 就好 主客觀分離,製造體感 客觀使用,就是真正在使用的時候。 主觀是我們在設計的時候要決定的,要抽象什麼。 ## Ch9 ![image](https://hackmd.io/_uploads/HJtKR7Lw1g.png) # B ==458== ## B.1 Web 開發者工具 Firebug 已經於 2017 停用了,功能也整合到開發者工具中。 ==460== ## B.1.2 Firefox 開發者工具 提供了 RWD 模式,開發者必須留意這件事!因為使用者除了 PC、還會用行動裝置、平板、甚至電視~ ![image](https://hackmd.io/_uploads/ryf3h3pO1l.png) ## B.1.3 F12 開發者工具 介紹了 IE 也有這個工具! ==461== ## B.1.4 WebKit Inspector 介紹了 Safari 也有這個工具! ==462== ## B.1.5 Chrome DevTools 介紹了 Chrome 也有這個工具! ==463== ## B.2.1 系統日誌 日誌敘述句是用來在程式執行期間輸出訊息,而不會妨礙程式的正常執行流程。 例如:`console.log` 方法。 程式列表 B.1 範例是在講如果我想知道 `x` 的值的時候可以怎麼做~ ==464== 利用日誌功能,可以檢視程式執行的不同面向,如執行了哪些重要函式、霧件重要屬性的變化、或是否發生了某個特定事件等等~~ ## B.2.2 中斷點 會讓程式停在某一行上,並暫停瀏覽器的動作。 ==465== ==466== 這例子是 logGreeting 還沒被執行到 ## step into 一個函式 ![image](https://hackmd.io/_uploads/rJLP9pTdyg.png) 點選後就進入到下一行 ![image](https://hackmd.io/_uploads/S1n1opa_ye.png) ==467== ## step over / step out 除了 step into 可以進入一個函式 step over 會一行接著一行執行程式碼,如果在某一行裡包含一個函式呼叫,除錯器也會一步步走過該函式(函式會被執行,但除錯器不會跳進函式) 如果暫停了一個函式的執行,點擊 step out 將會執行程式直到函式結束,而 debugger 會在恰好離開了函式的地方,再度暫停執行。 兩者差別是 step Over 站在函式外部執行 執行當前行的程式碼,如果有函式呼叫,則直接執行函式但不進入內部 step Out 站在函式內部執行 執行剩餘的函式程式碼,然後返回到呼叫該函式的地方 ```javascript! function logGreeting(name) { debugger; // 這邊 step out 的話會跳到 logGreeting(ninja); 這裡 console.log("Greeting to the great", name) } const ninja = "Hattori Hanzo" logGreeting(ninja); ``` ## 條件式中斷點 標準的中斷點會在每次除錯器到達程式的特定點時,停止應用程式的運行,但某些情況下有可能會帶來不便。 例如迴圈時,我有一個跑 50 次的迴圈,我想在 49 次的時候設置中斷點,阿難道我們要重複 49 次的中斷嗎??? 因此可以使用 條件式中斷點,這是只有在與其相關的表達式滿足的時候才會中斷。 ![image](https://hackmd.io/_uploads/SkKyOR6_Jl.png) ![image](https://hackmd.io/_uploads/ByomdCadJl.png) 要按播放,就會停在 49!!!!!! ![image](https://hackmd.io/_uploads/S1yU6CTOke.png) ==468== ## B.3 建立測試 好的測試有這幾個特點: 1. 可重現 重複執行的程式應該始終產生一致的結果。 ==469== 2. 簡單 去除不必要的 code,以免混淆測試案例的意圖。 3. 獨立 應該在獨立的環境執行,避免讓其他結果相依於其他資源。 有兩種方法可以建構測試:解構式、建構式 解構式測試案例:消減現有程式以隔離問題,去除掉任何與問題無關的東西。 -> 可以說是逐步刪除 不相關的部分,慢慢找出問題。 建構式測試案例:我們從一個已知沒有問題、消減過的測試對象開始,逐漸增加它的內容,直到我們可以重現 Bug。 -> 從一個 已知沒有問題的簡單測試案例 開始,逐步增加程式碼,慢慢找出問題。 ==470== ==471== ## B.4 測試框架的基礎概念 ## 斷言 ==473== ## B.4.2 常見的測試框架 測試需要框架最好具備這些多功能 - 模擬瀏覽器行為 - 對測試案例的互動控制能力 - 處理非同步測試的逾時情況 - 篩選要執行哪些測試的能力 ## QUnit >assert.ok() 在 QUnit 中的使用 assert.ok(value, message) 用來測試 某個值是否為真 (truthy),如果 value 是 true、非空字串、非零數字、非空陣列等,則測試通過。 ## Jasmine describe it expect https://codepen.io/Jamixcs/pen/XJrQyoG