[TOC] ## CH2 JavaScript 基礎介紹 ### typeof 判斷型別 ```javascript! //檢查型別 typeof 100; //"number" typeof "hello"; //"string" ``` 注意:typeof回傳的是字串喔! ### 字串 String 1. 使用單引號或雙引號包裹 2. 字串只有「相加 + 」,可以串連多個字串,ES6出現template literal樣板字面值寫法 3. 字串與不同型別相加,會觸發JS的強制轉型 #### template literal樣板字面值 ```javascript! //普通字串要換行需要加\n console.log("first sentence\n" + "second sentence") //樣板字面值可以直接enter換行 console.log(`first line second line`); ``` #### 轉型成字串 除了字串以外的型別想要轉型字串,JS有內建函式`String()` ```javascript! let a = 5; let aTostring = String(a); console.log(aTostring); //"5" ``` ### 數值 Number 1. 數值有「加減乘除+ - * /」 2. 其他型別轉成數值,JS內建函式`Number()` - 如要轉型整數,使用`parseInt()` - 如要轉型浮點數(小數點),使用`parseFloat() ` 3. 數值與不同型別相加,會觸發JS的強制轉型 #### 轉型成數值 ```javascript! //轉型成數值 let b = "7.8912"; let bTonumber = Number(b); console.log(bTonumber); //7.8912 //浮點數 console.log(parseFloat(b));//7.8912 //整數 console.log(parseInt(b));//7 ``` #### 強制轉型 當運算子兩邊為不同型別,JS會嘗試將兩邊轉型為同型別 ```javascript! //轉型成字串 let year = 2024; let yearString = 2024 + "";//數字與字串相加,轉型為字串 console.log(yearString);//"2024" ``` ```javascript! //轉型成數值 let c = +"2024"; //使用加號使後面字串轉型 console.log(c);//2024 ``` ```javascript! //轉型成布林 let a = "hello"; let b = ""; let c = 0; let d; console.log(Boolean(a));//true console.log(Boolean(b));//false console.log(Boolean(c));//false console.log(Boolean(d));//false console.log("加上反向運算子",!!a); ``` * 數值與字串相加 * 數值與undefined相加 - JS會把undefined轉型成數值型別,得到NaN - 任何數加NaN都會是NaN * 數值與物件相加 - 如有一邊是物件,物件轉型成字串,會得到`"[Object Object]"` ### NaN(Not a Number) * 不是數值的型別想要轉型成數值,此型別如無法成功轉型為數值,無法正常計算結果,JS會回傳NaN * NaN是數值型別,可使用`typeof`檢查 ```javascript! let d = "hello world"; let dTonumber = Number(d); console.log(dTonumber);//NaN console.log(typeof NaN);//"number" ``` ### undefined(未定義) * 變數宣告的預設值 * 表示某個變數已被宣告,但未賦予任何內容,預設為`undefined` * undefined 和 not defined是不一樣的,前者是宣告了沒有賦予任何內容,後者是未宣告就拿來使用而報錯 ### Null(空值) * 和undefined一樣,變數都沒有內容,但Null意思是我要讓變數的值是空值 * Null的typeof為Object ### 布林 Boolean * 只有true和false * JS有內建函式`Boolean()` * 使用反向運算子(`!!`),兩個!強制轉型,得到所對應的**正向布林值(因為一個!是反向)** ```javascript! let a = "hello"; console.log("加上反向運算子",!a); //false console.log("加上反向運算子",!!a); //true ``` ### 物件 Object * key和value組合 - 所有物件的key為字串型別 - 不存在物件裡面的key若要取值,會得到`undefined` * 沒有限制存放的型別 ```javascript! let family = { name:"小白", age:18, addr:"Tainan" } //取值方法1 console.log(family["name"]); //"小白" //取值方法2 點記法 console.log(family.age);//18 ``` ### 陣列 Array * 沒有限制存放的型別 * 存放陣列的值,會稱為element元素 * 每個元素都有索引值(index),從0開始 ### 運算子與運算元 一元運算子、二元運算子、三元運算子,元指的是跟幾個值放在一起做運算,一個值就是一元,如果兩個值就是二元,如果三個值就是三元 ```javascript! //一元運算子 用一個值就能運算 console.log(+"5");//5 console.log(!!8);//true //二元運算子 須用兩個值運算 console.log(1+4); //5 ``` #### 指派運算子「=」 #### 加法運算子「+」 範例[強制轉型coercion](#強制轉型) #### 減法運算子「-」 * 若型別不是數值型別,JS會嘗試轉型成數值型別 * 字串轉型成功,正常減法運算 * 字串轉型失敗,會得到NaN結果,因為任何數與NaN都會是NaN ```javascript! let str = "2023"; let str2 = "hello"; let num = 3; console.log(str - num); //2020. console.log(str2 - num); //NaN ``` #### 除法運算子「/」 * 任一數字除於0,結果都是無限大,在JS用`Infinity`來表示,超出JS能表示的範圍就會出現 * `Infinity`是數值型別,有正無限大(+Infinity)和負無限大(-Infinity) ```javascript! console.log(10 / 0);//Infinity ``` #### 乘法運算子「*」 * 一樣會看兩邊是不是數值型別,如有一邊不是,JS會嘗試轉型數值型別 * 須注意數值是否超過JS數值範圍 #### 餘數運算子「%」 ```javascript! let a = 350 let newA = 780 % a; console.log(newA);//餘數80 ``` #### 「 == 」「 === 」相等運算子 * 「 == 」寬鬆比較,會比較值,不會比較型別 * 「 === 」嚴格比較,會先比較型別,型別一樣再比較值 * 運算結果得出布林值 * 實際開發盡量不要用到「 == 」比較 > P2-39 物件被創造後,賦值給變數時,變數所儲存的內容不是整個物件內容,而是物件存放的記憶體位置 > 所以物件在做比較,不是比較物件內容,而是物件的記憶體位置 #### 「 != 」「 !== 」反向的相等運算子 * 「 != 」是「 == 」的反向(一樣會自動觸發轉型) * 「 !== 」是「 === 」的反向(實際開發建議使用) #### 「 > 」「 < 」「 >= 」「 <= 」運算子 JS自動轉型規則大方向: 兩側都是字串,則比較unicode編碼值 #### 運算子相依性和優先性 * 相依性(Associativity): 決定運算子從「什麼方向開始計算」 - 處理順序從左到右稱為「左相依性(Left-associativity)」,從右到左稱為「右相依性(right-associativity)」 - 例如: AND、OR為「左相依性」,賦值=為「右相依性」 * 優先性(Precedence): JS會先看優先序,如優先序相同,再比較相依性 - 優先性高到低 - 括號()擁有最高優先性的運算子 >[MDN:運算子優先序&相依性](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Operators/Operator_precedence) ```javascript! //左相依性 true || false //右相依性 var a = 1; var b = 2; var c = 3; a = b = c; //結果 a=3 ``` ```javascript! //優先性範例 1 + 2 * 3 //查找MDN表格可以知道,「*」優先性為13,「+」的優先性為12,「*」優先性比較高,所以先處理2*3,最後再1+6 ``` ### 強制轉型 Coercion JS在對不同型別的數值做運算時,會必須把兩個數值轉為同一類型,有兩種方式達成轉型。 * 明確轉型:使用JS內建函式,例如: Number() * 隱含轉型:JS自動轉型 弱型別語言,例如:JS,允許不同型別放在一起計算,在強型別語言,例如:python就會報錯 #### 轉為字串的轉型 * 明確轉型: String() * 隱含轉型: 使用+運算子觸發隱含轉型 ```javascript! //明確轉型 console.log(String(2)); //"2" //隱含轉型 console.log(2+"777"); //"2777" ``` #### 轉為布林值的轉型 * 明確轉型: Boolean() * 隱含轉型: 使用邏輯運算子、if或while等陳述式條件區塊觸發隱含轉型 * 只有falsy value一定會轉成false,其餘轉型為true * falsy value(假值): undefined 、 null 、 NaN 、 0 、 "" (空字串)和 false ```javascript! //明確轉型 console.log(Boolean(2));//true console.log(Boolean({}));//true console.log(Boolean([]));//true //隱含轉型 console.log(!!7); //true if("5"){} //true ``` #### 轉為數值的轉型 * 明確轉型: Number() - 特別 : null轉型數值為0,undefined轉型數值為NaN * 隱含轉型: - 大小於運算子(`<` `>` `<=` `>=`) - 寬鬆數值比較(`==` `!=`) - 算數運算子(`-` `+` `*` `/` `%`),須注意如果用到`+`,其中一邊如果是字串,則會觸發字串的強制轉型,JS的規則 - 在任一個數值前使用加法運算子`+` ### 物件的強制轉型p2-55~56 物件與運算子一起使用時,JS會先把物件轉為純值才能做運算,通常我們不會直接呼叫`toString`和`valueOf`,這個過程是由JS呼叫 #### 物件轉字串或數值 * 會使用ToPrimitive的演算流程 * 物件預設有`toString`和`valueOf`,`toString`影響物件轉字串,`valueOf`影響物件轉數值型別 * `toString`和`valueOf`兩者都會被呼叫只是順序上不同,如果JS認為須轉型字串就會先呼叫`toString`再`valueOf`; 如果認為是須轉型成數值就會先呼叫`valueOf`再`toString` ### falsy value與truthy value falsy value(假值): * undefined * null * NaN * 0 * "" (空字串) * false ==除了falsy value外,其餘轉型都屬於truthy value== ### 條件式判斷 * if判斷式 * if / else if * switch: 是用嚴格相等運算 ### 迴圈 * for 迴圈: 用在會知道要執行幾次 * while迴圈: 適合用在不知道要執行幾次的情況 ### 三元運算子 條件判斷? true執行:false執行 ```javascript! var score = 100; var message =''; //原本條件判斷 if (score >= 60){ message = 'pass'; } else { message = 'fail'; } //改成三元運算 var message = (score >= 60) ? 'pass' : 'fail'; console.log(message); //'pass' ``` ## CH3 執行環境與作用域 ### 編譯語言vs直譯語言 編譯: 開發者寫完程式碼後就會預先進行編譯 直譯: 即將要執行時才透過直譯器,直接動態進行編譯後執行產生的程式碼,也就是一邊解讀,一邊執行 >所以直譯語言執行速度比編譯語言慢 >JS屬於直譯語言 >直譯語言無法獨立執行,須仰賴環境,這環境要可以編譯且產生結果 ### 執行環境與執行堆疊 1. 執行環境(Excution Context) * 全域執行環境(Global Excution Context) * 函式執行環境(Functional Excution Context) * 每個函式都有自己的執行環境,但只有在被呼叫時候才會產生 * 不會產生全域物件 * eval函式執行環境(目前已不使用) 2. 執行堆疊(Excution Stack) - 函式有呼叫才會開始堆疊 ### 作用域(Scope)(又叫做範疇) 意思是==變數可以被使用的範圍== JS在scope內找不到某變數就會向外找 函式內有其他函式呼叫,就會產生執行環境的堆疊 * 語彙環境(Lexical Environment) 代表程式碼在程式中的物理位置,簡單說就是你把程式碼寫在哪裡 * 函式範疇(Function scope) 當函式結束時,會離開執行環境,裡面宣告的變數就無法取得 ### 區塊作用域(區塊範疇) 在ES6,多了`let`、`const`用區塊(block)來定義範疇 block scope用大括號`{}`來定義 例如:if、while、for、function等的大括號`{}` ### 作用域鍊(scope chain) 一連串查找動作所產生的物理位置的裡外關係(而不是執行環境產生的先後順序),就是 Scope Chain ### 提升現象(hoisting) v8引擎,在全域環境會執行 1. 產生全域物件window 2. 產生this物件 3. 進行記憶體指派流程 #### 執行環境的兩個階段 記憶體指派就是提升(hoisting)關鍵 * 創造階段 :為變數和函式保留記憶體空間,還不會寫入值,只會給定初始值`undefined`,此動作稱為提升(hoisting) * 執行階段 :變數在此階段賦值 ```javascript! hello(); //"hoisting" console.log(apple); //undefined console.log(banana); //ReferenceError: can't access lexical declaration 'banana' before initialization var apple = 3; let banana = 10; function hello(){ console.log("hoisting"); } ``` ### ES6的let/const變數宣告 * let - 是block scope - 不能被重複宣告,但可以修改 - 宣告變數不一定要給值 * const(常數) - 是block scope - 宣告的變數只能讀取,不能修改 - 宣告變數一定要給值,否則會報錯 - 與物件、陣列使用時,看起來可以對物件屬性做修改,實際是因為**物件存放的是記憶體位置**;如重新指派物件,不是同一個記憶體位置就會報錯 > var: 是函式作用域,只有函式能限制var的作用域 ```javascript! //使用let,內外的i並不會互相影響修改 let i = 0; for(let i = 1; i<10;i++){ console.log("block",i); //1~9 } console.log("global",i);//0 ``` ```javascript! //使用var,內外的a會互相影響修改 var a = 0; for(var a = 1;a<10;a++){ console.log("for",a);//1~9 } console.log("global",a);//10 ``` ```javascript! //let 可修改,但不能重複宣告變數 let name = "peter"; name = "David"; console.log(name);//"David" let name = "重複宣告name"; //SyntaxError: redeclaration of let name ``` ```javascript! //const 只能讀取不可修改 const price = 1; price = 2; console.log(price); //TypeError: invalid assignment to const 'price'無效的指派 ``` ```javascript! //const 物件,記憶體位置不能修改,否則報錯 const family = { father:"David", mother:"Esther", addr:"Tainan", } family.addr = "Taipei"; console.log(family); /*{ "father": "David", "mother": "Esther", "addr": "Taipei" }*/ ``` ### 暫時性死區(temporal death zone,TDZ) let / const在創造階段,宣告的變數會保留記憶體,但不會賦值,此稱為暫時性死區 >而var會先指派預設值`undefined` ## CH4 物件型別與原始型別 ### 物件型別 有物件、陣列、函式 使用`Array`物件的方法`isArray`判斷是否為陣列 ```javascript! let number = [5,7,9]; console.log(typeof number);//"object" //用Array.isArray判斷 console.log(Array.isArray(number));//true ``` ```javascript! //typeof 函式為function function hello() {} console.log(typeof hello); //function" //其實函式是物件,可像物件新增屬性 hello.morning = "good morning!"; console.log(hello.morning); //"good morning!" ``` ### 原始型別 又稱純值(primitive type)、基本型別 * string * number * boolean * undefined * null * symbol symbol類別是由Symbol()產生,可避免物件屬性意外被修改 ```javascript! let symbol1 = Symbol("apple"); let symbol2 = Symbol("apple"); console.log(typeof symbol1);//"symbol" console.log(symbol1 === symbol2);//false Symbol括號裡都一樣,其實是兩個不同值 ``` ### 變數指派 * 原始型別指派 - 為**傳值呼叫**(call by value) - 會為變數a和b各別預留記憶體空間,變數b再複製變數a的內容,修改時互不影響 ![image alt](https://hackmd.io/_uploads/By-A5CYQA.png =80%x) ```javascript! //原始型別指派 let a = "hello"; let b = a; //修改b值,a不受影響 b = "apple"; console.log(a);//"hello" console.log(b);//"apple" ``` * 物件型別指派 - 為**傳參考呼叫**(call by reference) - 變數實際存放是物件的記憶體位置的參考,非物件內容 ![圖片](https://hackmd.io/_uploads/HkUMoCFm0.png =80%x) ```javascript! //物件型別指派 let c = { coffee:100, blackTea:30, water:15 } //變數d複製變數c的記憶體位置 let d = c; //修改變數d物件,變數c物件也被修改 d.greenTea = 35; console.log(c); console.log(d); console.log(c === d);//true,指向同一個記憶體位置 ``` ### call by sharing 不像傳值呼叫,也不像傳參考呼叫 ```javascript! //函式參數為物件型別,函式內對陣列做修改,會影響到函式外的陣列,因為傳參考呼叫關係 let students = ["David","Peter","Esther"]; function addPeople(name){ name.push("Mary"); } addPeople(students); console.log(students); //["David","Peter","Esther","Mary"] ``` ```javascript! //函式參數為原始型別,不會有影響,因為傳值呼叫關係 let number = 50; function addNum(x){ return x * 2; } addNum(number); console.log(number);//50 console.log(addNum(number));//100 ``` 將另一個變數指派一個全新的物件,就會創造一個新的記憶體空間,使記憶體指向不同 ```javascript! //在函式內重新指派,不會影響函式外的陣列 let students = ["David","Peter","Esther"]; function addPeople(name){ name = ["nobody"]; //指派為新的物件 } addPeople(students); console.log(students); //["David","Peter","Esther"] ``` ## CH5 函式的進階概念 ### 陳述式 不會產生數值,例如:函式參數 * if / switch * for / while * 變數宣告 * 一般函式宣告 ### 表達式 只要執行會回傳結果就是表達式 * 函式呼叫 * 變數指派 * 運算式 ### 函式陳述式 vs 函式表達式 函式陳述式:一般函式宣告 函式表達式:宣告一個變數把函式指派給變數 兩者差別在**hoisting提升** CH3說到,在創造階段會為「宣告的變數」和「函式」保留記憶體位置,「整段函式」在創造階段就一同存入,而變數在執行階段才賦值 ![image](https://hackmd.io/_uploads/SJh9iMN4C.png) ```javascript! //函式陳述式 //在函式宣告之前呼叫成功,因為整段函式在創造階段已存入記憶體位置 callSomeone("David"); //"David" function callSomeone(name){ console.log(name); } ``` ```javascript! //函式表達式 //在函式宣告之前呼叫報錯,因只有宣告的變數提升,執行階段才會把整段函式賦值給變數 calculateScore(85,90); //Uncaught ReferenceError: Cannot access 'calculateScore' before initialization let calculateScore = function(Math,English){ console.log(Math + English); } ``` 函式是一種特殊物件 ```javascript! function callSomeone(name){ console.log(name); } //可以看成這樣 { name:callSomeone, code:... //這邊是示意,並不是真的JS程式碼 } //使用name屬性,可取到函式名稱 callSomeone.name; //callSomeone ``` ### 立即執行函式 IIFE 全名Immediately Invoked Function Expression ```javascript! let name = "Peter"; let age = 18; (function sayHi(name){ console.log(`hello!my name is ${name}`); })(name); (function sayHello(age){ console.log(`今年${age}歲`); }(age)); //呼叫括號在內或外都可以執行 //IIFE後或前須添加分號,沒隔開會報錯(ASI有關) ``` ### 一級函式 這個語言把函式當作變數值看待 接收函式作為參數、把函式做為回傳值 ### 高階函式 High Order Function(HOF) 接收函式做為參數,或是回傳函式作為輸出的函式 例如: map()、filter()、forEach() ```javascript //用for取用每個元素陣列 let score = [50,39,73,84,100,65]; for(let i = 0; i < score.length;i++){ console.log("for",score[i]); } //用forEach取用每個元素陣列 score.forEach(function(item,i){ console.log(i,item); }); ``` ### 箭頭函式 * function關鍵字改用`=>` * 回傳內容只有一個表達式時,可省略`{}` * 沒有arguments物件 * 沒有this ```javascript! //箭頭函式 const sum = (a,b) => { return a - b; } console.log(sum(100,80)); //省略{} const getValue = () => console.log("text") //箭頭函式使用IIFE可以執行 (() => { console.log("箭頭函式123"); })(); ``` #### 參數與引數 ![image](https://hackmd.io/_uploads/rJg0iGEVR.png =80%x) ![image](https://hackmd.io/_uploads/BJtAifNN0.png =80%x) * 用function關鍵字創造的函式,JS預設會提供「arguments物件」,是用來接收函式的引數 * arguments物件是類陣列(Array_liked),可用索引值取到引數值,要注意類陣列有些陣列的方法是無法使用,例如:forEach、map ```javascript! function add(a,b,c){ console.log(arguments); console.log(arguments[2]);//80 } add(100,50,80); ``` ![image](https://hackmd.io/_uploads/SJZbnMVNC.png =60%x) ### 回呼函式 callback function 在函式裡面執行另一個函式,尤其在非同步,確保某段邏輯在另一段邏輯後才執行 ### 閉包 Closure 一個函式內能夠存取到另外一個已經結束函式執行環境內所宣告的變數,因為JS在該函式執行環境結束後,會保留所取用的內容,直到這個取用內容的執行環境也結束 ### 其餘參數 * 使用在不確定函式的參數數量時 * 用`...`標示,並放在參數的最後 * 其餘的參數會是一個陣列傳入函式 ```javascript! function sum (a,b,...c){ return c; } sum(1,2,3,4,5,6);//[3,4,5,6] ``` ### 物件參數 直接取物件內容作參數,也就是ES6新語法**解構賦值(Destructuring)**,可快速把物件內容拆解取出使用 ![image](https://hackmd.io/_uploads/HJKMhME4C.png) ```javascript! let obj = { name:"David", age:18, addr:"Tainan" } //在參數加上大括號,直接取物件內容作參數 function displayObj({name,age}){ console.log(name,age);//"David" 18 } displayObj(obj); //解構賦值 let {name,addr} = obj; console.log(addr);//"Tainan" ``` ## CH6 同步與非同步 ### 同步與非同步 同步: 同一時間只做一件事 非同步: 同一時間內處理不只一件事 主程式會繼續,非同步的工作不是馬上進行,要等到主程式執行完畢 ### Browser(JavaScript runtime environment)組成 * JS引擎 * Event Queue事件儲列 * Web API * Event Table事件表格 * Event Loop事件迴圈 #### Event Queue事件儲列 * 屬於瀏覽器的一部分,專門存放非同步函式,等到主執行環境運行結束才開始依序執行事件儲列的函式 * Queue是先進先出,與堆疊stack後進先出相反 * 範例:主程式運行到setTimeout>>在event table計時>>計時完畢,把接收到的函式推送到event Queue>>等JS引擎運行結束,主程式執行結束,再把event Queue的函式推送到JS主執行環境 #### Event Table事件表格 和event Queue互相搭配,JS會把給定的函式和倒數秒數先推送到event table,**等目的達成(例如:計時完畢),正式推送到event queue等待執行** 例如:setTimeout會在event table計時,計時完成後再把函式推送到event queue #### Event Loop事件迴圈 無時無刻都在執行程式 檢查主程式執行環境是否為空,再檢查event queue是否有等待的函式要執行,如有,會把這些等待的函式放到主程式執行環境來執行 #### Web API * 操作DOM節點的API:`document.getElementById` * AJAX相關API:`XMLHttpRequest` * 計時類型API:`setTimeout` - setTimeout:幾毫秒後執行,只一次 - setInterval:每間隔幾毫秒執行一次,不會停 ```javascript! //setTimeout語法 //要執行的邏輯為函式形式 setTimeout(function(){},毫秒) ``` ```javascript! //題目 for(var i = 0;i<3;i++){ setTimeout(function(){ console.log(i); },1000); //1秒後印出結果 } //output: //3 //3 //3 //解法1 //IIFE,用function產生閉包,把每個迴圈的i保留 for (var i = 0; i < 3; i++) { (function (x) { setTimeout(function () { console.log("IIFE", x); },1000); })(i); } //output: //"IIFE",1 //"IIFE",2 //"IIFE",3 //解法2 //改成let,利用let的block scope for (let i = 0; i < 3; i++) { setTimeout(function () { console.log("let",i); }, 1000); //1秒後印出結果 } // "let",0 // "let",1 // "let",2 ``` ### Promise promise狀態 * pending:執行中的狀態,還沒有結果 * fulfilled:成功狀態,對應callback function為`resolve` * rejected:失敗狀態,對應callback function為`rejected` * settled:promise已解決,有結果 與`new`關鍵字搭配的函式稱為建構函式(constructor function) resolve:在promise內,行為成功時呼叫 reject:失敗發生時呼叫 ```javascript! //用new關鍵字創造物件 //Promise後方傳入callback funciton,參數為resolve和reject //resolve和reject也是函式 let p1 = new Promise((resolve,reject) =>{ if(1){ resolve("成功"); }else{ reject("失敗"); } }) let resolveResult = function(a){ console.log(a); } let rejectResult = function(b){ console.log(b); } p1.then(resolveResult,rejectResult); //也可以這樣寫 p1.then(resolveResult).catch(rejectResult); ``` Promise後方接then傳入兩個參數,一個成功函式一個失敗函式, 成功函式會接收到resolve的值作為參數, 失敗函式會接收到reject的值作為參數 Promise有兩種處理錯誤方式 1. 用then的第二個參數接 2. 用catch接 ![圖片](https://hackmd.io/_uploads/Sybqc-eHA.png) then方法內的callback function永遠以非同步呼叫 ![圖片](https://hackmd.io/_uploads/Hkeei9WlSA.png) #### 直接發出成功/直接發出失敗結果的Promise ```javascript! //直接發出成功結果 let P1 = Promise.resolve("直接發出成功"); P1.then(function(result){ console.log(result); //"直接發出成功" }) //直接發出失敗結果 let P2 = Promise.reject("直接發出失敗"); P2.catch(function(error){ console.log(error); }) //Promise.resolve(或resolve)如傳入另一個Promise,會直接解析Promise let P3 = Promise.resolve(5); let P4 = Promise.resolve(P3); console.log("P4",P4); //Promise {<fulfilled>: 5} console.log("P3", P3);//Promise {<fulfilled>: 5} console.log(P3 === P4);//true ``` Chrome瀏覽器印出: ![圖片](https://hackmd.io/_uploads/Skooq-lrR.png) #### then的串聯 ![圖片](https://hackmd.io/_uploads/B18hc-lB0.png) ```javascript! //then方法會創造另一個Promise //第二個then的callback function參數是第一個then的callback function回傳值 let p5 = Promise.resolve(7); p5.then((res)=>{ console.log(res);//7 return res*5; }).then((res) =>{ console.log(res);//35 }) ``` ```javascript! //then的callback function回傳值是另一個Promise,這個Promise直接被解析 let p5 = Promise.resolve(7); let p6 = Promise.resolve("p6"); p5.then((res)=>{ console.log(res);//7 return p6; }).then((res) =>{ console.log(res);//"p6" }) ``` #### Promise.all&Promise.race * 一次有多個promise要同時一起使用 * 把Promise物件組成的陣列 ```javascript! //Promise.all //Promise陣列裡的所有Promise都被解析完畢&狀態fulfilled,主Promise狀態為fulfilled,回傳resolve值組成的陣列 //如有一個是reject狀態,則主Promise狀態為reject,回傳第一個reject的值 let p1 = Promise.resolve(1); let p2 = Promise.reject(2); let p3 = Promise.resolve(3); //p1和p3皆為resolve,回傳為陣列 let successResult = Promise.all([p1,p3]); successResult.then((res) => { console.log("successResult",res); //"successResult" [1,3] }); //p1和p3皆為resolve,p2為reject,則回傳reject的值 let rejectResult = Promise.all([p1,p2,p3]); rejectResult.catch((err)=>{ console.log("rejectResult",err);//"rejectResult" 2 }) ``` ![圖片](https://hackmd.io/_uploads/rk4p9bxHR.png) ```javascript! //Promise.race //Promise陣列裡,只要有一個被解析,不論成功或失敗,就會回傳給主Promise let p4 = Promise.resolve(4); let p5 = Promise.reject(5); let p6 = Promise.resolve(6); //p4和p6皆為resolve,p5為reject //調整陣列裡的順序,結果也不同 //[p4,p5,p6]先取到p4,[p5,p4,p6]先取到p5 let promiseRace = Promise.race([p4,p5,p6]); promiseRace.then((res)=>{ console.log(res); }).catch((err)=>{ console.log(err);//4 }) ``` ![圖片](https://hackmd.io/_uploads/Hysa9ZlHA.png) ### MicroTask微任務 vs MacroTask宏任務 JS主程式是一個MacroTask Queue Promise會產生MicroTask,也有自己的Queue ==每一個MacroTask Queue裡面task結束,優先執行MicroTask Queue所有的task== ![圖片](https://hackmd.io/_uploads/ByJdr4HS0.png) ```javascript! //驗證順序 console.log("主程式開始"); setTimeout(() => console.log("Macrotask Queue")); Promise.resolve().then(() => console.log("Microtask Queue 1")); Promise.resolve().then(() => console.log("Microtask Queue 2")); console.log("主程式結束"); ``` 結果如圖,Microtask都執行完畢才會執行下一個Marcotask ![圖片](https://hackmd.io/_uploads/rkvOHVBrR.png) ### async 使用`async`關鍵字所宣告的函式為非同步函式,一定回傳一個Promise ```javascript! async function asyncFn(){ return 2; } console.log(asyncFn()); //Promise { 2 } ``` ### await * 被使用在async宣告的函式內,使用在一般函式內會報錯 * 須與另一個Promise一起使用 * 用途:等待一起使用的Promise被解析完畢,才繼續往下,並回傳解析結果 ```javascript! let p1 = new Promise((resolve) =>{ setTimeout(()=>{ resolve("成功"); },1000); }); //JS會等待await後方的Promise解析完畢,且回傳結果才繼續往下執行程式 async function asyncFn2(){ let result = await p1; console.log(result);//"成功" } //IIFE let p1 = new Promise((resolve) =>{ setTimeout(()=>{ resolve("成功"); },1000); }); (async function asyncFn2() { let result = await p1; console.log(result);//"成功" })(); asyncFn2(); //也可搭配Promise.all和Promise.race使用 //因為async會回傳promise,透過此方式就能搭配 async function fn5(){ return "fn5"; } async function fn6(){ return "fn6"; } async function getData(){ const asyncResult = await Promise.all([fn5(),fn6()]); console.log(asyncResult); //[ 'fn5', 'fn6' ] } ``` #### Promise錯誤處理(例外處理) * 程式碼邏輯發生預料之外的結果,如取用未宣告的變數 * Promise和async...await都能使用catch接收失敗原因 ```javascript! async function asyncFn2() { test; //未定義 return "有錯誤"; } //async會回傳一個Promise,所以可用catch來捕捉錯誤 asyncFn2().catch((err) => console.log(err)); //ReferenceError: test is not defined //也可使用try...catch捕捉錯誤 let api = () => new Promise((resolve) => { throw "api error"; resolve("api success"); }); async function asyncFn2() { try { let result = await api(); } catch (error) { console.log(error); //"api error" } } asyncFn2(); ``` ### try...catch 避免讓發生的錯誤中斷JS執行 ```javascript! try { let number = 7; number(); }catch(err){ console.log(err); //TypeError: number is not a function } //透過throw主動拋出例外,中止程式碼並跳到catch try { let number = 7; throw "拋出例外!" }catch(err){ console.log(err);//"拋出例外!" } ``` ## CH7 物件 物件是一連串key-value(鍵與值)的配對 物件屬性又稱鍵值(key),是**字串型別** new是一個運算子,它**創造物件** ```javascript! //有寫new關鍵字 let obj = new Object(); console.log(obj);//{} //沒有new關鍵字 let obj2 = Object(); console.log(obj2); //{} //查看Object是函式型別 console.log(typeof Object); //function ``` ### 建構函式(constructor) / 函式建構式(function constructor) * 用函式建構一個物件,慣例首字以**大寫**來命名 * 只用於與`new`搭配創造物件,不會當作一般函式使用 * 創造的物件須用this動態新增物件屬性, * `new` + 建構函式 = 物件,此物件有prototype連結 ```javascript! //先建構一個函式,第一個字大寫 function Bible(){ this.god = "Jesus", this.apostle = "Paul" } const newTestament = new Bible; //如果沒有加new會當作一般函式執行 console.log(newTestament); //{"god": "Jesus","apostle": "Paul"} ``` ![image](https://hackmd.io/_uploads/rkgBDicU2A.png) 物件字面值(object literal) / 物件實字:用大括號`{}`建立物件 ```javascript! //存取物件方法有兩種 let person = { name:"David", age:18, 1:"Tainan", $US:777, _list:"3c" } //1.點記法:存取物件屬性 console.log(person.name); //David //可以使用字母、數字、$(錢字號)或 _(底線) console.log(person.$US); //777 console.log(person._list); //"3c" //但存取數字開頭,會報錯 console.log(person.1); //SyntaxError: missing ) after argument list //2.中括號[]:存取物件的鍵值(key) console.log(person["name"]); //"David" //存取沒有任何限制,因為[]裡面是放字串 console.log(person['name']); //"David" console.log(person["1"]); //"Tainan" console.log(person["$US"]); //777 console.log(person["_list"]); //"3c" //中括號裡面的字串可以用變數來表示 const property = "name"; const name = person[property]; console.log(name); //"David" ``` ### hasOwnProperty 檢查物件裡面明確定義的屬性 ```javascript! const food = { japanese: "sushi" }; const foodName = food.hasOwnProperty("japanese"); console.log(foodName); //true ``` ### in運算子 檢查物件所有的屬性,包含繼承來的 ```javascript const food = { japanese: "sushi" }; //in運算子 console.log("japanese" in food); //true //hasOwnProperty屬性沒有明確存在food物件,是繼承而來的 console.log("hasOwnProperty" in food); //true ``` ```javascript! //判斷物件某屬性是否有內容,可用強制轉型布林值 console.log(!!food.japanese); //true ``` ### 巡訪物件 陣列有依序存取的方法,物件也有,須透過預設物件`Object` * `Object.keys`: 把物件自有的屬性(非繼承的)抽出並放到**字串陣列**,再透過陣列方法存取屬性,**適用於屬性具有相同概念或意義** ```javascript! const food = { japanese: "sushi", korea: "kimchi" } const objectKeyArray = Object.keys(food); console.log(objectKeyArray); //["japanese","korea"] ``` * `Object.values`: 取物件屬性的內容,抽出並放到**字串陣列** ```javascript! let drinkItems = { 10: "tea", 15: "coffee", 20: "milk" }; //Object.values取物件屬性的內容,並做字串陣列 function checkItem(value){ let drinkValue = Object.values(drinkItems); // console.log(drinkId); //["tea","coffee","milk"] let result = false; if(drinkValue.indexOf(value) >= 0){ //indexOf如找不到對應元素,回傳-1 result = true; } return result; } console.log(checkItem("milk")); //true ``` * `Object.entries`: 同時取物件屬性和內容,結合Object.keys和Object.values的方法,回傳為陣列,陣列裡的每一陣列包屬性的字串和內容的字串 ### 物件解構賦值 使用解構賦值取出物件屬性並宣告,依據物件有多個屬性可宣告多組變數 ```javascript! let drinkItems = { first: "tea", second: "coffee", third: "milk" }; //過去取物件屬性的值 let name = drinkItems["first"]; console.log(name); //"tea" //解構賦值 let { second } = drinkItems; console.log(second); //"coffee" //依據物件屬性可同時宣告多個變數 let { first, third } = drinkItems; console.log(first, third); //"tea" "milk" //大括號內的變數,如沒有存在就像變數未宣告,得到undefined let { first, third, fourth } = drinkItems; console.log(fourth); //undefined //可以先給定預設值 let { first, third, fourth = "juice" } = drinkItems; console.log(fourth); //"juice" ``` 如多組物件屬性相同,在解構賦值時可以加上**別名**,避免重複宣告 ```javascript! //在解構賦值用冒號加上別名 let personA = { name:"Peter", age:20 } let personB = { name:"David", age:24 } let { name: infoA } = personA; console.log(infoA); //"Peter" let { name: infoB } = personB; console.log(infoB); //"David" ``` ### 陣列解構賦值 陣列只有索引,在解構賦值時,變數對應的位置很重要 ![image](https://hackmd.io/_uploads/r143sc830.png) ```javascript! //第一個變數對應陣列第一個值 let score = [80,63,97,49,77]; //第一個變數對應陣列第一個值,以此類推 let [firstScore,secondScore] = score; console.log(firstScore,secondScore); //80 63 ``` ### 複製物件 避免物件傳參考被修改,創造與物件完全相同屬性的全新物件 #### 1.展開 使用展開運算子`...`,把物件的所有屬性,放到另一個物件裡,等於複製一個同樣屬性的物件 ```javascript! let dog = { name:"corgi", weight:20, } //複製同樣屬性到另一個物件,修改也不會動到dog物件 let newDog = { ...dog } newDog.name = "shiba"; newDog.weight = 18; console.log(dog); //{"name": "corgi","weight": 20} console.log(newDog); //{"name": "shiba","weight": 18} //合併物件如有相同屬性,後者屬性內容蓋掉前者 let orangeCat = { name: "neko", age: 1, addr: "Taipei" }; let blackCat = { name: "kuro", age: 3, color: "black" }; let mergeObj = { ...orangeCat, ...blackCat }; console.log(mergeObj); //{"name": "kuro","age": 3,"addr": "Taipei","color": "black"} ``` ```javascript! //展開也能使用在陣列 //複製一個陣列 let array = [1,2,3]; let copyArray = [...array]; console.log(copyArray); //[1,2,3] //合併兩個不同陣列 let array1 = ["a","b","c"]; let array2 = ["d","e","f"]; let mergeArray = [...array1,...array2]; console.log(mergeArray); //["a","b","c","d","e","f"] ``` #### 2. Object.assign ```javascript! //Object.assign(目標物件,來源物件) //把來源物件的所有屬性(不包含繼承)複製到目標物件 let person1 = { name: "Peter", gender: "male" }; let person2 = { age: 33 }; let result = Object.assign(person1, person2); console.log(result); //{"name": "Peter","gender": "male","age": 33} //可以有多個來源物件,屬性與內容後者覆蓋前者 let newObj = Object.assign({name:"小安"},{name:"小白"},{name:"小黑"}) console.log(newObj); //{name:"小黑"} ``` #### 淺拷貝 shallow copy ```javascript! let family = { members:{ dad:"小黑", mom:"小花", son:"小白" }, addr:"Tainan", } let newFamily = {...family}; //新物件修改第二層物件,原物件的也會被修改 newFamily.members.son = "大白"; console.log("newFamily",newFamily); //{"members": { "dad": "小黑","mom": "小花", "son": "大白"},"addr": "Tainan"} console.log("family",family); //表示來自同一個記憶體位置 console.log(family.members === newFamily.members); //true ``` #### 深拷貝 deep copy 1. 用JSON格式先轉成類似純文字,再轉成物件 2. Object.create ```javascript! //深拷貝 //JSON.stringify先轉成字串 //再用JSON.parse轉物件 let deepFamily = JSON.parse(JSON.stringify(family)); console.log(deepFamily); ``` ### 屬性描述器 Property Descriptor (實戰上較不會使用到) 物件屬性有附加資訊,需透過屬性描述器才能得到或修改 [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/OwnPropertyDescriptor) ![image](https://hackmd.io/_uploads/rJ0khcL2A.png) * value:物件屬性內容 * writable:屬性內容是否可變更 * enumerable:使用巡訪方法(`Object.keys`/`Object.values`/`Object.entries`)能不能被列舉 * configurable:屬性描述器能否被修改 * get:物件屬性上的getter函式,**取用**物件屬性 * set:物件屬性上的setter函式,**指派**物件屬性 ```javascript! let country = { a:"Japan", b:"Korea", c:"America" } //用getOwnPropertyDescriptor查看屬性描述器內容 let descriptor = Object.getOwnPropertyDescriptor(country,"a"); console.log(descriptor); /*{ "value": "Japan", "writable": true, "enumerable": true, "configurable": true } */ //使用`Object.defineProperty`來改變物件屬性的附加資訊 Object.defineProperty(country,"a",{ value:"Japan", writable:false, //改成false無法寫入 enumerable:true, configurable:true }); country.a = "Taiwan"; //改成"Taiwan" console.log(country.a); //印出仍"Japan" ``` * 存取描述器:描述器上有Get和Set設定,value和writable的值會被忽略,會以get和set函式的內容為主 * 資料描述器:描述器上沒有Get和Set設定 ![image](https://hackmd.io/_uploads/H1L-3cUh0.png) ### This * 方便從執行環境內部取得外部物件 * 在呼叫函式時,透過不同方式決定它要指向哪個物件 this的binding(綁定):指向哪一個物件 #### 預設的綁定 函式直接被呼叫,函式裡的this指向全域 ```javascript! function callInfo() { console.log(this.message); } var message = "global"; callInfo(); //"global" ``` #### 隱含的綁定 this會指向到「呼叫這個函式的物件」 ```javascript! //隱含的綁定 //this會指向到「呼叫這個函式的物件」 function callInfo() { console.log(this.message); } var family = { message: "Peter", callInfo: callInfo, }; family.callInfo(); //"Peter" callInfo(); //undefined,在全域呼叫this會指向window,window底下找不到message ``` 隱含的綁定消失 ```javascript! //範例1 //把函式指派給另一個變數,透過變數呼叫,對於JS來說變成一般函式呼叫,所以指向全域window function callInfo() { console.log(this.message); } var family = { message: "Peter", callInfo: callInfo, }; var result = family.callInfo; result(); //"global" ``` 範例2 函式裡的第二層函式,被當作一般函式呼叫,this會指向全域 ![image](https://hackmd.io/_uploads/H1sGn5InR.png) ```javascript! var userInfo = { name:"小花", changeName:function (){ this.name = "小黑"; //由小花改成小黑 console.log("1",this.name); //"小黑",因為物件的name已被改成小黑關係 function changeNewName(){ //此函式被當作一般函式執行 console.log("2",this.name); //"global" this.name ="小白"; } changeNewName(); console.log("3",this.name); //"小黑",因為物件的name已被改成小黑關係 }, }; var name = "global"; userInfo.changeName(); console.log("4",name); //"小白",因為全域被changeNewName函式修改 ``` #### 明確的綁定 所有函式都有`call`和`apply`,==指定this要指向的物件== ```javascript! //明確綁定 call&apply //call(指定this指向的物件 , 傳入該函式的參數) //apply(指定this指向的物件 , 以陣列方式傳入該函式的參數) let fruits = { summer:"durian", winter:"strawberry" } function buyFruits(){ console.log(this.summer); } buyFruits(); //undefined buyFruits.call(fruits); //"durian" buyFruits.apply(fruits); //"durian" //硬綁定 bind //會回傳一個新的函式,確定這函式的this不會被call和apply修改 let objectA = { a:"a" } let objectB = { b:"b" } function buySomething(){ console.log(this); } buySomething(); //window let buyFruit = buySomething; buyFruit(); //window let buyDrink = buySomething.bind(objectA); buyDrink(); //{"a": "a"} buyDrink.call(objectB);//{"a": "a"} ``` #### new運算子綁定 函式內的this會綁定到新創造物件上 ### 箭頭函式 **箭頭函式的this會依據箭頭函式的程式碼位置而定** 把callback function改成箭頭函式,防止this隱含的綁定消失,就不用使用that或是self ```javascript! let obj = { func:function (){ console.log("func",this); }, arrowFunc:() => { console.log("arrowFunc",this) } } console.log(obj.func()); //this指向obj,印出obj內容 console.log(obj.arrowFunc()); //指向window //書本範例 let userInfo = { name: "小黃", changeName: function () { this.name = "小黑"; console.log(this.name); //"小黑" let changeOtherName = () => { this.name = "小白"; console.log(userInfo.name); //"小白" }; setTimeout(changeOtherName,1000); }, }; userInfo.changeName(); //"小白" ``` # CH8 原型與物件 ## 物件與類別 物件導向語言有類別(class)和物件(object) ![image](https://hackmd.io/_uploads/SJwVhqU3R.png) 類別與物件的關係: * 類別class:定義某件事或某個功能的基本概念,就像是建築的設計藍圖 * 物件object:透過類別的描述的內容**實現的東西**,像是建築物 ### 物件導向的繼承關係 物件導向的繼承(inheritance):指類別以另一個類別為基礎,進行擴充、修改,==讓某些屬性可以共用,且可以減少重複== 例如:所有動物的共用行為有「呼吸」,而鳥類有「飛行」的行為(呼吸屬性共用,飛行行為擴充) ![image](https://hackmd.io/_uploads/Sy2H35U2A.png) ## 原型 prototype JS裡面沒有類別繼承,而是利用物件之間的「原型prototype」達成類似繼承的效果,JS每個物件都有prototype可以與其他物件共用屬性與方法。 說JS是物件導向,物件原型導向更貼切。 JS的物件在存取屬性有預設行為,如在此物件找不到某屬性,就會==透過`[[Prototype]]`屬性往上查找==,連結到原型物件是否有同樣屬性。 ![image](https://hackmd.io/_uploads/ryIL39UnA.png) > 自己理解: > 如圖片8-3,anObject物件本身只有a屬性,但prototype有b屬性。 > > 當anObject.b時,因本身沒有此屬性,此時就會會透過prototype往上查找到b,console.log(anObject.b)就會是"property b" 創造新物件的方法: * `Object.assign`:可接收並合併多組物件。 * `Object.create`:執行完只會收到一個空的新物件,而傳入的物件作為基礎,創造新物件。 > 個人理解:也就是空的新物件繼承傳入的物件 ```javascript! const obj = { name:"John", age:20 }; const newObj = Object.create(obj); console.log(newObj); //{},空的新物件,已繼承obj物件 console.log(newObj.name); //拿到原型物件obj的name屬性"John" //用Object.getPrototypeOf查找物件的原型 console.log(Object.getPrototypeOf(newObj)); // {"name": "John","age": 20} //所以可以知道obj是newObj的原型 ``` ### 建構函式上的 prototype 屬性 在物件導向有「類別」概念,JS只有物件,只能用建構函式來模擬類似類別的效果。 JS所有函式包含建構函式都有prototype屬性,==使用建構函式創造物件時,建構函式上的prototype會跟所創造的物件原型[[Prototype]]做連結==。 ![image](https://hackmd.io/_uploads/B1Fi3qUn0.png) 每個物件都有一個隱藏屬性[[Prototype]],當建構函式創造物件時,此物件的[[Prototype]]會與建構函式的prototype連結。 ```javascript! function UserInfo(name){ this.name = name; } const user = new UserInfo("Peter"); //驗證:物件的[[Prototype]]和建構函式的prototype是一樣的 Object.getPrototypeOf(user) === UserInfo.prototype; //true ``` ### 物件上的__proto__屬性 物件內部有一個不可取得的原型屬性:`[[Prototype]]` JS在每個物件上都提供了 `__proto__` 屬性,讓我們可以觀察物件原型,==代表該物件原型的連結==,==等同 Object.getPrototypeOf== > 這個屬性不在ECMA記載,大多數瀏覽器都還是實作這屬性(可以用,但是不標準) > > 所以在官方不推薦使用!若想要取得物件原型,推薦使用 `Object.getPrototypeOf` ```javascript! function UserInfo(name){ this.name = name; } const user = new UserInfo("Peter"); //驗證:物件的__proto__同樣指向建構函式的prototype user__proto === UserInfo.prototype; //true // __proto__ 等同 Object.getPrototypeOf user.__proto__ === Object.getPrototypeOf(user); //true ``` 圖片說明 ![image](https://hackmd.io/_uploads/H1Y635LnA.png) ### 原型繼承 JS透過**建構函式**模擬「類別」,透過**原型**模擬「繼承」 可以透過類別來創造許多相同規格的物件(如下圖) > ![image](https://hackmd.io/_uploads/H16kT582A.png) > 每個由UserInfo建構函式創造的物件,都能使用原型與建構函式上的prototype屬性產生連結。 > > 也就是說,當我們改變建構函式prototype屬性內容時,被創造出來的物件是可以共享(自己理解:都能夠使用建構函式prototype的方法) ```javascript! //先建立一個建構函式 function UserInfo(name){ this.name = name; } //在UserInfo函式的prototype屬性新增一個setAge的方法,此方法是一個函式 UserInfo.prototype.setAge = function(age){ this.age = age; } //實例化,userA和userB const userA = new UserInfo("Peter"); //實例化,userA物件有name屬性 //在userA使用setAge方法,並帶入數值,此物件新增age屬性 userA.setAge(20); //{name: 'Peter', age: 20} const userB = new UserInfo("Esther"); userB.setAge(30); //{name: 'Esther', age: 30} ``` > Q: 為什麼不要把setAge函式直接放在建構函式裡就好,當實例後每個物件不是都有這個方法嗎?為什麼還要特地放在prototype裡面? > > A: 因為同樣數值或函式會複製好幾次,例如生成1000個物件,JS會要好幾倍的記憶體空間,所以要達到同樣目的,只要放在prototype就可以用較低成本達成。 一般物件導向程式語言的繼承,指的是類別與類別的繼承,而JS的繼承是類別與物件的繼承 ## 原型鍊 JS在物件內找不到某屬性,會往原型物件找相同的屬性,這樣一連串查找關係鍊結就是原型鍊。 ```javascript! //推論:陣列與物件有繼承關係 const array = []; //驗證 console.log(array.__proto__ === Array.prototype); //true ``` 圖片說明: 透過`__proto__`屬性向上查找對應原型屬性 ![image](https://hackmd.io/_uploads/BkpbpcL2C.png) 一般我們使用方括號快速建立新陣列,以及物件使用大括號建立新物件,其實陣列可以使用new Array()來創造,物件用new Object()創造。 全域物件Array本身就是陣列的建構函式。 **來看陣列與物件的關係** ```javascript! const array = []; //創造出的陣列__proto__與Array的prototype console.log(array.__proto__ === Array.prototype); //true //陣列原型物件的原型是Object的prototype console.log(array.__proto__.__proto__ === Object.prototype); //null代表連結到底了,在原型鍊中找不到想要的屬性 console.log(array.__proto__.__proto__.__proto__);// null ``` < 圖片說明 > ![image](https://hackmd.io/_uploads/S1yma582C.png) ## 類別之間的繼承 傳統繼承:透過被繼承的「後代類別」(child class)所產生的物件,一開始就要有「前代類別」(parent class)的屬性與方法。 ```javascript! //範例 function Human(name, age, height) { this.name = name; this.age = age; this.height = height; } function User(firstname, lastname) { this.firstname = firstname; this.lastname = lastname; } Human.prototype.getHumanAge = function () { return this.age; }; User.prototype.getFullName = function () { return this.firstname + this.lastname; }; ``` 上面建立了Human和User兩個類別,**如何讓兩個互不相干的類別共享屬性與方法?** **透過原型鍊實現兩者繼承關係**,才能夠讓繼承類別與被繼承類別共享屬性與方法。 有兩個步驟: 1. 類別之間**原型物件**的繼承:要繼承的類別上的方法,必須與被繼承類別共享。 2. 類別之間**建構函式**的繼承:要繼承類別的建構函式的內容或是定義的屬性,要與被繼承類別共享。 ### 類別之間原型物件的繼承 後代類別產生的物件,若在這物件內或是在原型物件內都找不到此屬性,就往前代類別的原型物件找。 若要達到繼承效果,就必須修改`__proto__`,但不建議直接修改它,因為會破壞物件的預設行為。提供以下方法來修改: ```javascript! User.prototype = Object.create(Human.prototype); ``` `User.prototype`被修改成一個新的空物件,此空物件的原型是指向`Human.prototype`,這樣`User`就繼承`Human`。 但這樣子做會發現一個問題,User上的prototype屬性裡面的`constructor`不見了,`constructor`屬性是函式上prototype裡面的屬性,**預設會指回該函式**。 為了盡量符合預設行為,還是把`constructor`加回去: ```javascript! User.prototype.constructor = User; //把constructor指回User自己 ``` ![image](https://hackmd.io/_uploads/HJK469L2C.png) ```javascript! //第一步驟:用原型共享屬性 function Human(height, race) { this.height = height; this.race = race; } function User(firstname, lastname) { this.firstname = firstname; this.lastname = lastname; } User.prototype = Object.create(Human.prototype); User.prototype.constructor = User; Human.prototype.getHumanHeight = function () { return this.height; }; User.prototype.getFullName = function () { return this.firstname + this.lastname; }; const userA = new User("Fang","Huang"); console.log(userA); ``` ![image](https://hackmd.io/_uploads/Bkfw6c83R.png) 建立UserA新物件,`console.log`出來看看內容,`[[prototype]]`有指向Human,`constructor`屬性也指向User建構函式。 ### 類別之間建構函式的繼承 前代類別的內容出現在後代類別的建構函式所創造的物件裡,有一個很經典的方法,也就是**在User建構函式裡面呼叫Human建構函式**。 ```javascript! //第二步驟:User建構函式裡面呼叫Human建構函式 function Human(height, race) { this.height = height; this.race = race; } function User(firstname, lastname, height, race) { this.firstname = firstname; this.lastname = lastname; Human.call(this, height, race); } const userB = new User("fang", "Huang", "160", "running"); console.log(userB); //User {firstname: 'fang', lastname: 'Huang', height: '160', race: 'running'} ``` User內的this透過call方法綁定到Human建構函式上,當用new呼叫User時,User的this會綁定到新物件上,同時也會透過this將Human建構函式定義的屬性新增到此物件上。 ```javascript! //完整程式碼 //1.原型鍊共享原型內容 //2.建構函式能夠共用屬性的定義 function Human(height, race) { this.height = height; this.race = race; } function User(firstname, lastname, height, race) { this.firstname = firstname; this.lastname = lastname; Human.call(this, height, race); } //要注意prototype修改的位置,放在這裡,後面的getHumanHeight和getFullName才不會不見,因為Object.create會變成空物件 User.prototype = Object.create(Human.prototype); User.prototype.constructor = User; Human.prototype.getHumanHeight = function () { return this.height; }; User.prototype.getFullName = function () { return this.firstname + this.lastname; }; const userC = new User("fang","Huang","160","biking"); console.log(userC); //User {firstname: 'fang', lastname: 'Huang', height: '160', race: 'biking'} ``` > 自己補充: > 注意User.prototype修改的位置,會影響到getHumanHeight和getFullName,一開始放在這兩個函式後面,結果要用時找不到 🙄 ## class語法糖 ES6之後出現class,讓類別功能的程式碼變得更物件導向、更直觀、更好閱讀。 ### Class 基本用法 ==過去建構函式模擬類別來產生物件,現在透過class宣告類別,不需要修改原型物件也能達成繼承。== ```javascript! //原本建構函式寫法 function User(name) { this.name = name; } const user1 = new User("John"); User.prototype.getName = function () { return this.name; }; //class宣告 class User { constructor(name) { this.name = name; } getName(){ return this.name; } } //查看class User的型別,是function! typeof User; //'function' ``` - class是一個特殊函式,不能當作一般函式使用。 - 建構函式的內容,移動到class裡的constructor函式。 - 原本須用prototype來共享方法,在class直接宣告就可以。 - 在class裡面,方法的宣告與一般函式用法不太一樣,它是**宣告函式屬性的縮寫**。?? ```javascript! class User { constructor(name) { this.name = name; } getName(){ return this.name; } //自己理解getName原本是長這樣? getName:function(){ return this.name; } } ``` - 除了constructor函式必須是**建構函式本身的邏輯**,基本上class裡面可隨意新增自己定義的函式屬性。 ### Class 宣告式的防呆機制 使用new運算子搭配建構函式創造實體,有時候忘記要加上new,這樣呼叫函式還是有效,也不會有錯誤提示。 但使用class就必須注意要有new運算子呼叫,否則會回報錯誤。 ```javascript! class User { constructor(name) { this.name = name; } getName() { return this.name; } } //少new運算子會報錯 User("John"); //TypeError: Class constructor User cannot be invoked without 'new' //有new運算子成功創造物件 new User("Esther"); //User{name: 'Esther'} ``` ### 透過 Class 宣告達成類別繼承 class要實現繼承,須搭配另一個關鍵字`extends`。 1. 先建立Human類別: ```javascript! class Human { constructor(race,gender) { this.race = race; this.gender = gender; } getRace() { return this.race; } } ``` 2. 再建立Human的子類別: ```javascript! class Child extends Human { constructor(race,gender,name){ super(race,gender); //代表Human建構函式,須先呼叫 this.name = name; } } const childA = new Child("running","female","Orchird"); console.log(childA); //Child {race: 'running', gender: 'female', name: 'Orchird'} ``` - extends代表Child繼承了Human類別 - 在constructor函式裡面呼叫了super函式,super函式代表了被extends的Human建構函式 (之前需要從後代建構函式裡面呼叫前代建構函式) ![image](https://hackmd.io/_uploads/rJCkAcI2C.png) Child的[[Prototype]]繼承Human,constructor指向class Child > 自己補充: > 如果super函式寫在後代類別屬性後,會報錯 > ```javascript! class Human { constructor(race,gender) { this.race = race; this.gender = gender; } getRace() { return this.race; } } class Child extends Human { constructor(race,gender,name){ this.name = name; super(race,gender); //放在this之後會報錯 } } const childA = new Child("running","female","Orchird"); console.log(childA); //ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor //GPT說明:在 JavaScript 中,必須在後代類別中首先调用 super(),才能存取使用 this 關鍵字。 ``` ### 透過 static 定義靜態方法 靜態方法是物件導向的概念,只能由類別本身取得方法,類別所產生的物件實例無法取得。 ```javascript! //未加上static靜態方法 class User{ constructor(name){ this.name = name; } getHeight(){ return "more than 160"; } } //此時類別產生的物件adult可以使用class上的getHeight方法 const adult = new User("Vicky"); adult.getHeight(); //'more than 160' ``` ```javascript! //加上static靜態方法 class User{ constructor(name){ this.name = name; } static getHeight(){ return "more than 160"; } } //此時類別產生的物件adult無法使用class上的getHeight方法 const adult = new User("Vicky"); adult.getHeight(); //TypeError: adult.getHeight is not a function //User類別本身可以取得 User.getHeight(); //'more than 160' ``` > 不太知道何時會需要static? ### class 建構子內的super super代表前代類別,所以在後代類別一定要呼叫super才能完成屬性繼承。 在==class內定義的其他方法會被定義在原型物件內。== ```javascript! class Human { constructor(gender) { this.gender = gender; } getGender() { return this.gender; } } class Girl extends Human { constructor(gender, name) { super(gender); this.name = name; } getGender() { return super.getGender(); //取得Human的getGender方法 } } ``` ## JavaScript 內建物件 8-31~8-37 ing