# J_忍者2 JavaScript 筆記 [TOC] # PART 1 熱身 ## Chapter 1 無所不在的JavaScript ### 瞭解JavaScript程式語言 JS的某些觀念在本質上與其他大多數語言有很大區別: * 函式為頭等物件 >JS中,函式可以同樣被視為一種物件處理,也可以與任何物件一同存在,可以由實值所建立、用變數參照、作為函式引數進行傳遞,甚至作為另一個函式的回傳值,第三章大部分都在探討這件事。 * 函式閉包 > 一個函式保持了他本體所用的外部變數,他就是一個閉包,第五章將詳細介紹。 * 可見範圍 > 他寫說直到最近JS還沒像C語言般的區塊變數,所以仰賴全域變數及函式變數。但我不清楚let跟const是不是區塊變數,但書中確實都只看到var,也有可能let跟const的概念沒有跟C語言的區塊變數一樣? * 基於原型的物件導向 > JS使用原型而不是類別來實作物件導向的概念,這也造成跟其他以類別為基礎的語言有所不同,所以這裡也要深入研究基於原型的物件導向如何運作及實作。 以上屬於基本概念,再來就要特別關注以下一些功能: * 生成器(generator):根據不同請求產生多個值,且在請求與請求之間暫停其執行的函式 * 約定(promise):更好的控制非同步程式 * 代理(proxy):控制對特定物件的存取 (pattern)史塔克與鋼鐵衣,鋼鐵衣代理先做第一層處理 * 進階的陣列方法:處理陣列的程式變得更優雅 * 對應表(map):建立字典般的資料集合 * 集合表(set):處理具有唯一性的數值集合 * 正規表達式:簡化原本可能複雜的程式(會先跳過) * 模組:將程式分解得更小,相對獨立出不同的部分,使專案更容易管理 > 將這些概念跟功能結合在一起,對語言的理解會更上一層,開發更輕鬆愜意 寫這本書的時候,ECMAScript委員會剛完成了ES7/ES2016的版本。ES7相對於ES6,是一個較小的升級版本,本書將徹底探討ES6,順便專注ES7的一些功能。 #### 轉譯器 瀏覽器平凡更新,會常常有新功能,但有時候會遇到其他用戶使用的是舊版本的瀏覽器,其中一個解法就是使用==轉譯器(轉換+編譯)==,將新的JS程式碼轉換為大多數現有瀏覽器都可正常運作的等效(或類似)程式碼。 最受歡迎是==Traceur==跟==Babel== ### 了解瀏覽器 JS可以運行在許多環境,但==瀏覽器才是最初的環境==,書中將專注以下幾點: * 文件物件模型DOM > 學習如何建立DOM以及如何撰寫操作DOM的有效程式碼 * 事件 > 大多數JS應用程式都是靠事件驅動的,包括網路事件、計時器和使用者產生事件,例如點擊、滑鼠移動、按鍵盤等等。十三章將探討背後機制。 * 瀏覽器API > 瀏覽器提供API,允許存取設備資訊、本地端儲存資料與遠端伺服器通訊。 ### 使用當前的最佳實踐 掌握語言與瀏覽器功能是程式開發高手的重要成分,但還要學習展現其他行業先知們證明過的實戰心法,稱為最佳實踐,有助於開發出高品質程式碼,另外還有幾類元素: * 程式碼除錯技巧 * 測試 * 效能分析 #### 程式除錯 現在大部分主要瀏覽器都有自己的除錯工具: * Firebug * Chrome DevTools * Firefox Developer Tools * F12 Developer Tools * WebKit Inspector #### 測試 這裡他自己定義了`assert`函式,不是JS的功能,他在附錄B有介紹他怎麼寫 `assert(condition, message);`,第一個引數是為真的條件,第二個引數是當前者不成立要顯示的訊息。 例如: ```javascript console.assert(a === 1, "Disaster! a is not 1!"); ``` 測試結果: ![image](https://hackmd.io/_uploads/rk8RYQZ_A.png) #### 效能分析 使用`console.time`啟動一個計時器並賦予名稱,然後寫上想測的程式碼,最後用`console.timeEnd`並給他跟剛剛相同名稱來結束測量,將會顯示計時器啟動以來經過的時間: ```javascript= console.time("My operation"); for(var n = 0; n < maxCount; n++){ //想要進行量測的操作 } console.timeEnd("My operation"); ``` 測試結果: ![image](https://hackmd.io/_uploads/S1X2jX-_C.png) ### 提高技能轉移性 了解JS的基礎原則,並具備核心API相關知識,可以成為多用途的開發者,透過瀏覽器和Node.js,我們幾乎可以開發出任何想得到的應用程式: * 桌面應用程式 * 與行動裝置平台進行互動 * 伺服器端與嵌入式系統應用程式 ## Chapter 2 在執行時期產生網頁 ### 2.1生命週期概述 ![image](https://hackmd.io/_uploads/ryhBvNZO0.png) 1. 使用者輸入網址或點擊超連結 2. 瀏覽器產生請求並發到伺服器 3. 伺服器執行某行為或取得某資訊,將回應送回客戶端 4. 瀏覽器處理HTML、CSS及JS,並建立頁面 5. 瀏覽器開始監聽事件,一次處理一事件 6. 使用者與頁面互動(滑鼠或鍵盤等) 7. 關閉Web應用程式 Web應用程式屬於圖形使用者介面(GUI)應用程式,所以生命週期與其他GUI應用程式類似,由以下兩步驟完成: 1. 建立頁面:設置使用者介面 2. 事件處理:進入一個等待事件發生及處理事件的迴圈 接著他示範了簡單的互動頁面,結果可以到source code的chapter2的`listing-2.1.html`看: ```html= <!DOCTYPE html> <html> <head> <title>Web app lifecycle</title> <style> #first { color: green;} #second { color: red;} </style> </head> <body> <ul id="first"></ul> <script> function addMessage(element, message){ var messageElement = document.createElement("li"); messageElement.textContent = message; element.appendChild(messageElement); } var first = document.getElementById("first"); addMessage(first, "Page loading"); </script> <ul id="second"></ul> <script> document.body.addEventListener("mousemove", function() { var second = document.getElementById("second"); addMessage(second, "Event: mousemove"); }); document.body.addEventListener("click", function(){ var second = document.getElementById("second"); addMessage(second, "Event: click"); }); </script> </body> </html> ``` ```javascript function addMessage(element, message){ var messageElement = document.createElement("li"); messageElement.textContent = message; element.appendChild(messageElement); } ``` `addMessage(element, message)`執行時會將傳入的`element`中建立一個`<li>`標籤,並將傳入的`message`放進`<li>`中。 ```javascript document.body.addEventListener("mousemove", function() { var second = document.getElementById("second"); addMessage(second, "Event: mousemove"); }); ``` `document.body.addEventListener("mousemove", function(){})`是在`<body>`中監聽滑鼠的移動事件,只要移動就執行後面的`function` 使用`getElementById`來取得文件中具有此`id`的元素 所以只要在`<body>`移動滑鼠就會在`second`中加入`<li>`並填入`"Event: mousemove"`;在`<body>`中點擊滑鼠左鍵就會在`second`加入`<li>`並填入`"Event: click"`。 ### 2.2頁面建立階段 Web應用程式可以操作之前,必須先從伺服器端接收到回應訊息,並根據得到的訊息建立出來(通常是HTML、CSS及JS),頁面建立階段的目標是設置Web應用程式的UI介面,兩步驟: 1. 解析HTML並建立DOM 2. 執行JS程式 處理HTML時執行步驟一,遇到特殊類型的HTML元素(`<script>`元素)執行步驟二,在建立階段瀏覽器可以根據需要在兩個步驟間切換多次。 ![image](https://hackmd.io/_uploads/SkV1MHbuA.png) #### 2.2.1解析HTML並建立DOM 接收到HTML代碼後將解析他(一次處理一個HTML元素)並建立DOM(每個HTML元素都代表著一個節點): ![image](https://hackmd.io/_uploads/HkeTmrZ_A.png) 如圖所示,每讀到一個標籤就會建立一個元素,最後組成DOM,這裡展示的為讀到第一個`<script>`前所建立的多個元素的DOM結構。 雖然DOM是從HTML建構的,但他們==不是同一種東西==,應該將HTML代碼視為建立最初的DOM時所及遵循的藍圖,瀏覽器甚至能發現這個藍圖中的問題並修復它,以建立有效的DOM。 ![image](https://hackmd.io/_uploads/rJfR4rZd0.png) 圖中展示了`<p>`元素被放置在`<head>`元素中,但head區塊是用於提供一般性頁面訊息,例如:頁面標題、字元代碼、外部樣式等等,不是用來定義頁面內容,所以這是一項錯誤,瀏覽器為了建立正確的DOM,默默地修復他,將它(`<p>`)放到`<body>`中。 當瀏覽器碰到特殊類型HTML元素時:`<script>`元素,用來包含JS程式碼的區塊,此時瀏覽器就會暫停閱讀HTML來建立DOM結構,轉而執行JS程式碼。 #### 2.2.2執行JavaScript程式碼 `<script>`中的JS程式碼,都是由瀏覽器的JS引擎執行的,Firefox的spidermonkey、Chrome和Opera的V8及IE的Chakra。由於JS主要目的是提供頁面的動態行為,瀏覽器透過一個全域物件來提供一組API,讓JS引擎跟頁面互動,並修改頁面。 **JavaScript中的全域物件** ==瀏覽器透露給JS引擎的主要全域物件是window物件==,代表著頁面所屬的瀏覽器視窗,他是一個特殊全域物件,藉由他可以存取所有其他全域物件、全域變數及瀏覽器API。其擁有==最重要的屬性是document==,也就是目前頁面的DOM結構,藉由使用這個物件,JS代碼可以改變所在頁面的DOM結構,像是修改、刪除現有元素,甚至建立及插入新元素。 ```javascript var first = document.getElementById("first"); ``` 使用document全域物件從DOM結構中找出ID為first的元素,將他指派給first這個變數,接著就可以任意操作這個元素了。 > 瀏覽器API > 若要涵蓋瀏覽器所有API,會遠遠超出一本JS書籍的範疇,Mozilla建立了[MDN Web API各式資訊](https://developer.mozilla.org/en-US/docs/Web/API) **不同類型的程式碼** 書中將JS程式碼分為兩種:全域程式及函式程式。 ```html <script> function addMessage(element, message){ var messageElement = document.createElement("li"); messageElement.textContent = message; element.appendChild(messageElement); } </script> ``` 上面屬於==函式程式==,處於函式內 下面屬於==全域程式==,處於函式外 ```html <script> var first = document.getElementById("first"); addMessage(first, "Page loading"); </script> ``` ==全域程式==會在JS引擎中自動逐行==直接被執行==。 且為了執行函式程式,必須透過全域函式、其他函式或由瀏覽器呼叫。 **在頁面建立階段執行JS程式碼** ![image](https://hackmd.io/_uploads/HkTEFP-d0.png) 繼續前面的例子,當瀏覽器讀到`<script>`後停止對DOM結構的建立,轉而執行JS程式碼,此時已經可以透過`document.getElementById("first")`來操作已經建立好的first元素了,所以成功建立`<li>Page loading</li>`到first這個`ul`元素中,這時就可以任意修改DOM的結構,新增或刪除節點等等,但==做不到選取或修改尚未建立的元素==,所以此時還無法操作ID為second的`ul`元素,因為second是在這段JS執行完後才會建立的,所以這是人們==往往都會將`<script>`元素放在頁面底部==的原因之一,也就不用擔心還有哪些元素還沒被建立。 執行完`<script>`中的最後一行後,瀏覽器就會退出JS執行模式,繼續處理後續的HTML代碼,建立其他的DOM節點,接著如果再次碰到`<script>`元素,就再次停止建立DOM,執行JS程式碼,此時JS引擎會保持他的全域狀態,前面執行過的`<script>`裡的內容,也能繼續被其他`<script>`中的程式碼取用,這是因為存放所有全域變數的==全域物件window,在頁面的整個生命週期都是有效且可存取的==。 接著一樣重複: 1. 解析HTML並建立DOM 2. 執行JS程式 兩個步驟不斷交互執行,直到瀏覽器處理完所有的HTML代碼,意味著頁面建立階段的結束,此時瀏覽器將進入生命週期第二部分:事件處理。 ### 2.3事件處理 Web應用程式即是GUI應用程式,代表他會對不同類型事件做出反應,例如:移動滑鼠、點擊、打鍵盤等等。所以頁面建立時執行的JS程式碼,除了影響Web的全域狀態及修改DOM結構外,還可以註冊事件監聽器,也就是==瀏覽器在事件發生時要執行的函式==。 #### 2.3.1事件處理概述 由於瀏覽器執行環境是以單一執行緒執行模型(一次只能執行一段程式碼),所以每件事都會排隊執行,但也不可能永遠叫使用者等到這個事件處理完再觸發下一個事件,所以瀏覽器使用了事件佇列: 1. 瀏覽器檢查事件佇列頂部 2. 沒有事件,瀏覽器持續檢查 3. 佇列頂部有事件,瀏覽器會取得第一個且執行相關處理程式,剩餘的事件繼續在佇列中等待 ![image](https://hackmd.io/_uploads/SkEIHdZuR.png) 由於每次處理一個事件,所以要小心事件所需時間,執行需要大量時間的事件處理程式,Web應用程式會無法產生回應(十三章將詳細討論事件迴圈) :::info 將事件放入佇列中是在頁面建立階段及事件處理階段之外的機制,判斷事件何時發生並將其送入事件佇列這些動作,並不在處理事件的執行緒中。 ::: **事件是非同步** 控制使用者照順序觸發事件很奇怪,所以==事件的處理以及對事件處理函式的呼叫會以非同步進行==。 可能發生的事件類型: * 瀏覽器事件,例如網頁已載入或未載入 * 網路事件,來自伺服器的回應 * 使用者事件,點擊滑鼠 * 計時器事件,等候逾時或間隔性觸發條件 ==事件處理的概念是Web應用程式的核心==,事先設置好程式碼,以便之後執行。 #### 註冊事件處理器 兩種方式: * 將函式指定給特殊屬性 * 使用內建addEventListener方法 針對load事件註冊一個事件處理器: ```javascript window.onload = function(){}; ``` --- 為document主體的click事件註冊一個處理器: ```javascript document.body.onclick = function(){}; ``` 但==不建議這麼做==,這樣對某一特定事件只能指派一個事件處理器,所以很容易覆蓋掉之前的事件處理函式,所以另一個替代方法:addEventListener,可以註冊多個事件處理函式(前面就是用這個方式): ```html <script> document.body.addEventListener("mousemove", function() { var second = document.getElementById("second"); addMessage(second, "Event: mousemove"); }); document.body.addEventListener("click", function(){ var second = document.getElementById("second"); addmessage(second, "Event: click"); }); </script> ``` 接著做了一個解說:使用著在頁面完全建立後,快速移動滑鼠且點擊滑鼠,此時就會將mousemove及click事件放到事件佇列上,先是mousemove事件,再來是click事件。 事件處理階段: 1. 事件迴圈檢查佇列 2. 發現mousemove事件並執行相關事件處理器 3. 處理mousemove時click會在事件佇列中等待 4. mousemove執行完畢後繼續查找事件佇列 5. 發現click事件並處理它 6. 一旦click處理器執行完畢,佇列中沒有新事件 7. 事件迴圈繼續循環,等待處理新事件 8. 直到使用者關閉Web應用程式 # PART 2 理解函式 ## Chapter 3 初探頭等函式:定義與引數 ### 3.1 使用函式與否的差異 JS中,==物件==具有一些特性: * 由實值來建立 * 可以指派給變數、陣列或其他物件的屬性 ```javascript var ninja = {}; ninjaArray.push({}); ninja.data = {}; ``` --- 也可以作為引數傳遞給函式: ```javascript function hide(ninja){ ninja.visibility = false; } hide({}); //將新建立物件作為引數傳遞給函式hide() ``` --- 作為函式的回傳值: ```javascript function returnNewNinja() { return {}; //函式回傳一個新物件 } ``` --- 有可動態建立和指派的屬性: ```javascript var ninja = {}; ninja.name = "Hanzo"; //在物件上新增一個屬性 ``` --- 與許多其他語言不同,==在JS中,函式能做到的幾乎和物件一模一樣==。 ### 3.1.1 函式作為頭等物件 由於JS函式擁有物件所有功能,所以當物件般處理: * 由實質建立: ```javascript function ninjaFunction() {} ``` --- * 指派給變數、陣列、物件的屬性: ```javascript var ninjaFunction = function() {}; ninjaArray.push(function(){}); ninja.data = function(){}; ``` --- * 作為引數傳遞給其他函式: ```javascript function call(ninjaFunction){ ninjaFunction(); } call(function(){}); ``` --- * 作為函式的回傳值: ```javascript function returnNewNinjaFunction() { return function(){}; } ``` --- * 有可動態建立和指派的屬性: ```javascript var ninjaFunction = function(){}; ninjaFunction.name = "Hanzo"; ``` --- 簡單來說,物件可以做的函式都可以,而==函式是可以被呼叫的物件==。 ### 3.1.2 回呼函式 當我們設置一個函式希望在稍後被呼叫,無論是透過瀏覽器的事件處理或透過其他程式碼,就是在設置一個回呼函式, 白話文:我建立一個函式,他等等在適當的時機才會被呼叫執行。 例子3.1(可以開source code的listing-3.1.html): ```javascript= var text = "Domo arigato!"; report("Before defining functions"); function useless(ninjaCallback) { report("In useless function"); return 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"); ``` 上面要加上他自己寫的`assert`跟`report`才可以跑,我簡單改寫一下: ```javascript= function report(text) { console.assert(false, text) } var text = "Domo arigato!"; report("Before defining functions"); function useless(ninjaCallback) { report("In useless function"); return ninjaCallback(); } function getText() { report("In getText function"); return text; } report("Before making all the calls"); console.assert(useless(getText) !== text, "The useless function works! " + text); report("After the calls have been made"); ``` 在`assert`中呼叫了`useless`,所以執行了`useless`函式,在`useless`函式中又對傳入的`getText`進行回呼,所以又執行了`getText`且得到了回傳值`text`。 他的結果會是: ![image](https://hackmd.io/_uploads/B1T6m-MOC.png) 我改寫的結果是: ![image](https://hackmd.io/_uploads/Sy-MEdOFR.png) 順序一樣。 也可以重寫成: ```javascript= var text = 'Domo arigato!'; function useless(ninjaCallback) { return ninjaCallback(); } assert(useless(function () { return text;}) === text, "The useless function works! " + text); ``` 直接定義作為引數的回呼函式。 這也是一個重要的特性,能在程式中任何地方建立函式,除了使程式碼更緊湊且更好理解,這個函式也==不需要從別的地方引用==,避免了全域命名空間被==不必要的命名污染==。 前面提過的瀏覽器呼叫,定義為mousemove的事件被觸發時瀏覽器將呼叫後面的函式: ```javascript document.body.addEventListener("mousemove", function() { var second = document.getElementById("second"); addMessage(second, "Event: mousemove"); }); ``` **使用比對器進行排序** 這邊介紹了陣列的`Array.prototype.sort`方法,傳入一個回呼函式,定義你想要用什麼演算方式來排序這些數值,如果傳入的兩個數想要反轉,則回呼函式需要回傳一個正整數;如果不反轉,回呼函式則要回傳負數,如果兩數相等則回傳0。 ```javascript= var values = [0, 3, 2, 5, 7, 4, 8, 1]; values.sort(function(value1, value2){ return value1 - value2; }); ``` 不需要探討底層細節,總之就是每次需要比較兩個元素時呼叫我們提供的回呼函式。 ### 3.2 函式作為物件的有趣之處(?) > 不有趣((你的同事也不會覺得有趣,這章介紹的技巧都是另外找容器來裝值,但其實只要直接用容器處理就好 > [name=Chris] 因為函式屬於物件,所以我們可以將屬性附加到函式上: ```javascript var ninja = {}; //物件 ninja.name = "hitsuke"; var wieldSword = function(){}; //函式 wieldSword.swordType = "katana"; ``` 先預告派上用場的時機: * 把多個函式存在集合中,輕鬆管理彼此互間有關聯的函式,例如一系列的事件回呼函式 * 讓函式記住之前計算得到的舊值,提高後續呼叫的執行效能 #### 3.2.1 儲存函式 > 使用`new Set()` > [name=Chris] 建立一個store物件來存放函式: ```javascript= var store = { nextId: 1, cache: {}, add: function(fn) { if (!fn.id) { fn.id = this.nextId++; this.cache[fn.id] = fn; return true; } } }; function ninja(){} console.log(store.add(ninja), "Function was safely added."); console.log(!store.add(ninja), "But it was only added once."); ``` 這邊有兩個屬性: * 第一個屬性nextId:存放下一個可用的ID值 * 第二個屬性cache:一個物件,暫存所要儲存的函式 並將函式透過add()方法來存放到cache中。 ```javascript function(fn) { if (!fn.id) { fn.id = this.nextId++; this.cache[fn.id] = fn; return true; } ``` 首先檢查id存在與否,如果已經存在表示這個函式之前處理過了,將忽略這次操作,如果不存在,就對該函式指派一個id屬性值(並同時增加nextId屬性值),以這個id值作為屬性名稱添加到cache中,然後回傳true,以便在呼叫add()後知道函式已被添加完成。 結果: ![image](https://hackmd.io/_uploads/ByQkHzfOA.png) 他在第二次的時候用了`!store.add(ninja)`所以還是會收到true,不太懂為什麼要。 可以查看store裡面確實有了ninja ![image](https://hackmd.io/_uploads/HydwHzMd0.png) #### 3.2.2 自我記憶函式 > 使用`new Map()` > [name=Chris] ```javascript= function isPrime(value) { if (!isPrime.answers){ isPrime.answers = {}; } if (isPrime.answers[value] !== undefined) { return isPrime.answers[value]; } var prime = value !== 0 && value !== 1; // 1 is not a prime for (var i = 2; i < value; i++) { if (value % i === 0) { prime = false; break; } } return isPrime.answers[value] = prime; } console.log(isPrime(5), "5 is prime!" ); console.log(isPrime.answers[5], "The answer was cached!"); ``` `isPrime`最開始先檢查了有沒有用來暫存的空物件,沒有就建立: ```javascript if (!isPrime.answers){ isPrime.answers = {}; } ``` 再來對我們傳入的值判斷有沒有在暫存的answers中: ```javascript if (isPrime.answers[value] !== undefined) { return isPrime.answers[value]; } ``` 在暫存中,會將傳入的value作為鍵值(key)來儲存算後的結果(true or false),如果有找到答案就回傳他。 如果沒有找到,就會進行計算判斷是否為質數,並將判斷結果儲存在剛剛的answers中: ```javascript return isPrime.answers[value] = prime; ``` 最後測試: ```javascript assert(isPrime(5), "5 is prime!" ); assert(isPrime.answers[5], "The answer was cached!" ); ``` 執行`isPrime(5)`後我們已經可以透過`isPrime.answers[5]`找到剛剛的結果了。 然而會有幾個缺點: * 任何類型的暫存都是==花費記憶體來換取效能== * 有些人認為一個函式一個方法應該只做一件事並將他做好,關於這點之後再討論處理 * 很難對其進行負載測試或測量效能 ### 3.3 定義函式 我們可以直接用字面上的方式定義一個函式((?,==函式就像字串和數值一樣,是可以在語言中使用的值==,即是你有沒有發現,你都一直這麼做。 主要的四種定義函式方式: * 函式宣告(declaration)和函式表達式(expression),這兩種最常見,了解兩者的差異能更清楚何時可以呼叫他們: ```javascript function myFun(){ return 1;} ``` * 箭頭函式,ES6提供的新功能,==更簡潔,甚至解決回呼函式的一些問題==: ```javascript myArg => myArg*2 ``` * 函式建構式(constructor),不太常用,==可以接收一個動態產生的字串,動態的建立新的函式==: ```javascript new Function('a', 'b', 'return a + b') ``` * 生成器(generator)函式,ES6加入的,可以在應用程式執行退出和重新執行他,同時在不同進入點保持他的變數值,生成器可以定義函式宣告、函式表達式和建構函式,==有看沒有懂,等大神解釋==。 ```javascript function* myGen(){ yield 1; } ``` 生成器在第六章才會詳細介紹,但我們會先跳過,==Chris覺得還不用研究太深==。 ==函式建構式(new Function)將完全忽略不提==,雖然有些有趣功能,但他覺得不重要。 從最簡單且最傳統的方式開始:函式宣告與函式表達式。 #### 3.3.1 函式宣告與函式表達式 **函式宣告** 最基本的方式,必要的樣子: ```javascript function myFunctionName() {} ``` 剩下的參數、逗號、函式內容選擇性放入: ```javascript function myFunctionName ( myFirstArg, mySecondArg ) { myStatement1; myStatement2; } ``` 另一個函式宣告例子: ```javascript function samurai() { return "samurai here"; } function ninja() { function hiddenNinja() { return "ninja here"; } return hiddenNinja(); } ``` 在`ninja()`內定義另一個`hiddenNinja()`。 **函式表達式** 前面提過函式能像數值一樣被使用,數值的使用: ```javascript var a = 3; myFunction(4); ``` 所以我們可以將函式這樣使用: ```javascript var a = function() {}; myFunction(function(){}); ``` 這種==指派函式給變數或是作為另一個函式的參數==,就是函式表達式,這讓我們在==真正需要函式的時候才定義他==,程式會更好理解。 所以與函式宣告的差別之一就是程式碼出現的位置不同,==函式宣告是一個獨立的區塊==,相反的,==函式表達式總是另一個敘述句的一部份==,另一個差異在於,==函式名稱在函式宣告時是必須的==,==函式表達式中可有可無==。 因為函式基本要求就是要可以被呼叫,所以函式宣告要有名字我們才能藉由名字呼叫他。 函式表達式是其他表達式的一部分,所以可以透過其他方式呼叫。 指派給變數,使用變數名字來呼叫他: ```javascript var doNothing = function(){}; doNothing(); ``` 函式是另一個函式的參數,透過參數名稱來呼叫: ```javascript function doSomething(action) { action(); } ``` **立即函式(immediate function)** 看一下結構: ![image](https://hackmd.io/_uploads/H12HHhVuA.png) 左邊是==標準函式呼叫==,右邊是==定義完函式立即呼叫== 立即呼叫新定義好的函式,就是立即呼叫的函式表達式(IIFE: immediately invoked function expression),簡稱**立即函式**,幫助我們模擬出模組功能,十一章將關注應用。 最後介紹四個表達式: ```javascript +function(){}(); -function(){}(); !function(){}(); ~function(){}(); ``` 利用四個一元運算子來區分他們與函式宣告不同,就不需要在函式周圍使用括號,雖然用這些運算子最後會計算出一些結果,但他不會儲存於任何地方,所以不需要在意這點,重點是對IIFE的呼叫。 > 題外話,`~`這個小蚯蚓運算子是NOT運算子,將資料視為二進位然後全部反轉((超難搜尋的 #### 3.3.2 箭頭函式(arrow function)(lambda) 之前的範例: ```javascript var values = [0, 3, 2, 5, 7, 4, 8, 1]; values.sort(function(value1,value2){ return value1 – value2; }); ``` 改用箭頭函式: ```javascript var values = [0, 3, 2, 5, 7, 4, 8, 1]; values.sort((value1,value2) => value1 – value2); ``` 簡潔許多。 比較箭頭函式與函式表達式: ```javascript var greet = name => "Greetings " + name; console.assert(greet("Oishi") === "Greetings Oishi", "Oishi is properly greeted"); var anotherGreet = function(name){ return "Greetings " + name; }; console.assert(anotherGreet("Oishi") === "Greetings Oishi", "Again, Oishi is properly greeted"); ``` 我改成`console.assert`,這邊放到瀏覽器主控台去跑不會有東西,因為兩個等號都成立所以不會印出後面的字串,作者自己寫的assert(),不管條件成立與否都會印出來,只是字的樣式不同。 作者說來==欣賞==一下箭頭函式: ![image](https://hackmd.io/_uploads/SyFjR3VdR.png) 左邊==參數如果只有一個,括號可以省略==,右邊如果只有一個表達式,那表達式的值就會是箭頭函式的回傳值;如果是一個區塊,那就跟一般函式區塊一樣,沒有回傳值就會回傳`undefined`: ```javascript var greet = name => "Greetings " + name; ``` --- ```javascript var greet = name => { var helloString = 'Greetings '; return helloString + name; }; ``` ### 3.4 引數(argument)與函式參數(parameter) * 參數是定義函式時列出的變數 * 引數是呼叫函式時傳遞給他的值 所以白話文就是:定義函式時寫的是參數,呼叫執行時傳的是引數。 ```javascript= function skulk(ninja) { //ninja是參數 return performAction(ninja, "skulking"); //這兩個是引數 } var performAction = function (person, action){ //這兩個是參數 return person + " - " + action; }; var rule = daimyo => performAction(daimyo, "ruling"); // 前面daimyo是參數,後面daimyo跟"ruling"是引數 skulk("abc"); //這邊的"abc"都是引數 rule("abc"); ``` 引數與參數數量不同: ```javascript= practice ("Yoshi", "sword", "shadow sword", "katana"); //"katana"未被指派給變數 function practice (ninja, weapon, technique) { ... } practice ("Yoshi"); //ninja指派為"Yoshi",剩下的兩個為undefined ``` 如果引數數量超過參數了,例如這邊的`"katana"`,他不會被指派給任何參數,下一章將實現即使未被指派,我們依然可以取得他。 又如果引數數量少於參數,那剩餘沒有對應的參數會被設定成`undefined` 接下來就要談到ES6的新功能:不定參數(rest parameter),和預設參數(default parameter)。 #### 3.4.1 不定參數 直接看範例: ```javascript= function multiMax(first, ...remainingNumbers){ var sorted = remainingNumbers.sort(function(a, b){ return b - a; }); return first * sorted[0]; } ``` `multiMax()`會將第一個參數與後續所有參數中的最大值相乘後回傳。 其中會先將剩餘所有參數`...remainingNumbers`放在新的陣列,將他==降冪排列==後指派給新變數`sorted`,這樣就可以用`first`去乘上後續參數中最大的數了。 * ==只有最後一個參數可以是不定參數==,放在中間會報錯。 #### 3.4.2 預設參數 也直接看範例: ```javascript= function performAction(ninja, action) { return ninja + " " + action; } console.log(performAction("Fuma", "skulking")); console.log(performAction("Yoshi", "skulking")); console.log(performAction("Hattori", "skulking")); console.log(performAction("Yagyu", "sneaking")); ``` 這邊只有`"Yagyu"`這個人想要的`action`不一樣,其他忍者都使用相同的`"skulking"`,所以看起來`"skulking"`,所以考慮將其設定成預設參數,==在ES6之前的做法==: ```javascript function performAction(ninja, action){ action = typeof action === "undefined" ? "skulking" : action; return ninja + " " + action; } console.log(performAction("Hattori")); //'Hattori skulking' console.log(performAction("Yagyu", "sneaking")); //'Yagyu sneaking' ``` ==用`typeof`來判斷`action`這個參數有沒有被設定==,如果沒有(也就是`undefined`),就給定預設的`"skulking"`;否則就是給引數的值。 因為很麻煩,所以ES6推出了新的做法: ```javascript function performAction(ninja, action = "skulking"){ return ninja + " " + action; } console.log(performAction("Hattori")); //'Hattori skulking' console.log(performAction("Yagyu", "sneaking")); //'Yagyu sneaking' ``` 直接在宣告參數時就給定預設值,如果傳入相對應的引數,就將預設值覆蓋過去。 預設參數可以設定基礎型值、複雜類型的物件、陣列甚至是函式,我們也可以在函式中取得之前預設的參數: ```javascript function performAction(ninja, action = "skulking", message = ninja + " " + action){ return message; } ``` 書中說雖然JS允許這麼做,但要小心使用,他覺得不會增加可讀性,所以應該盡量避免,但適度使用對於==避免取到空值(null)==或==作為設定函式行為的簡單旗標==(flag)確實有所幫助。 不太懂取到空值null的意思,我嘗試傳進去null會很奇怪。 對於旗標(flag)理解: 通常用來控制函式的某些行為,下面的例子是==透過布林值來啟用或禁用某個功能==: ```javascript= function downloadFile(url, savePath, overwrite = false) { if (overwrite) { console.log("Overwriting the existing file."); } else { console.log("Not overwriting the existing file."); } // 其他下載檔案的邏輯 } //overwrite預設是false,所以一般情況不會覆寫檔案 downloadFile("http://example.com/file.txt", "/path/to/save"); //這邊改變了overwrite成true,表示我就是要覆寫 downloadFile("http://example.com/file.txt", "/path/to/save", true); ``` > 這個年代沒有人會這樣寫,好的程式碼不應該有旗標 > [name=Chris] ## Chapter 4 老手看函式:理解函式呼叫 ### 4.1 使用函式隱含引數 會說隱含是因為他==沒有列在函式署名中(function signature)==,默默傳遞給函式,並在函式內可存取。 找不太到很具體的解釋,感覺像你在宣告時寫的內容。 [function signature參考](https://www.basedash.com/blog/function-signatures-in-javascript) #### 4.1.1 arguments參數 `arguments`參數讓我們可以取得函式內所有的參數之集合,雖然前面提過的不定參數已經解決大部分問題,因此對`arguments`的需求比較少,但處理比較舊的程式碼會碰到。 `arguments`擁有`length`屬性來表示擁有的引數確切數量,也可以透過索引值取得引數:例如`arguments[2]`取得第三個引數。 ```javascript= function whatever(a, b, c) { console.log("a ===", a); //a === 1 console.log("arguments[0] ===", arguments[0]); //arguments[0] === 1 console.log("arguments[3] ===", arguments[3]); //arguments[3] === 4 console.log("arguments[4] ===", arguments[4]); //arguments[4] === 5 console.log("arguments.length === ", arguments.length); //arguments.length === 5 } whatever(1, 2, 3, 4, 5); ``` 宣告時只有宣告a, b, c三個參數,但呼叫時傳入了五個引數,所以前三個`1, 2, 3`會分別賦值給`a, b, c`,後面的`4, 5`就只能用`arguments`來取得,我們也可以使用`arguments.length`來查看所有引數的數量。 雖然`arguments`看起來很像陣列,這些用法也跟陣列一樣,但實際上他不是,當我們想在上面用一些陣列方法可能會失敗,應該將它視為陣列般的結構,盡量不去使用它。 這邊我做了小觀察,將`arguments`用`console.dir`去展開: ![image](https://hackmd.io/_uploads/HyvkhC2OR.png) 這邊顯示他是一個物件。 現在來做一個將不定數量引數全部加總的函式: ```javascript= function sum() { var sum = 0; for(var i = 0; i < arguments.length; i++) { sum += arguments[i]; } return sum; } console.log(sum(1, 2, 3)); //6 console.log(sum(1, 2, 3, 4, 5)); //15 ``` 如此一來就能看出`arguments`物件能讓我們寫出更多樣化、更靈活的程式碼。 **arguments的小陷阱** `arguments`參數有個奇怪功能,可以透過`arguments[]`來改變參數的值: ```javascript= function infiltrate(person) { console.log("person ===", person); //person === gardener console.log("arguments[0] ===", arguments[0]); //arguments[0] === gardener arguments[0] = "ninja"; //改變了arguments[0]的值 console.log("changed arguments[0] to ninja"); console.log("person ===", person); //person === ninja console.log("arguments[0] ===", arguments[0]); //arguments[0] === ninja person = "gardener"; //改變person的值 console.log("changed person gardener"); console.log("person ===", person); //person === gardener console.log("arguments[0] ===", arguments[0]); //arguments[0] === gardener } infiltrate("gardener"); ``` ![image](https://hackmd.io/_uploads/SyByXk6d0.png) 可以發現不管單獨改動`person`還是`arguments[0]`,兩個值都會一起改變,所以==這個別名是雙向的==。 **避免別名** 使用`arguments`物件來作為參數的別名可能造成混淆,所以JS提供了嚴格模式來禁止`arguments`作為參數的別名: ```javascript= function infiltrate(person) { "use strict" //嚴格模式 console.log("person ===", person); //person === gardener console.log("arguments[0] ===", arguments[0]); //arguments[0] === gardener arguments[0] = "ninja"; //改變了arguments[0]的值 console.log("changed arguments[0] to ninja"); console.log("person ===", person); //person === gardener console.log("arguments[0] ===", arguments[0]); //arguments[0] === ninja person = "others"; //改變person的值 console.log("changed person to others"); console.log("person ===", person); //person === others console.log("arguments[0] ===", arguments[0]); //arguments[0] === ninja } infiltrate("gardener"); ``` ![image](https://hackmd.io/_uploads/ByNYmkauR.png) 改成嚴格模式後雙向的別名就消失了,我改`arguments[0]`就只改到`arguments[0]`,我改`person`就只改到`person`。 然後他等等才要回來討論`arguments`,現在要來討論`this`了:cry: #### 4.1.2 this參數:介紹函式背景空間(context) 除了前面提到的`arguments`外,另一個隱含的參數就是`this`,他是JS在物件導向寫法裡的重要成分,是==函式呼叫的相關物件==,通常被稱為==函式背景空間(function context)==。 我剛剛講的函式呼叫書中是寫函『數』呼叫,台灣狗椅素嗎?我去看原文都是寫function。 再來他說,物件導向語言(例如java)使用者可能會以為自己很了解JS的`this`,但JS中的`this`會根據呼叫函式的方式不同而有所影響,之後會檢視各種呼叫函式的方式,主要區別都會在如何決定`this`的值,霹靂啪拉講一堆我其實也懶得解釋,==他說不懂沒辦法進入狀況沒關係==。 ### 4.2 呼叫函式 四種方法來呼叫函式,其中都各有一些差別: * 作為函式 `skulk()` > 函式的一般呼叫形式 * 作為方法 `ninja.skulk()` > 將函式作為方法綁定到物件上,透過物件來呼叫方法 * 作為建構器函式 `new Ninja()` > 建立新物件 * 經由函式的`apply`或`call`方法 > `skulk.apply(ninja)`或`skulk.call(ninja)` 接下來他就寫了這些用法的例子,但大家應該都多少用過,就不另外寫出來了。 #### 4.2.1 作為函式來呼叫 這邊特別講這個最一般的函式呼叫,是==為了與其他像是『當作方法呼叫』、『當成建構器函式』或『透過apply或call呼叫』做區別== 呼叫之前宣告的函式: ```javascript function ninja(){}; ninja(); ``` 函式表達式當函式呼叫: ```javascript var samurai = function(){}; samurai(); ``` 立即函式表達式當函式呼叫: ```javascript (function(){})(); ``` 這三種都屬於==函式作為函式來呼叫==,不知道能不能先理解成一般函式呼叫,在這些狀況呼叫函式時,`this`的值會有兩種可能: * ==普通模式下`this`是`window`物件== * ==嚴格模式下`this`是`undefined`== #### 4.2.2 作為方法呼叫 ==將函式指派給物件的屬性,使用屬性來呼叫它==,就是被視為物件的方法呼叫: ```javascript var ninja = {}; ninja.skulk = function(){}; ninja.skulk(); ``` 此時`ninja`這個物件會成為背景空間,也就是說==取`this`的值會是`ninja`這個物件==。 一般函式呼叫: ```javascript= function whatIsThisNow() { return this; } whatIsThisNow(); //Window物件 ``` 作為`ninja1`的方法呼叫: ```javascript= function whatIsThisNow() { return this; }; var ninja1 = { name: "ninja1", getMyThis: whatIsThisNow, //設定成ninja1的方法 }; ninja1.getMyThis(); //ninja1物件 ``` `this`如預期的會是`ninja1`物件,這裡==需要注意的是`whatIsThisNow`這個函式他還是獨立的函式,還是可以透過各種方式呼叫==,不會因為設定成`ninja1`的方法就改變。 #### 4.2.3 作為建構器呼叫 建構函式宣告就跟一般函式一樣,所以我們也可以使用一般函式宣告或是函式表達式來建立新物件,但==箭頭函式例外,之後再討論==。 **建構函式的超能力** ```javascript! function Ninja(name) { this.name = name; this.skulk = function() { return this; } } var ninja1 = new Ninja("Jeremy"); var ninja2 = new Ninja("Watson"); console.log(ninja1); console.log(ninja2); ``` ![image](https://hackmd.io/_uploads/rkB9k-0dA.png) 這樣我們就建立了`ninja1`跟`ninja2`兩個物件,而且裡面都有`skulk()`技能可以使用。 再來複習一次`new`建構函式後會發生什麼: 1. 建立一個新的空物件 2. 將這個空物件做為`this`參數傳遞給建構器,成為函式背景空間 3. new會回傳這個新建立的物件(也有例外之後再談) 圖解在這: ![image](https://hackmd.io/_uploads/ryVOgZC_C.png) **建構器的回傳值** 當我宣告建構函式時在最後`return`另一個值會如何呢: ```javascript= function Ninja() { this.skulk = function() { return this; }; return 1; } console.log(Ninja()); //1 console.log(new Ninja()); //Ninja{} ``` 確實直接執行`Ninja()`會得到回傳值`1`,但是==透過`new`來呼叫時==還是得到了新的`Ninja`物件,證明他==忽略了回傳值`1`==。 剛剛回傳的是一個數值,那如果現在改成回傳物件呢: ```javascript= var puppet = { rules: false, } function Emperor() { this.rules = true; //將新物件的rules設成true return puppet; //回傳剛剛就建立好的物件puppet } var emperor = new Emperor(); console.log(emperor); //{rules: false} ``` 這次他==回傳了原本`rules: false`的物件`puppet`==,而不是新建立的`Emperor`物件。 這邊我又嘗試看看回傳陣列,結果`new`出來是一個陣列,也不是`Emperor`物件,應該是因為陣列也是一種物件? **建構器設計考量** 建構器的目的是在對之後建立的新物件初始化,如果我們使用一般函式呼叫或是指派給物件的方法使用,通常沒有什麼意義: ```javascript= function Ninja() { this.skulk = function() { return this; } } var whatever = Ninja(); ``` 這麼做跟直接呼叫`Ninja()`是一樣的,因為他沒有執行背景,所以在普通模式下會是==在`Window`物件上建立`skulk`方法==,非常奇怪。又如果是在==嚴格模式,`this`將會是`undefined`,然後程式就會掛掉==,這時候就能看出嚴格模式的使用時機,如果普通模式可能不容易察覺哪裡出問題。 **建構函式命名** 所以這邊就介紹了命名規範讓我們去遵守,==函式跟方法我們會使用動詞開頭來命名==,表示一個行為,以小寫字母為開頭,==建構器通常會用描述一個物件的名詞來命名==,以大寫字母開頭。 #### 4.2.4 使用apply或call方法來呼叫函式 前面介紹的呼叫方法,我們都必須搞清楚不同呼叫方式他的`this`是什麼,這邊要介紹==我想要我的`this`是什麼他就是什麼==!!也就是自己設置我的`this`。 還不知道什麼情境會用到?看一下範例: ```html= <button id="test">Click Me!</button> <script> function Button(){ this.clicked = false; this.click = function(){ this.clicked = true; }; } var button = new Button(); var elem = document.getElementById("test"); elem.addEventListener("click", button.click); </script> ``` 這邊我們預期在點擊`<button>`標籤時觸發事件呼叫`button.click`方法,而這個方法會將執行環境的button物件中的`clicked`屬性,切換成`true`,可以打開`listing-4.10.html`來點點看,跑出來的都是紅色,==表示`button.clicked`並沒有被切換成`true`== ![image](https://hackmd.io/_uploads/BJBfBNAOC.png) 我點了五次,這有紅字是因為他有寫`assert`函式去表示,有興趣再去看書裡面的附件B,總之重點就是button這個物件並沒有因為點擊事件而被改動 原因是因為當我觸發事件後執行`button.click`,裡面改變的是`this.clicked`,而這裡的`this`並不是我們前面所想像的`button`物件,在瀏覽器事件處理中,執行背景環境會是目標元素,也就是`<button>`元素,來檢查看看`<button>`的屬性: ![image](https://hackmd.io/_uploads/BJAsuVC_C.png) 多出了一個`clicked: true`,所以我們可以得知==呼叫`button.click`時的`this`是`<button>`而不是`button`物件==。 **使用apply或call方法** JS提供了一套作法,在呼叫函式的同時==明確地指定我想要的物件作為執行背景環境`this`==,透過所有函式都具備的兩種方法`apply`跟`call`。 * 使用apply方法傳入兩個參數 > 第一個是想綁定的物件,第二個是一個陣列包含想傳入的引數 * 使用call方法傳入多個參數 > 第一個是想綁定的物件,後續直接接著想要傳入的引數 ```javascript= function juggle() { var result = 0; for (var n = 0; n < arguments.length; n++) { result += arguments[n]; } this.result = result; } var ninja1 = {}; var ninja2 = {}; juggle.apply(ninja1, [1, 2, 3]); //第二個傳入陣列包含所有引數 juggle.call(ninja2, 5, 6, 7); //第二個開始填入引數 console.log(ninja1.result); //6 console.log(ninja2.result); //18 ``` 在呼叫`juggle`時分別使用`apply`跟`call`綁定`ninja1`跟`ninja2`。 > 題外話,如果已經把引數做成陣列但硬要用`call`就這樣寫: > `juggle.call(ninja2, ...array)`領域展開((? 接下來介紹兩種不同的程式設計方式: * 命令式程式設計 > 通常將一個陣列傳給函式,在用`for`迴圈一一處理每個元素 ```javascript function(collection) { for (var n = 0; n < arguments.length; n++) { //對collection[n]進行操作 } } ``` * 函式型程式設計 > 一次只對一個函式做處理 ```javascript function(item) { //對item進行操作 } ``` 然後談到了陣列的`forEach`方法,對==陣列的每個元素呼叫回呼函式==,他覺得這樣更簡潔,所以接下來要來嘗試製作一個`forEach`: ```javascript= function forEach(list, callback) { for (var n = 0; n < list.length; n++) { callback.call(list[n], n) } } var weapons = [ { type: 'a' }, { type: 'b' }, { type: 'c' } ]; forEach(weapons, function(index) { console.log(this === weapons[index]); //三個true }) ``` 這裡宣告一個`forEach`函式,傳入一個陣列跟想對陣列每個元素做的`callback`,這裡傳入的`callback`會檢查當下的`this`是不是跟傳入的陣列中的元素相同,上面的`callback.call(list[n], n)`最後的`n`表示下面`function`中的`index`,這邊好難懂...但大概知道在做什麼了。 那`apply`跟`call`作用幾乎一樣,用哪一個比較好?取決於想要放進去的引數類型,==如果引數是多個不相干的變數,那就用`call`一個一個放進去==,==但如果引數已經是個陣列,或者很容易將他包成陣列,那用`apply`更好==。 ### 4.3 修復函式背景空間問題 前面使用了`apply`跟`call`方法來解決`this`不符合期望的問題,現在多了兩個選擇:箭頭函式與`bind`方法。 #### 4.3.1 使用箭頭函式來繞過函式背景空間 ==箭頭函式的`this`會在定義時記住當下的環境==,沿用前面按鈕的例子: ```html= <button id="test">Click Me!</button> <script> function Button(){ this.clicked = false; this.click = () => { //使用箭頭函式 this.clicked = true; //這裡的this就會記錄為當下的環境 }; } var button = new Button(); var elem = document.getElementById("test"); elem.addEventListener("click", button.click); </script> ``` 例子打開`listing-4.13.html`,按下按鈕後`button.clicked`值就被改成`true`了,這次`this`就變成了`button`物件而不是`<button>`標籤,因為`button.click`在宣告期用箭頭函式,所以`this`就綁定了當下的環境`button`物件。 講仔細一點就是在==用建構函式`Button`建構`button`物件時,`click`這個方法裡面的`this`就被綁定在這個新建立物件`button`上了==。 **箭頭函式和物件實值(object literal)** `this`參數的值在建立箭頭函式時取得,可能會遇到怪怪的問題,回到前面按鈕的例子,因為整個頁面我們只需要一個按鈕,所以不需要使用建構函式來建立`button`物件,我們可能只會想直接簡單建立一個`button`物件: ```html= <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> ``` 打開`listing-4.14.html`點擊按鈕,又跟之前不一樣了`button`物件的`clicked`值沒有被改成`true`,這時的`this`是`Window`物件。 由於`click`箭頭函式是建立為物件實值上的屬性,這個物件又是建立在全域程式碼中,所以`this`就會是全域的`this`,所以點擊後全域多了一個`clicked`屬性: ![image](https://hackmd.io/_uploads/rkugV80_C.png) #### 4.3.2 使用bind方法 每個==透過`bind`方法建立出一個新的函式==,這個新函式會長一樣,但==背景空間會永久綁定到你指定的物件上==,不管怎麼呼叫他。 ```html= <button id="test">Click Me!</button> <script> var button = { clicked: false, click: function(){ this.clicked = true; } }; var elem = document.getElementById("test"); elem.addEventListener("click", button.click.bind(button)); var boundFunction = button.click.bind(button); </script> ``` 結果可以打開`listing-4.15.html`來查看。 我們註冊了一個點擊事件處理,點擊他會呼叫`button.click.bind(button)`,這裡就永久綁定了`button`物件: ```javascript elem.addEventListener("click", button.click.bind(button)); ``` 所以點擊`<button>`標籤時,就會將`button`物件的`clicked`屬性改為`true`。 然後我們用: ```javascript var boundFunction = button.click.bind(button) ``` 來確認這個新建立的函式跟舊函式的關係。 書中用自己寫的`assert`來觀察, ```javascript assert(boundFunction != button.click,"Calling bind creates a completly new function"); ``` 所以只要他跟原本函式不相同,就會印出這串綠綠的字! ![image](https://hackmd.io/_uploads/S1iu2ICd0.png) 因此==證明了使用`bind`會建立一個新的函式==,不會影響到原來的函式。 ### 4.5 習題 1. 改寫`sum` 題目: ```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 10'); ``` 將題目用上一章的不定參數改寫,使得不用使用`arguments`物件。 我的想法: ```javascript= function sum(...numbers) { return numbers.reduce((total, element) => { return total + element }) } sum(1, 2, 3); //6 ``` 解答用`for`迴圈 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 = getNinja("Yoshi"); ``` `getSamurai`函式使用嚴格模式,所以改了`arguments[0]`也不會影響參數值,所以==變數`samurai`的值會是`"Toyotomi"`== `getNinja`用普通模式,所以`arguments[0]`跟參數`ninja`是雙向連結的,所以==都改成了`"Fuma"`== 3. 哪些檢查會通過: ```javascript= function whoAmI1(){ "use strict"; return this; } function whoAmI2(){ return this; } assert(whoAmI1() === window, "Window?"); assert(whoAmI2() === window, "Window?"); ``` `whoAmI1`是嚴格模式,所以抓到全域環境的`this`會是`undefined` `whoAmI2`普通模式,`this`會抓到全域的`Window`物件 所以第一個==失敗==,第二個==通過==!! 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?"); ``` 第一個==會通過==,`ninja1.whoAnI()`,因為呼叫時有綁定`ninja1`,所以 第二個==會失敗==,`ninja2.whoAmI()`,宣告方法`whoAmI`的時候只是複製了`ninja1.whoAmI`的樣子,也就是`return this`,呼叫時透過`ninja2`來呼叫,所以`this`會是`ninja2` 第三個==會失敗==,跟第二個情況很像,但呼叫時沒有綁定任何物件,會被當成一般函式呼叫,所以`this`會是全域`Window`物件 第四個==會通過==,呼叫`whoAmI.call(ninja2)`用`call`綁定了`ninja2`所以`this`會是`ninja2` 5. 哪些會通過: ```javascript= function Ninja(){ this.whoAmI = () => this; } var ninja1 = new Ninja(); var ninja2 = { whoAmI: ninja1.whoAmI }; assert(ninja1.whoAmI() === ninja1, "ninja1 here?"); assert(ninja2.whoAmI() === ninja2, "ninja2 here?"); ``` 因為建構函式`Ninja`裡面已經用箭頭函式宣告了,所以`this`永久綁定在新建立的物件上,也就是`ninja1`這個物件,之後不管怎麼呼叫他的`this`都會是`ninja1`, 所以==第一個會通過==,==第二個會失敗== 6. 哪些會通過: ```javascript= function Ninja(){ this.whoAmI = function(){ return this; }.bind(this); } var ninja1 = new Ninja(); var ninja2 = { whoAmI: ninja1.whoAmI }; assert(ninja1.whoAmI() === ninja1, "ninja1 here?"); assert(ninja2.whoAmI() === ninja2, "ninja2 here?"); ``` 透過`bind`永久將`this`綁定在新建構的物件`ninja1`上,所以不管怎麼呼叫`this`都是`ninja1` 所以==第一個會通過==,==第二個會失敗== 再來要講到閉包了,非常刺激。 ## Chapter 5 大師級函式:閉包與範圍 ### 5.1 瞭解閉包 簡單來說,閉包的特性讓函式能夠存取及操作函式外部的變數,要理解他透過程式碼會比較清楚,來看看一個簡單的閉包: ```javascript= var outerValue = "ninja"; function outerFunction() { if (outerValue === "ninja") { console.log("I can see the ninja.") } } outerFunction(); //"I can see the ninja." ``` 全域中定義一個`outerValue`變數和`outerFunction`函式,接著呼叫`outerFunction`時能夠存取到外部的`outerValue`,雖然很直觀,但這就是不知不覺利用了閉包的特性! 另一個例子: ```javascript= var outerValue = "samurai"; var later; function outerFunction() { var innerValue = "ninja"; function innerFunction() { if (outerValue === "samurai") { console.log("I can see the samurai.") } if (innerValue === "ninja") { console.log("I can see the ninja.") } } later = innerFunction; } outerFunction(); later(); //"I can see the samurai." //"I can see the ninja." ``` 先執行了`outerFunction`,但什麼都還不會印出,因為`innerFunction`還沒執行,而他被賦值給了`later`,所以透過`later`來呼叫`innerFunction`時,本來`outerFunction`的作用環境應該已經消失,但是`innerFunction`還是可以透過閉包的特性來取用到`innerValue`! 書裡面==形容函式在定義時將函式跟他所處的範圍內的變數裝在一個『安全氣泡』內==,這樣函式執行時就能取用他需要的所有東西,所有==裝在裡面的東西都擁有相同壽命==,與函式本身相同。 :question:我在想這個壽命應該是指整個JS運行的時間,感覺像是頁面重新刷新壽命才結束。 ![image](https://hackmd.io/_uploads/Hki11Nn9A.png) ### 5.2 開始使用閉包 #### 5.2.1 摹擬私有變數 許多程式語言都有私有變數,但JS並沒有私有變數,藉由閉包可以做出類似的功能: ```javascript= function Ninja() { var feints = 0; this.getFeints = function () { return feints; }; this.feint = function () { feints++; }; } var ninja1 = new Ninja(); ninja1.feint(); console.log(ninja1.feints); //undefined console.log(ninja1.getFeints()); //1 var ninja2 = new Ninja(); console.log(ninja2.getFeints()); //0 ``` 建立出`Ninja`物件`ninja1`,接著呼叫`ninja1.feint()`來將內部的`feints`加一,此時我們想直接取得`ninja.feints`卻得到`undefined`,必須透過內部的另一個方法`ninja.getFeints()`來取得,如此一來就無法從外部影響裡面的變數,就實現了類似私有變數的功能了! ![image](https://hackmd.io/_uploads/H1nZmN3cR.png) 所以這邊也是因為閉包,使得物件內部的方法能夠取得內部的變數,而外部任何方法就沒辦法。 #### 5.2.2 伴隨回呼來使用閉包 ```html= <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."); } }, 10); } animateIt("box1"); </script> ``` 看成果參見檔案`listing-5.4.html`。 `animateIt()`函式用來向右下推動放入的元素`box1`,每10毫秒推`1px`,直到推到`100px`後停止並檢查能不能取得`tick`、`elem`及`timer`這三個變數,這邊還看不出閉包的好處,因為就算將這三個變數拉到全域,效果依然正常,這樣為什麼不將他放到全域呢? 我們想像今天要做兩個盒子的動畫`box1`跟`box2`,如果這些變數在全域,`animateIt("box1")`跟`animateIt("box2")`就會共用到剛剛的三個變數而造成混亂,要解決變成要多令三個變數`tick2`、`elem2`跟`timer2`,如果沒有閉包,我要做幾個動畫就要多令幾組變數。藉由使用閉包,我們可以將每組變數限制在自己的氣泡中不會與外界互相影響: ![image](https://hackmd.io/_uploads/Sk2jYE25R.png) ### 5.3 使用執行背景空間(execution context)追蹤程式執行 之前有提到過JS程式有兩種類型,全域程式與函式程式,而裡面的敘述句都會在特定的執行背景空間執行;另一件事就是JS是單執行緒,一次執行一段程式碼,所以當某個函式被呼叫(假設是函式A),就必須先停止當前的執行背景空間,建立出新的函式執行背景空間A,直到A執行完畢,完成他的任務後,這個執行背景空間A通常會被丟棄,然後恢復到剛剛暫停的背景空間繼續。 這些空間都會暫時被記錄下來,包括執行中、等待中,最簡單的方法就是使用堆疊,被稱為執行背景空間堆疊(呼叫堆疊)。 ```javascript= function skulk(ninja) { report(ninja + " skulking"); } function report(message) { console.log(message); } skulk("Kuma"); skulk("Yoshi"); ``` ![image](https://hackmd.io/_uploads/rk2BgS39R.png) 1. 從全域執行背景空間開始執行 2. 呼叫了`skulk`時,新的函式背景空間放到堆疊中並暫停全域執行背景 3. 呼叫`report`時又一個新的函式背景空間放到堆疊中且暫停`skulk`執行空間 4. `report`執行完後他的執行背景彈出而恢復`skulk`執行背景 5. `skulk`執行完一樣執行背景彈出,然後恢復全域執行背景空間 打開`listing-5.5.html`可以使用除錯工具來看看堆疊: ![image](https://hackmd.io/_uploads/r1RIMB3cR.png) ### 5.4 使用字彙環境來追蹤識別項 JS引擎內部的結構,用來==追蹤識別項到特定變數的對應(map)==: ```javascript var ninja = "Hattori"; console.log(ninja); ``` 當`console.log`要存取變數`ninja`時,就會到字彙環境去查找。 > 書裡寫說通常大家稱這為範圍(scope)。 而字彙環境可以與一些JS程式碼的特定結構有關聯,例如一個函式、一段程式碼或一個try-catch的catch區塊,這些例子都可以有自己的識別相對應。(我的解讀是可以將這些區塊就當成一個字彙環境的範圍) #### 5.4.1 巢狀程式(code nesting) ==一個程式碼結構能包在另一個程式碼結構中==: ```javascript= var ninja = "Muneyoshi"; //skulk函式在全域程式中 function skulk() { var action = "skulking"; // report函式在skulk函式內部 function report() { var reportNum = 3; // for迴圈在report函式內部 for(var i = 0; i < reportNum; i++) { console.log(ninja + " " + action + " " + i); } } report(); } skulk(); ``` ![image](https://hackmd.io/_uploads/r1WBn5350.png) 每次程式碼執行時,這個程式碼結構都會生成一個字彙空間;在內部的程式碼結構可以存取外部程式碼結構的變數。 #### 5.4.2 巢狀程式與字彙環境 除了追蹤區域內的內容外,每個字彙環境還必須追蹤外部的字彙環境,如果當前環境中找不到識別項,就往外一層搜尋,直到找到匹配的變數為止,但如果一直找到了全域環境還是沒有,就會得到參照錯誤(reference error)。 範例: ```javascript= var ninja = "Muneyoshi"; function skulk() { var action = "Skulking"; function report() { var intro = "Aha!"; console.log(intro); console.log(action); console.log(ninja); } report(); } skulk(); // "Aha!" // "Skulking" // "Muneyoshi" ``` `report`函式由`skulk`函式呼叫,`skulk`由全域呼叫,建立了三個字彙環境,且環境中都包含該空間所定義的識別項對應,例如: * 全域環境保存`ninja`跟`skulk`的對應 * `skulk`環境保存`action`跟`report`的對應 * `report`環境保存`intro`的對應 在這個例子裡,`report`嘗試存取外部函式`skulk`的變數`action`以及全域變數`ninja`,為了讓他可以做到這點,JS做法是以函式作為頭等物件,每建立一個函式時,指向該函式所屬的字彙環境會儲存在`[[Environment]]`的內部屬性中(不能直接被存取或操作)。 所以上面例子中,`skulk`被呼叫,建立`skulk`的環境,外部的環境為全域環境(建立`skulk`函式時的環境),呼叫`report`時,外部環境被設置為`skulk`環境。 當`console.log(intro)`執行時,必須解析`intro`識別項,JS引擎會先檢查當下的執行背景空間環境(`report`環境)。然後就找到了,結束。 第二行`console.log(action)`執行時,再一次檢查當前的執行背境空間環境,這次`report`環境中找不到`action`的識別項了,所以這次要找`report`環境的外部還境:`skulk`環境,然後就找到了!結束。 找`ninja`的方式也是類似的過程。 ### 5.5 暸解JS的變數類型 JS中可用三個關鍵字定義變數:`var`、`let`跟`const`,其可變性與字彙環境有所不同。 #### 5.5.1 變數的可變性 以可變性來區分變數,會把`const`放一邊,`var`跟`let`放另一邊,使用`const`定義的變數都不可變,只能被設置一次;用`var`跟`let`定義的變數則是普通變數,可以根據需求改變多次。 ##### const變數 宣告時必須提供一個初始值,之後就不能指派新的值給他。 用`const`通常有兩個目的: * 指定一些不應重新指派的變數 * 參照到一個固定的值 > 書中舉例浪人團最多就由幾位浪人組成:MAX_RONIN_COUNT,用個有意義的名稱,而不是234這樣的數字。這讓程式更容易理解和維護。 `const`變數的行為: ```javascript= const firstConst = "Jeremy"; console.log(firstConst); //"Jeremy" firstConst = "Watson"; //TypeError: Assignment to constant variable. ``` 企圖指派新的值給`const`變數會報錯。 --- ```javascript= const secondConst = {}; secondConst.weapon = "Watson"; console.log(secondConst.weapon); //"Watson" ``` 無法指派全新的物件給`secondConst`,但可以修改它。 --- ```javascript= const thirdConst = []; console.log(thirdConst.length); //0 thirdConst.push("Fang"); console.log(thirdConst.length); //1 ``` 修改的規則也適用於陣列。 ==結論:`const`變數的值必須在初始化時設置,之後就不能指派新的值給他,但我們可以修改現有的值,只是不能完全覆寫他。== #### 5.5.2 用來定義變數的關鍵字與字彙環境 `var`、`let`跟`const`可以根據他們與字彙環境的關係(他們的範圍)來分類,將`var`方一邊,`let`跟`const`放一邊。 ##### 使用`var`關鍵字 使用`var`宣告時,變數是定義在最鄰近的函式或全域字彙環境中(程式區塊將被忽略)。 > 不要這麼做,非常可怕!! > [name=Ajay] ```javascript= var globalNinja = "Jeremy"; function reportActivity() { var functionActivity = "eat"; for(var i = 1; i < 3; i++) { var forMessage = globalNinja + " " + functionActivity; console.log(forMessage); console.log("current count: ", i); } console.log(i); //在for迴圈外取i } reportActivity(); // "Jeremy eat" // "current count: 1" // "Jeremy eat" // "current count: 2" //3 if (typeof functionActivity === "undefined" && typeof i === "undefined" && typeof forMessage === "undefined") { console.log("We cannot see function variables outside of a function.") } //"We cannot see function variables outside of a function." ``` 前面看起來一切正常,`for`迴圈中正常存取區塊變數`i`跟`forMessage`、函式變數`functionActivity`和全域變數`globalNinja`。但奇怪的地方在於`console.log(i)`,因為使用`var`宣告的變數,會註冊在最鄰近的函式或全域字彙環境中,無視了程式區塊結構,所以此時會有三個字彙環境: * 全域環境,註冊`globalNinja`變數。 * reportActivity環境,包含`functionActivity`、`i`跟`forMessage`三個變數,因為他們都是用`var`宣告的,而這是他們最鄰近的函式環境。 * `for`區塊環境,空的,因為裡面變數`i`用了`var`宣告就忽略了程式區塊。 因為太奇怪了,所以ES6版本提供了兩個新的變數宣告關鍵字:`let`跟`const`。 ##### 使用`let`和`const`來指定區塊範圍內的變數 因為`var`實在太詭異了,所以和他相比,`let`跟`const`就顯得更加直覺,因為他把變數定義在最靠近的字彙環境中,用`let`和`const`重寫之前的例子: ```javascript= const GLOBAL_NINJA = "Jeremy"; function reportActivity() { const functionActivity = "eat"; for(let i = 1; i < 3; i++) { let forMessage = GLOBAL_NINJA + " " + functionActivity; console.log(forMessage); console.log("current count: ", i); } if(typeof i === "undefined" && typeof forMessage === "undefined") { console.log("Loop variables not accessible outside the loop.") } } reportActivity(); // "Jeremy eat" // "current count: 1" // "Jeremy eat" // "current count: 2" // "Loop variables not accessible outside the loop." if(typeof functionActivity === "undefined" && typeof i === "undefined" && typeof forMessage === "undefined") { console.log("We cannot see function variables outside of a function."); } //"We cannot see function variables outside of a function." ``` 這次一樣會有三個字彙環境: * 全域環境,變數`GLOBAL_NINJA`所在的環境。 * `reportActivity`環境,定義了變數`functionActivity`。 * `for`區塊環境,定義了變數`i`和`forMessage`。 這次所有的變數都註冊在離自己最近的字彙環境中,這樣就能與其他類C語言支援相同作用範圍規則,所以之後書中幾乎都用`const`跟`let`取代`var`。 #### 5.5.3 在字彙環境中註冊識別項(Chris筆記了Hoisting) 一般來講,我們會認為JS程式碼以直接逐行執行: ```javascript firstRonin = "Jeremy"; secondRonin = "Watson"; ``` 將`Jeremy`指派給`firstRonin`,接著將`Watson`指派給`secondRonin`,非常直覺。 看看另一個例子: ```javascript const firstRonin = "Jeremy"; check(firstRonin); //"The ronin was checked!" function check(ronin) { if(typeof ronin !== "undefined"){ console.log("The ronin was checked!") } } ``` 這次一樣先指派`Jeremy`給`firstRonin`,接著用識別項`firstRonin`當作引數來呼叫`check`函式,Wait Wait Wait,如果代碼是逐行執行,能夠呼叫執行`check`嗎?實際上程式正常執行了,表示JS對我們在哪裡定義函式並不太在意,可以在呼叫之前或之後宣告。 ##### 註冊識別項的過程 上述例子就能證明JS並不是逐行執行,其實JS程式碼的執行分成兩個階段,==第一階段==是每次建立新的字彙環境時啟動。這個階段程式碼不會執行,但==JS引擎會讀取並註冊當前字彙環境中所有已宣告的變數及函式==。==第二階段==是JS執行階段,==接在第一階段完成後開始==,確切的行為要==取決於變數類型和環境類型==。 過程: 1. 建立函式環境,包含其所含的函式參數及引數的隱含式`arguments`識別項,如果處理的是非函式環境,則跳過這個步驟。 2. 如果建立的是全域環境或函式環境,會先掃描(不會進到其他函式裡面)目前程式碼以==找出所有函式宣告==(不包含函式表達式或箭頭函式)。對每個發現的函式宣告都建立新的函式並將他們綁定到該環境中同名的識別項,如果此識別項已經存在,就覆寫他的值。如果處理的是區塊環境則會跳過這個步驟。 3. 掃描目前程式碼以==找出變數宣告==: > 用`var`宣告的變數,且在當下函式和全域環境中,並定義在其他函式之外(但可以在區塊中),這些變數可以被找到。 > 用`let`跟`const`宣告的變數,如果定義在其他函式與區塊之外也可以被找到。 如果在區塊環境中,只會找到當前區塊中用關鍵字`let`跟`const`宣告的變數。 找到這些變數後,如果識別項不存在環境中,便註冊識別項且設定初值為`undefined`,如果識別項存在,則將值指派給它。 > 這邊超難懂:cry: > [name=Jeremy] ##### 在函式宣告前呼叫他 直接來實驗: ```javascript= console.log(fun); //fun() {} console.log(myFunExp); //undefined console.log(myArrow); //undefined function fun(){}; var myFunExp = function(){}; var myArrow = (x) => x; ``` 因為`fun`是以函式宣告的方式定義,所以可以在定義前就存取他,前面有提到,用函式宣告定義的函式以其識別項的註冊,是在JS程式碼執行之前建立的,所以`console.log(fun)`以前函式`fun`就已經存在了。 這個特性僅限函式宣告,函式表達式與箭頭函式不會有這個現象,所以我們無法在定義前取得`myFunExp`跟`myArrow`。 ###### 覆寫函式 ```javascript= console.log(fun); //fun(){} var fun = 3; console.log(fun); //3 function fun(){}; console.log(fun); //3 ``` 第一次檢查中,`fun`是一個函式,第二及第三次`fun`則是數字`3`。 在前面的步驟二中遇到了`fun`並將他註冊為一個函式,所以當程式碼開始執行時第一次的`console.log(fun)`印出了函式,之後執行了`var fun = 3`,將`3`指派給識別項`fun`,此時便失去了對函式的參照,從此以後`fun`指向的就是數字`3`。 ### 5.6 探索閉包的運作方式 用執行背景空間及字彙環境來解釋閉包背後的原理。 #### 5.6.1 重新檢視如何用閉包來摹擬私有變數 ```javascript= function Ninja() { var feints = 0; this.getFeints = function(){ return feints; } this.feint = function(){ feints++; } } var ninja1 = new Ninja(); console.log(ninja1.feints); //undefined ninja1.feint(); console.log(ninja1.getFeints()); //1 var ninja2 = new Ninja(); console.log(ninja2.getFeints()); //0 ``` 當我們使用`new`建立`Ninja`物件時,也會建立一個新的字彙環境,用來追蹤建構函式內的區域變數,所以這邊用來追蹤變數`feints`的`Ninja`環境會被建立。 然後我們建立了兩個函式`getFeints`跟`feint`,他們都具有對`Ninja`環境的參照。 當每次使用建構函式`Ninja`建構物件時,都會建立一個新的`Ninja`字彙環境,並存放他自己的變數`feints`。 因為我們在函式`getFeints`中沒有定義任何變數,所以`getFeints`的字彙空間是空的,當我們呼叫他時要找`feints`找不到,所以就會往外層找,在這邊就是`Ninja`環境,然後就找到了`feints`。 這些私有變數不是物件中的私有屬性,而是建立在建構函式中由物件方法所控制的變數。 #### 5.6.2 留意私有變數 ```javascript= function Ninja() { var feints = 0; this.getFeints = function(){ return feints; }; this.feint = function(){ feints++ }; } var ninja1 = new Ninja(); ninja1.feint(); var imposter = {}; imposter.getFeints = ninja1.getFeints; console.log(imposter.getFeints()); //1 ``` 結果我們把`ninja1.getFeints`方法指派給新的`imposter`物件,透過`imposter`來呼叫`getFeints`函式,結果是能存取到`ninja1`中的`feints`值。 :question:還不知道為什麼要這麼做 #### 5.6.3 重新檢視閉包和回呼範例 ```html= <div id="box1">First Box</div> <div id="box2">Second 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"; } 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."); } }, 10); } animateIt("box1"); animateIt("box2"); </script> ``` 這次來探討字彙環境,當呼叫`animateIt("box1")`跟`animateIt("box2")`時,分別建立了兩個字彙環境,分別存取了自己的變數`elem`、`tick`跟`timer`,所以當每個函式中的`setInterval`到期後就會將自己環境中的`tick`加一,就無須自行維護變數間的對應關係。 # PART 3 深入物件世界,以強化你的程式碼 ## Chapter 7 以原型來實現物件導向 前言:所謂原型就是在尋找一個屬性時,可以被委派這項任務的物件。(供殺毀?) 他扮演著傳統物件導向程式語言中的類別的角色。 我也看不懂,直接進入內容吧。 ### 7.1 瞭解原型 直接用==物件實質==建立物件: ```javascript= let obj = { prop1: 1, prop2: function(){}, prop3: {} } ```