# W_JS忍者 [toc] # Watson infor >我會閱讀所有章節,依據章節節錄重點,也當作個人複習 # page20 個人理解是一個資料結構,就像連連看 ex ```html= <html> <head> <meta> </head> <body> ``` 這樣就會向下圖 ![Screenshot from 2024-05-23 14-50-33](https://hackmd.io/_uploads/ByE8Pv3QR.png) document是指目前網頁的文檔對像模型(DOM),它是一個全域對象,用於存取和操作HTML文檔的內容和結構。document物件提供了一系列的方法和屬性,使你能夠動態地取得和修改網頁的內容。 --- # page22 全域程式 # page23-24 - 瀏覽器在頁面建立階段遇到script節點 就會先停止html 並開始執行js - js執行完script最後一行,便會繼續處理html來建立其他得DOM節點 ### 第一次讀書會 CH.1 BY Chris said 複雜名詞物件導向 複雜動詞function programming CH.2 習題 一個是只能執行一次 addevenlistener是可以除鋰多個事件 keyword 觀察者模式 WEB應用程式得生命週期有那兩個階段:頁面建立 事件處理 chris 比喻玩switch 開機與按按鈕 如果硬要說關機算一個 也就是有三個 but? 圖形化界面 這就是早期沒有畫面互動 到有畫面互動 所以跟歷史有關 有趣 js是非同步優先與鹽 是為了瀏覽器 忍者在寫得時候是ES7之前 所以沒有async await proxy就像是東尼使塔客跟鋼鐵衣關西 proxy就像鋼鐵衣 --- # CH.3 初探頭等函式:定義與引述 ==35.== 在JS,函式是頭等物件,或是稱他們為頭等公民。 ## 3.1 使用函式與否得差異為何? ==36.== 因為他是主要得模組化執行單元。我們位頁面編寫得所有程式碼都會除存在函式內。 **js物件具有特性:** - 藉由實質:{} - 可以指派給變數、陣列、其他物件屬性 ```javascript= var ninja = {}; ninjaArray.push({}); ninja.data = {}; ``` ==37.== - 可以作為引述傳遞給函式 ```javascript= function hide(ninja){ ninja.visibility = false; } hide({}); ``` - 可作為函式回傳值 ```javascript= function returnNewNinja() { return {}; } ``` - 可動態建立和指派得屬性 ```javascript= var ninja = {}; ninja.name = "Hanzo"; ``` ### 3.1.1 函式作為頭等物件 js函式擁有物件所有功能,因此也可被當成是物件般來處理 - 由實質建立 ```javascript= function ninjaFunction() {} ``` - 可以指派給變數、陣列、其他物件屬性 ```javascript= var ninjaFunction = function() {}; ninjaArray.push(function(){}); ninja.data = function(){}; ``` ==38.== - 作為引數傳遞給其他函式 ```javascript= function call(ninjaFunction){ ninjaFunction(); } call(function(){}); ``` - 作為函式回傳值 ```javascript= function returnNewNinjaFunction() { return function(){}; } ``` - 他們擁有可動態建立和指派屬性 ```javascript= var ninjaFunction = function(){}; ninjaFunction.name = "Hanzo"; ``` 所以物件可以做得事情,==函式都可做到==,函式也是物件,但卻是可以被呼叫得物件 > **js中得函式型程式設計** > 這種設計風格的重點是透過撰寫還是來解決問題,此設計可以幫助我們寫出更易於測試、擴展和模組化得程式碼 頭等物件得其中一項特性,是他們可以作為==引數傳遞給函式==。對函式而言,這代表我們將一個函式作為引數傳遞給另一個函式,而在稍後得某個時間點,它可能會呼叫這個被傳入得函式,這是==回呼函式==得概念 ### 3.1.2 回呼函式 ==39.== 這個術語源自這樣得事實:我所建立得函式會在稍後某個適當得時機點,由其他程式碼==回呼(call back)== ```javascript= function useless(ninjaCallback) { return ninjaCallback(); } ``` 這段"無用"得code展示了==將函式作為參數傳遞==給令一個函式,並隨後透過傳遞參數呼叫該函式的能力,下面更多示範 > 程式列表3.1 一個簡單得回呼範例 ```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"); ``` ==40.== 此為執行後結果 ![image](https://hackmd.io/_uploads/ryomGJAuC.png) ==41.== 如圖詳細講解,getText函式作為引數傳給useless函式,這表示useless函式內部可以藉由ninjaCallback參數來指向getText函式。然後,藉由呼叫ninjaCallback(),我們執行了getText函式,getText函式被我們當作引數傳遞,而被useless函式呼叫 ![image](https://hackmd.io/_uploads/SkAalJAOA.png) JS最重要得特性之一,就是可以在程式中==任何地方建立函式==,這個特性還可在一個函式並不需要從程式碼中許多地方來引用時,避免全域命名空間被不必要得命名污染 ==42.== 使用對比器進行排序sort [MDN.sort](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) sort對陣列進行排序並回傳此陣列 在下方程式碼中將讓排序演算法用回呼得方式來呼叫這個函式,而當排序演算法需要進行比較時,就會呼叫這個回呼函式。如果傳入的值應該反轉順序 > 為了比較數字而不是字串,比較函式可以僅僅利用 a 減 b。以下函式將會升冪排序陣列 by MDN ```javascript= var values = [0, 3, 2, 5, 7, 4, 8, 1]; values.sort(function(value1, value2){ return value1 - value2; }); //result[0, 1, 2, 3, 4, 5, 7, 8] ``` ==43.== ## 3.2 函式作為物件得有趣之處 (Chris 表示不有趣 :) 我們可以將==屬性附加==到函式上 ```javascript= var ninja = {}; ninja.name = "hitsuke"; var wieldSword = function(){}; wieldSword.swordType = "katana"; ``` 可以讓這像功能派上用場得場景: - 把多個函式儲存在集合中,這可以讓我們輕鬆得管理彼此護有關連得函式。 - 讓函式可以記住之前計算所得到得舊值,進而提高後續呼叫時得執行效能 ### 3.2.1 儲存函式 #### 3.2 將一組具唯一性得函式儲存起來 當管理回呼函式集合時,我們不想看到任何重複,因為單一事件將導致對同一個回呼函是的多次呼叫。可以將函式儲存在陣列中,然後對陣列資料像循環一次去檢查重複得函式,但這種作法==在校能上表現不佳==,下方將講解解決辦法:==??== ```javascript= var store = { nextId: 1, // 初始化 nextId 為 1 cache: {}, // 初始化 cache 為空對象 add: function(fn) { // 定義 add 方法 if (!fn.id) { // 檢查函數是否已經有 id 屬性 fn.id = this.nextId++; // 分配新的 id 給函數,並將 nextId 增加 1 this.cache[fn.id] = fn; // 將函數存儲在 cache 中,鍵為 id return true; // 返回 true 表示函數已成功添加 } } }; function ninja() {} // 定義一個名為 ninja 的函數 assert(store.add(ninja), "Function was safely added."); // 添加 ninja 函數到 store 中,返回 true,表示添加成功 assert(!store.add(ninja), "But it was only added once."); // 再次嘗試添加 ninja 函數,由於已經有 id,所以返回 undefined,表示沒有添加成功 ``` > note:add()可相加,可添加 ==??== ==45.== ### 3.2.2 自我記憶函式 > 建立一個函式,使奇能夠記住之前得計算值 > 我們可以將結果與函式參數一起儲存,當使用同一組參數進行另一次呼叫時,我們可以回傳先前儲存得結果,而不用重新計算。可提高效能 下方將以計算質數為例子 #### 程式列表3.3 記憶先前已計算得值 ```javascript= function isPrime(value) { if (!isPrime.answers) { isPrime.answers = {}; // 如果 isPrime 沒有 answers 屬性,則初始化為空對象,作為緩存 } if (isPrime.answers[value] !== undefined) { return isPrime.answers[value]; // 如果緩存中已存在該數值的計算結果,則直接返回 } var prime = value !== 1; // 1 不是質數 for (var i = 2; i < value; i++) { if (value % i === 0) { prime = false; // 如果能被 i 整除,則不是質數 break; // 結束循環 } } return isPrime.answers[value] = prime; // 將計算結果存入緩存並返回 } assert(isPrime(5), "5 is prime!"); // 測試 5 是不是質數 assert(isPrime.answers[5], "The answer was cached!"); // 確認 5 的計算結果已被緩存 ``` ==47.== 這種方法有兩個主要優點 - 藉由取得之前得值,讓終端使用者在呼叫函式時得到效能上得改善 - 它會在幕後運行,不須執行特殊請求或做任何初始化,來使這一切發生 缺點部份 - 任何暫存都是花費記憶空間來換取效能 - 純粹主義者可能認為暫存是一個不應該與業務邏輯==??==放在一起得事物,一個函式或一個方法應該只做一件事並且做好 - 很難對這樣得演算法進行負載測試或測量其效能,因為結果取決於之前對於函式提供得輸入值 ## 3.3 定義函式 記住,作為頭等物件,函式就像字串和數字這樣得值一樣,是可以在語言中使用得值。 JS提供了幾種定義函是的方法,他們可以分為四類 - 函式宣告和函式表達式,了解他們差異可以幫助我們知道函是在什麼時候被呼叫: ```javascript= function myFun(){ return 1;} ``` ==48.== - 箭頭函式,于ES6得新功能,解決回呼函式得常見問題,也更加簡潔得來定義函式 ```javascript= myArg => myArg*2 ``` - 函式建構式,一種不太常用得方法,讓我們能夠從一個可以動態產生得字串中,動態建立一個新的函式 ```javascript= new Function('a', 'b', 'return a + b') ``` - 生成器函式,ES6中加入到JS得功能,讓我們建立以往不同得函式,是可以在應用程式執行時退出和充新加入他們,同時在這些不同得進入點之間保持它門得變數值 ```javascript= function* myGen(){ yield 1; } ``` ### 3.3.1 函式宣告和函式表達式 在js中,兩種最常用得函式定義方法,是使用函式宣告和函式表達式 #### 函式宣告 ![image](https://hackmd.io/_uploads/SymV_I0O0.png) - 必要function關鍵字開頭 - 必要得函式名稱 - 必要括號 - 逗號分隔參數名稱表 - 大括號,內部為函式主體,會置入零個或多個敘述句 每個函式宣告都必須符合使形式之外,還有一個條件:一個函式宣告必須獨立放置,作為一個單獨得js敘述句 ### 程式列表3.4 函式宣告範例 ```javascript= function samurai() { return "samurai here"; } function ninja() { function hiddenNinja() { return "ninja here"; } return hiddenNinja(); } ``` ```javascript= function ninja() { function hiddenNinja() { return "ninja here"; } return hiddenNinja(); } ``` 定義在另一個函式中得函式,這完全正常。 >在函式放在其他函適中,可能會引起一些關於範圍和識別像,解析得複雜問題,但現在先跳過他們,因為我們將在第五章中詳細討論 ### 函式表達式 函是在js頭等物件,這代表他們可以藉由實值建立、指派給變數和屬性,並用做參數或回傳值傳給其他函式。 ```javascript= var a = 3; myFunction(4); ``` 我們也可以在相同得位置使用函式實值: ```javascript= var a = function() {}; myFunction(function(){}); ``` 函式表達式定義:總是作為敘述句得函式,例如指派表達式得右值(翻譯問題),或作為另一個函式得參數 下面程式展示函式宣告與函式表達式之間差異 ==51.== |程式列表3.5 函式宣告與函式表達式| ```javascript= function myFunctionDeclaration(){ function innerFunction() {} } var myFunc = function(){}; myFunc(function(){ return function(){}; }); (function namedFunctionExpression () { })(); +function(){}(); -function(){}(); !function(){}(); ~function(){}(); ``` 讀解釋@ ==52.== 函式宣告與函式表達式差異: - 函式宣告必須要加上名稱,因為他是獨立得,由於函式基本要求是它必須是可被呼叫得,所以唯一呼叫他的方式就是藉由名稱來呼叫 - 函式表達式名稱可有可無,他是js表達是一部份,如果呼叫,我們可以先給函式表達式指派給一個變數,之後可以使用該變數來呼叫 ### 立即函式 ![image](https://hackmd.io/_uploads/rJhpDNkK0.png) 左為標準函式呼叫,右為立即呼叫得函式表達式 ==??== 識別項 ==??== - 左:在最基本得函式呼叫中,我們是透過識別項來指定呼叫得函式 - 右:首先建立一個函式,然後立即呼叫這個新建立得函式,這種作法稱為立即呼叫得函式表達式(aka IIFE:immediate invoked function expression) ==53.== > 圍繞函式表達式得括號 重點擷取 > - 括號包裹函式表達式是語法上的需求。 > - JavaScript解析器需要區分函序宣告和函數表達式。 > - 如果沒有括號,解析器會將獨立語句中的函式視為沒有名稱函式宣告,導致錯誤。 > - 括號告訴解析器這是函式表達式而敘述句。 > - 另一種方法是直接用括號立即調用的函數。 在==51.==的程式碼中展示得4個表達式也是相同概念 ```javascript= +function(){}(); -function(){}(); !function(){}(); ~function(){}(); //也可使用一元運算子來區分,無須在函式表達式周圍使用括號,而這些一元運算子得結果也不會處存在任何地方。 ``` ==54.== ## 3.3.2 箭頭函式 🥷:箭頭函式是在ES6加入 箭頭函式是表達式得,也是js語法糖 語法糖:更簡短、簡潔得寫法 以下是案例比較,這個例子把回呼函式表達式傳給陣列物件sort ```javascript= //一般函式表達式 var values = [0, 3, 2, 5, 7, 4, 8, 1]; values.sort(function(value1,value2){ return value1 – value2; }); //箭頭函式,相對簡潔 (lambda!!!!) var values = [0, 3, 2, 5, 7, 4, 8, 1]; values.sort((value1,value2) => value1 – value2); ``` 在上方差異中可以看到 - 沒有function關鍵字 - 沒有大括號 - 沒有return - 新增箭頭運算子`=>` 簡單解讀 ```javascript= param => expression ``` 接受一個參數param並返回一個表達是expression > 程式列表3.6 比較箭頭函式與函式表達式 ```javascript= var greet = name => "Greetings " + name; assert(greet("Oishi") === "Greetings Oishi", "Oishi is properly greeted"); var anotherGreet = function(name) { return "Greetings " + name; }; assert(anotherGreet("Oishi") === "Greetings Oishi", "Again, Oishi is properly greeted"); ``` --- ![image](https://hackmd.io/_uploads/Bypl6IkFR.png) - 零或多個參數需要使用括號,只有一個參數括號可有可無 - 必要箭頭運算子 - 如果箭頭函式得主體只有一個表達式,那即是函式回傳值 - 如果主體是一段程式區塊,就和標準函式一樣,若沒有return敘述句,則回傳值是undefined;反之,回傳值就是return敘述句所回傳的值 ==56.== ## 3.4 引數(argument)與函式參數(parameter) - 參數是我們在函式定義中所列出得變數 - 引數是當我們呼叫函式時傳遞給他的值 ==57.== ![image](https://hackmd.io/_uploads/HyKheukY0.png) 閱讀@ 當引數作為呼叫函式得一部分時,這些引數按指定得順序分配給函式定義中得參數,第一位引數分配給第一個參數,第二引數給第二參數 以此類推 ==58.== ![image](https://hackmd.io/_uploads/ByKyGdytR.png) 解釋圖內文@ ### 3.4.1 不定參數 🥷:ES6加入 ==59.== > 程式列表3.7 使用不定參數 ![image](https://hackmd.io/_uploads/HJfFLdyF0.png) 在參數名稱之前加上省略符號`...`便會將其轉換為==不定參數得參數陣列==,剩餘得傳入引數將會包含在其中 ==??== 在圖中multiMax使用了四個引數來呼叫:multiMax(3,1,2,3),第一個引數值3被指派給maltiMax函式得first。由於第二個參數是==不定參數==,所有剩餘得值(1,2,4)都放在一個新陣列remainingNumber中,接者,我們對陣列做降冪排序,在取出最大得數 🥷:只有最後一個函式參數可以是不定參數,若放置在非最後得參數將會得到警告 `SyntaxError: parameter after rest parameter.` ==60.== ## 3.4.2 預設參數 🥷:ES6加入 ![image](https://hackmd.io/_uploads/BJ2rEo1FR.png) 當我們呼叫函式並且忽略相應對應得引數值時,正如範例中得Fuma、Yoshi和Hattori,次時就會預設action為skulking,如果有使用傳入值,那預設值就會被取代 ## 總結 # CH4. ## CH5 閉包與範圍 ### 名詞定義 #### 閉包: 函式操作在函式不的變數 依照讀書會結論,閉包是一種工具,主要特徵是函式裡面又包裹了另一個函式,根據書本案例,將函式作為建構式去撰寫。其作用可偵測和使用外部得變數, #### 私有變數:簡單來說就是變數建立在函式裡面,外部無法直接呼叫。 - 安全不會被修改 - 易維護,不會被外部影響 - 提高模組化設計 #### 執行背景空間堆疊: 簡單來說就是程式執行是有順序的,初期是執行全域背景空間,如果有呼叫就會依序排列並且印出。直到一切皆空 ```javascript= // 模擬背景執行堆疊行為的範例 function firstFunction() { console.log("進入 firstFunction"); secondFunction(); console.log("退出 firstFunction"); } function secondFunction() { console.log("進入 secondFunction"); thirdFunction(); console.log("退出 secondFunction"); } function thirdFunction() { console.log("進入 thirdFunction"); console.log("thirdFunction: 執行中..."); console.log("退出 thirdFunction"); } console.log("程式開始"); firstFunction(); console.log("程式結束"); //印出結果 程式開始 進入 firstFunction 進入 secondFunction 進入 thirdFunction thirdFunction: 執行中... 退出 thirdFunction 退出 secondFunction 退出 firstFunction ``` #### 字彙環境 aka Code nesting function包住得範圍 可以理解為圖層,一層一層嵌套。 # CH 7 ==195== 7.3圖 prototype 可以存取到父層 反正我的理解是正確,就是可以瘋狂串接AKA原型鍊,假設有a、b、c三位物件,b親了c,b就可以存取c的東西,但是a也像要親c,如果a也想要取得c得內容,那可以透過間接接吻的方式,a親b,由於b親了c,所以已經有存取到c的吻,那a親b就是透過b取到c的吻了 #### 7.3 chatGPT改進版: 原型鏈(Prototype Chain)其實是 JavaScript 中物件的繼承機制。一個物件可以透過它的 prototype 屬性連結到另一個物件,從而繼承該物件的屬性或方法。就像你說的: - b 親了 c:代表 b.__proto__ = c,b 的原型(__proto__)指向 c,b 就能「繼承」 c 的屬性或方法。 - a 想要親 c:a 其實親不到 c(因為沒有直接指向),但 a 可以親 b(a.proto = b)。 - 間接接吻:a 親了 b,而 b 親了 c,這條鏈就像「接力」,讓 a 能透過 b 找到 c 的內容。 用技術的語言來說: 當我們在物件 a 上尋找某個屬性或方法時,JavaScript 會先檢查 a 本身。如果找不到,就會沿著原型鏈(__proto__)一路向上尋找,直到找到目標或達到頂端(null)。 ==208== 再者裡演示了建立prototype方法,建立 Person Ninja 兩個函式,其中Person有一個dance物件 - 用建構子屬性參照Person,所以Person原型會建立 - Ninja則是轉移原型到Person(new Person) - 建立ninja去 new Ninja(這裡Ninja 有 new Person),所以ninja裡面的原型會是Person,所以可以存取Person裡面的東西 原理就是,由於ninja本身沒有dance,所以js環境會對ninja這物件本身做檢查,然後會一路往上找(前提是有綁定父親原型,而父親要有綁定祖父,這樣就會往上找物件),找到祖父有這個東西就會取用,這就是繼承的方式 by Chris #### 擁有這三個條件才算物件導向,js少了,所以不是物件導向 - 封裝(Encapsulation)js缺這個 - - 繼承 - prototype部分 - 動態連結 - 只不要重複使用函式或功能,而是取得別人創造的功能 #### 判斷型別三個方法 or SOP - typeof(如果簡單型別爆出是object,那就可以在往下測試複雜型別) - consturtor.name - instanceof #### == / === 用法 === undefined || === null 等同於 == null 要找 undefined、null 才用 ==,其他就用 === #### object 與 class 差異,和寫 class 的原因 - object 主旨在要別的屬性 - class 主要在想要索取別人的function,但前提示對方也是class ## Object Object.defined --- ## CH8 ![image](https://hackmd.io/_uploads/B12SyJeLJx.png) <!-- Chris 建立proxy 可以先想像鋼鐵人概念 p --> ==230== > get取值器 set設值器 是一種JS透過閉包來摹擬物件私有變數的一種方式 #### 它主要處理三大情境 > 以下為set優點描述 - 可以限制物件,不會犯下像是給錯資料類型(比方在set裡面建立正規表達式) - 預防輸入垃圾值 - 針對輸入值判斷是否正確 - 希望可以紀錄所有在物件裡發生的變化 - 紀錄改變值的順序,會知道錯誤發生時機 - vuex pinia 會用到這功能(紀錄修改值順序) - 在頁面某處顯示我們在物件設定的屬性質 - 針對畫面變更 ==231== 提到 get set 方法可以應用在例如:日誌紀錄、資料驗證、或是修改UI(比方深色模式) ==232== ### 8.1.1 定義 get set 的兩種方法 - 透過物件實值或 ES6 的類別定義 - 使用內建Object.defineProperty方法 > 複習 Object.defineProperty ### 8.2 在物件實質裡定義get取值器 set設值器 ```javascript= const ninjaCollection = { ninjas:["Chris","Watson","Jeremy"], get firstNinja(){ return this.ninjas[0] }, set firstNinja(name){ this.ninjas[0] = name; } } ninjaCollection.firstNinja;//'Chris' ninjaCollection.firstNinja = "Danny";//'Danny' ninjaCollection;//['Danny', 'Watson', 'Jeremy'] ``` ==233== ### 圖8.1導讀 從8.2我們看到get set的行為 - get 回傳索引值為0的元素 - set 對該元素指派新值 ==234== 在存取屬性時,真正取值的方法會被==隱性==呼叫 > ?:為什麼是隱性?(隱性轉型、顯性轉型) 在設值時,設值器也會在背地裡呼叫,然後修改集合裡索引值為0的ninja > ?:這段是否在解釋==230==頁的這段"可以紀錄所有在物件裡發生的變化,紀錄改變值的順序,會知道錯誤發生時機"?(已解:理解沒問題) 綜合以上所談到,這種原生get set 可以讓我們用==一般的屬性操作方式==來存取屬性 ==235== ### 圖8.3導讀 ### 8.3 在ES6類別中定義 get set ```javascript= class NinjaCollection { constructor() { this.ninjas = ["Fang", "Jami", "Tangerine"]; } get firstNinja() { return this.ninjas[0]; } set firstNinja(name) { this.ninjas[0] = name; } } //用 new 搭 class 創建出新的實例(instance) const ninjaCollection = new NinjaCollection(); ninjaCollection.firstNinja; //"Fang" ninjaCollection.firstNinja = "Danny";//"Danny" ninjaCollection;//["Danny", "Jamie", "Tangerine"] ``` 此實驗證實即使使用class,還是能如預期運作 ==236== >注意: >我們不一定需要為一個屬性同時定義 get set >- 如果只需要讀取值,可以只用 get >- 如果只需要設值,可以只用 set > >當只有設定get or set 其中之一,還會出現兩種情況 >- 在一般模式下,JS引擎會默默忽略任何get or set動作 > ``` javascript= > const obj = { > get value() { > return 42; > } >}; > >obj.value = 100; // 不會報錯,但無效 >console.log(obj.value); // 42 > > ``` >- 在嚴格模式下,JS會報錯(如果是常數,不想被改動的話可以使用嚴格模式 or setter,告訴程式語言 我的程式不想被改 ) > ``` javascript= >'use strict'; > >const obj = { > get value() { > return 42; > } >}; > >obj.value = 100; // 會直接報錯:TypeError: Cannot set >property value which has only a getter > ``` --- JS 並沒有真正的私有變數,只能透過物件包住變數,做出閉包來模擬私有變數。如果要使用get set來達到私有變數的方法,這裡可以用Object.defineProperty方法。 ### 8.4 用Object.defineProperty方法定義get set ```javascript= function Ninja() { //私有變數 (命名規則 可加上前綴 $ or _ ,這技巧是用來彌補語言缺陷) let _skillLevel = 0; Object.defineProperty(this, "skillLevel", { get: () => { return _skillLevel; }, set: (value) => { _skillLevel = value; }, }); } const ninja = new Ninja(); ninja._skillLevel; //無法直接存取私有變數,回傳undefined ninja.skillLevel //0 ninja.skillLevel = 10 //10 ``` ![Screenshot from 2024-12-31 11-52-15](https://hackmd.io/_uploads/SkGicyb8kx.png) > 產生閉包 ==239== ### 8.5 用set來驗證屬性值 ```javascript= function Ninja() { let _skillLevel = 0; Object.defineProperty(this, "skillLevel", { get: () => { return _skillLevel; }, set: (value) => { if (!Number.isInteger(value)) { throw new TypeError("wrong"); } _skillLevel = value; }, }); } //建立了ninja instance const ninja = new Ninja(); try { ninja.skillLevel = "hello"; } catch (error) { throw new Error("Wrong"); } //Uncaught Error: Wrong ``` 這裡實驗: set 設定了 if 判斷,當指派的值是整數,set 會正常發揮。如果非整數,則會報錯。如此一來,就可避免指派錯誤的型態 ==240== get set 還可以用來定義可計算屬性,其值是在每次存取時計算出來。 ### 8.6 定義可計算屬性 ```javascript= //shogun將軍 const shogun = { name: "Watson", //家族 clan: "Li", get fullTitle() { return this.name + " " + this.clan; }, set fullTitle(name) { const segments = name.split(" "); this.name = segments[0]; this.clan = segments[1]; }, }; shogun.name;//"Watson" shogun.clan;//"Li" shogun.fullTitle;//"Watson Li" shogun.fullTitle = "Jeremy Kuo";//改值 name:"Jeremy",clan:"Kuo" ``` 每當要取fullTitle,取值方法會把 name clan 屬性拼接在一起。 ### 總結 > JavaScript 的物件是一組動態屬性的集合,我們可以自由新增、修改或刪除屬性。而 get 和 set 的作用,是為屬性提供一種控制存取的方式,允許我們在屬性被讀取或寫入時進行監測、驗證或計算。 #### 本章節提到的功能 - 用Object.defineProperty方法定義get set - 物件實質裡定義get取值器 set設值器 - 用 class 定義 get set - 用set來驗證屬性值 - 定義可計算屬性 #### 觀察到現象 - 有做法是在 set 裡設置Validate、if else、判斷輸入是否為正確的驗證碼 - get set 是對物件屬性的操作,可以透過get取得值,透過 set 來重新設定值 - 透過 Object.defineProperty 建立模擬私有變數,檢查get set 的 scopes 有產生閉包 #### 疑問 - 目前已知 get set 是為了控制物件屬性,直接改物件不是也行?為什麼需要get set?(已解) - 做了實驗,寫了get set 在inspect 裡的scopes沒看到閉包,他不算閉包,只是某方面來說兩者挺像?(已解,在==236==) #### Chris 語錄 `Date.now()`(unix timestamp) 是 integer 是一個抽象數字。而date顯示出來的字串日期,則是透過 get set 實做出來的結果。 set - 改變值之前先動手腳 - 數學概念-定義域:某數值會有合理範圍 - 抽象存取方式 8.6 為例 這裡是抽象,舉水電,使用者只知道開關與結果,但是水電配置使用者不懂,所以除了呼叫以外就叫做抽象,其他為非抽象(使用者觀點) ```javascript= const shogun = { name: "Watson", //家族 clan: "Li", get fullTitle() { return this.name + " " + this.clan; }, set fullTitle(name) { const segments = name.split(" "); this.name = segments[0]; this.clan = segments[1]; }, }; ``` defind:上方的設計是為了實現下方的抽象 ```javascript= shogun.name;//"Watson" shogun.clan;//"Li" shogun.fullTitle;//"Watson Li" shogun.fullTitle = "Jeremy Kuo";//改值 name:"Jeremy",clan:"Kuo" ``` get - 數學概念-值域:輸出值會檢視過,比方一天不會有25h,所以get會檢視過後才會輸出結果。 - 相同的值,可以經由自己的設定,調整成不同格式輸出。比方設定單位轉換,時間、尺寸、匯率(舉例:在get設定method,只要輸入NTD:1,就會印出已設定好的匯率)、資料過濾,哪個欄位顯示,拿到資料前作手腳(有點類似上次提到的,不想在重寫function,所以透過繼承取得基礎型別的code共用)。 ### 8.2 使用proxy(代理)來控制對物件得存取 ![image](https://hackmd.io/_uploads/BJxVwa_Ikl.png) ==242== ### 比較 #### get set - get set 只能控制單一屬性的存取 - get set 可做出紀錄日誌、資料驗證、計算屬性。 #### proxy - proxy 能夠控制與物件之間的所有互動,包括對方法的呼叫 - proxy 除了包含上述功能,還能很容易地為程式做效能剖析及量測,也可用來對屬性自動設值,避免空值異常,或是為DOM這類容器提供一層包裝,減少瀏覽器相容性問題 ==243== ### 8.7 用 Proxy 建構器函式來建立代理 ```javascript= const emperor = {name: "Watson"} //使用proxy建構器函式,將emperor物件包裝在 representative 代理物件裡。 const representative = new Proxy(emperor, { get:(target,key)=>{ //判斷目標物件是否有key值 return key in target ? target[key] //此處為proxy功能,對get設置機關,有問題就會觸發訊息 :"Don't bother the emperor !" }, set:(target, key, value)=>{ target[key] = value; return true } }) //直接呼叫 emperor.name;//"Watson" //藉由proxy代理物件呼叫 representative.name;//"Watson" //呼叫不存在的屬性 emperor.nickName;//undefined //藉由proxy呼叫不存在屬性,會跳出在get設定中的警告訊息 representative.nickName;//"don't bother the emperor !" //透過proxy新增屬性 representative.nickName = "càitóu" //呼叫emperor新增的屬性 emperor.nickName;//"càitóu" //藉由proxy呼叫新增屬性 representative.nickName;//"càitóu" ``` - target 目標物件本身,也就是emperor - key 就是屬性名稱 - value 屬性的值 >?:直接用set也可以設值,那proxy用意?此處尚無體會proxy的點,還是機關部分 ==244== ### 在8.7裡面,proxy 作用像是為 get 設立了一個機關,而機關的功能是 - 如果目標物件確實有指定屬性,就會回傳屬性 - 如果目標物件裡沒有指定屬性,就會回傳警告訊息(沒proxy會傳undefined) ### 關於使用直接呼叫和proxy呼叫差別(可參照圖8.4) ```javascript= //直接呼叫 emperor.name;//"Watson" //藉由proxy代理物件呼叫 representative.name;//"Watson" ``` - 如果直接呼叫emperor.name屬性,會回傳"Watson" - 如果使用 proxy ,get會被隱性呼叫,由於目標物件有找到name屬性,所以也會回傳"Watson" ==245== >讀 注意: ### 8.7程式碼,如果直接對物件存取不存在的屬性 ```javascript= //呼叫不存在的屬性 emperor.nickName;//undefined //藉由proxy呼叫不存在屬性,會跳出在get設定中的警告訊息 representative.nickName;//"don't bother the emperor !" ``` - 直接存取 emperor.nickName 會得到 undeined - 透過 proxy 來存取則會啟動==取值動作處置器== > 導讀圖8.4 ### 設置新屬性 透過 proxy 來設置新屬性`representative.nickName = "càitóu" `。此設值是透過 proxy 完成,所以會觸發設值機關,記錄一段訊息並且指派一個新的屬性給emperor。當然也是可以使用物件本來新增值 ==246== 使用 proxy 要點在於啟動機關來控制目標物件的存取 >導讀四點舉例 下面稍微提一下 proxy 機關無法對相等運算子(== or ===),instanceof,typeof進行複寫操作。以相等運算子為例,在比較兩個物件時,不應該對該物件有存取行為。基於類似理由,我們也不能為instanceof, typeof設置機關。 ### 8.2.1 使用代理來記錄日誌 本篇章針對日誌探討,在運作程式時,想了解運作原理,或是找出bug,這時就需要取得資訊,知道程式何時編輯,以及編輯內容等 #### 8.8 未使用代理 >?:個人覺得書上的範例不太直覺,用物件集合方式來比較,下面我用我自己的方式 ```javascript= const ninja = { _name: "Yoshi", get name() { console.log(`Getting property "name": ${this._name}`); return this._name; }, set name(value) { console.log(`Setting property "name" to: ${value}`); this._name = value; }, //當我新增Weapon 我就要在重寫一次 get set get weapon() { console.log(`Getting property "weapon": ${this._weapon}`); return this._weapon; }, set weapon(value) { console.log(`Setting property "weapon" to: ${value}`); this._weapon = value; }, }; ninja.name; // Getting property "name": Yoshi ninja.weapon = "sword"; // Setting property "weapon" to: sword console.log(ninja.weapon); // Getting property "weapon": sword ``` #### 8.9 使用代理 ```javascript= function makeLoggable(target) { return new Proxy(target, { get: (target, property) => { console.log(`hey, I'm calling u "${property}:${target[property]}"`); return target[property]; }, set: (target, property, value) => { console.log(`ohh, We add "${property}:${value}" in the object`); target[property] = value; }, }); } //這邊建立物件 let ninja = { name: "Yoshi" }; //丟到function裏面,成爲target ninja = makeLoggable(ninja); ninja.name; ninja.weapon = "sword"; ``` 以上面兩個日誌範例的差別 - get set 如果新增物件屬性,只能針對特定屬性"手動更新",不能動態改變日誌內容,這樣就會每次新增一個物件屬性就要在重寫一次get set。 - proxy 則可以依據以設定好內容搭配樣板字面值自動更新日誌,我只要寫一次get set就好,其他交給proxy的參數 property 與 value ### 8.2.2 使用代理來測量效能 主要用來測試函式的效能,而不需要更改函式的內容 #### 8.10 使用代理來測量效能 >?:這裏proxy的部分不懂,需要解惑 ```javascript= //使用代理來測量效能 function isPrime(num) { num < 2 ? false : true; for (let i = 2; i < num; i++) { return num % i === 0 ? false : true; } } console.log(isPrime(22)); //false console.log(isPrime(23)); //true isPrime = new Proxy(isPrime, { //this apply is proxy handler apply: (target, thisArg, args) => { //開始計時 console.time("isPrime"); //idk what it is, only know it's collect result of true false //後來經由測試,thisArg是空值 undefined const result = target.apply(thisArg, args); //停止計時器 console.timeEnd("isPrime"); return result; }, }); isPrime(1299827); ``` ==250== ### 8.2.3 藉由代理自動對屬性設值 ```javascript= function Folder() { return new Proxy( {}, { get: (target, property) => { console.log(`reading ${property}`); if (!(property in target)) { target[property] = new Folder(); } return target[property]; }, } ); } const rootFolder = new Folder(); try { rootFolder.ninjasDir.firstNinjaDir.ninjaFile = "yoshi.txt"; pass("an exception wasn't raised"); } catch (e) { throw new Error("An exception has occurred"); } ``` >?:目前理解這運作原理,當get偵測到不存在的屬性,就會自動建立,new 出一個新的Folder (實例?) >所以在下面看到當存取不存在的屬性時,不會報錯。面對空值異常,這是一個避免的工具 ==252== ### 8.2.4 使用代理來實作陣列負數索引值 js 不支援負數索引,但是可以透過proxy來模擬這功能 #### 8.12 藉由代理來模擬陣列負數索引值 ```javascript= function createNegativeArrayProxy(array) { //判斷:如果不是陣列就拋出錯誤訊息 if (!Array.isArray(array)) { throw new TypeError("Expected an array"); } //回傳proxy,以陣列作爲代理對象 return new Proxy(array, { //讀取到索引值就觸發取值機關 get: (target, index) => { //將index 利用一元正號運算子的特性,將值轉換爲數字 //怕遇到轉型問題 index = +index; //由於陣列索引不能使用負數來移動索引位置(會出現undefined),所以判斷, //如果索引值小於 0(=負索引),我們將負索引轉換為對應的正索引: // 計算方式是將陣列的長度加上目前的索引值 (target.length + index)。 //假設傳入 -1,程式會計算 3 + (-1) = 2,對應陣列中的索引 2, "hattori"。 // 這樣的邏輯類似 string.at(), -1 對應倒數第一個元素。 //const str = "hello"; //console.log(str.at(-1)); // "o" (倒數第一個字元) return target[index < 0 ? target.length + index : index]; }, set: (target, index, val) => { index = +index; return (target[index < 0 ? target.length + index : index] = val); }, }); } const ninjas = ["yoshi", "kuma", "hattori"]; const proxiedNinjas = createNegativeArrayProxy(ninjas); console.log(ninjas[0]);//yoshi console.log(ninjas[1]);//kuma console.log(ninjas[2]);//hattori console.log(proxiedNinjas[0]);//yoshi console.log(proxiedNinjas[1]);//kuma console.log(proxiedNinjas[2]);//hattori console.log(typeof ninjas[-1]);//undefined console.log(typeof ninjas[-2]);//undefined console.log(typeof ninjas[-3]);//undefined console.log(proxiedNinjas[-1]);//hattori console.log(proxiedNinjas[-2]);//kuma console.log(proxiedNinjas[-3]);//yoshi proxiedNinjas[-1] = "Hachi"; ninjas[1] = "Hachi" ``` ==254== ### 8.2.5 使用代理的效能成本 proxy 是用來==控制物件存取動作==的代理人。我們可以在 proxy 中定義機關,每當對 proxy 物件進行了某些動作時,就會觸發它們。 proxy 可用來控制物件的存取與操作,並實現功能如效能測量、記錄日誌、自動設值及負數索引等。然而,proxy也有一些==缺點==,所有動作都必須透過 proxy ,因此無形中增加了一道間接層,讓我們在實作上述功能之餘,也產生了額外的處理動作,進而影響程式的效能。 為測試效能差異,我們可參考程式列表 8.12,比較普通陣列與 proxy 陣列的效能表現。 ==255== #### 8.13 檢查代理的效能限制 ```javascript= function measure(item) { const starTime = new Date().getTime(); for (let i = 0; i < 500000; i++) { item[0] === "yoshi"; item[1] === "kuma"; item[2] === "hattori"; } return new Date().getTime() - starTime; } const ninjas = ["yoshi", "kuma", "hattori"]; const proxiedNinjas = createNegativeArrayProxy(ninjas); console.log("Proxies are around", Math.round(measure(proxiedNinjas) / measure(ninjas))); //Proxies are around 23 ``` >?:以結果來看,在chrome 瀏覽器上,proxy 比普通陣列慢了23倍 ### 總結 Proxy 是 JS 的一種工具,允許攔截和定義對目標物件的基本操作,所有動作都必須經過 proxy,當指定的行爲發生,便會觸發定義在proxy的機關。但是proxy執行速度不會,若頻繁執行,便會帶來效能上的負擔 #### 本章節提到的功能 - 記錄日誌 - 函式效能與proxy效能測試 - 資料驗證 - 自動爲屬性設值 - 陣列負數索引值 #### 提問 #### Chris 語錄 ## CH9 處理資料集合 [Chris 分享](https://hackmd.io/@unayojanni/HyzWz9R0u/%2F%40unayojanni%2FryUNVTPyt) ==259-262== 講解陣列的特性,陣列是物件,當丟東西進去時,他不會阻攔,甚至事會幫你新增值。 ```javascript= const test = ["Chris", "Watson"]; test[2]//undefind test;//["Chris", "Watson", undefined]; ``` 上面結果test索引2是 undefind 如果後續在設值進去就會取代undefined ```javascript= test[2]="Jeremy" test;//["Chris", "Watson", "Jeremy"]; ``` ==262== 這裏是在講test陣列的長度是2,所以test.length-1 = 2-1 = 1 後來會長成這樣test[1],而索引值1是"Watson" ```javascript= const test = ["Chris", "Watson"]; test[test.length-1]//"Watson" ``` ==263~266== 這裏在講陣列有四大方法可做到兩端新增或移除,會影響陣列長度 - push - 在 array 最尾端增加資料 - unshift - 在 array 最前端增加資料 - 會影響索引值 - pop - 移除 array 最尾端資料 - shift - 移除 array 最前端資料 - 會影響索引值 ==266== 介紹delete方法,可以刪除陣列裡面的內容,但不會影響陣列長度 ```javascript= const name = ["Chris", "Watson", "Jeremy","Jami","Fang","Tangerine"]; delete name[1] name.length == 6 name;//["Chris", undefined, "Jeremy","Jami","Fang","Tangerine"] ``` ==267~268== 這裏介紹 splice 方法,很有趣,他會分割內容和影響長度 ```javascript= const name = ["Chris", "Watson", "Jeremy"]; let cut = name.splice(1,1) cut.length == 1 cut;//["Watson"] name.length == 2 name;//["Chris", "Jeremy"] ``` 由上面程式碼可知 name 影響了,cut 使用賤方法取走了 name 的一個資料。 splice 有兩個引數 - 第一個是選中要拆的索引值 - 第二個是決定從索引開始算要刪除多少資料,假設寫2就會是從選定的索引值開始算,往後刪除兩個,如果只寫索引不寫後面的值,那這樣後面的全部都會倍拆掉 ```javascript= const name = ["Chris", "Watson", "Jeremy","Jami","Fang","Tangerine"]; let cut = name.splice(1) cut.length == 5 cut;//["Watson","Jeremy","Jami","Fang","Tangerine"] name;//['Chris'] ``` ==269== 這裏講解陣列有一個loop方法是 forEach