# ES6 > - ECMAScript(ECMA) 是一種標準 > - JS 是實現此標準的其中一種語言 > - ECMA 每年六月發佈一次版本 > - 201506 -> ES2015(ES6) > - 201606 -> ES2016 > - ... > - ES6 指 ES2015 以後的版本 > 兼容性: https://kangax.github.io/compat-table/es6/ > - IE10, FF, Chrome, Mobile, NodeJS ## 變量 > - var問題: 可以重複定義, 不能限制修改, 只有函數級作用域(沒有塊級作用域) > - let: 不能重複定義, 變量(可以修改), 塊級作用域 > - const: 不能重複定義, 常量(限制修改), 塊級作用域 ### let > - let 是用來聲明變量的 > - 不可以重複聲明 ```javascript= var b; var b; let a; let a; // Identifier 'a' has already been declared ``` > - 不會 hoisting > - 先聲明後使用 ```javascript= console.log(a); var a = 10; // undefined console.log(b); let b = 10; // Cannot access 'b' before initialization ``` > - 在全局作用域下聲明, 不會給 window 添加屬性 ```javascript= let a = 1; var b = 1; console.log(window.a); // undefined console.log(window.b); // 1 ``` > - 在一個 {} 中 let 聲明的變量具有塊級作用域 ( 只有在 {} 裡面才能訪問到該變量 ) > - var 不具備此特點 ```javascript= if (true) { var a = 100; let b = 100; } console.log(a); // 100 console.log(b); // b is not defined ``` ```javascript= // 防止循環變量變成全局變量 for (var i = 0; i < 3; i++) {} console.log(i); // 3 for (let j = 0; j < 3; j++) {} console.log(j); // j is not defined ``` > - 暫時性死區 > - 在塊級作用域 let 聲明變量, 會讓該作用域綁定變量, 百毒不侵 ```javascript= var a = 10; if (true) { console.log(a); // Cannot access 'a' before initialization let a = 10; // -> 在塊級 let 聲明後, 外面的變量就不能用了 } ``` > - 經典題 ```javascript= var arr = []; for (var i = 0; i < 2; i++) { arr[i] = function () { console.log(i); } } arr[0](); // 2 arr[1](); // 2 // var 沒有塊級作用域的概念, 也就是說兩個函數輸出的 i 都是全局的 // 所以調用時, i 已經 = 2 而跳出循環, 故兩次輸出都是 2 ``` ```javascript= let arr = []; for (let i = 0; i < 2; i++) { arr[i] = function () { console.log(i); } } arr[0](); // 0 arr[1](); // 1 // 因為for循環的產生了兩個塊級作用域, /* { let i = 0; arr[i] = function () { console.log(i); } } { let i = 1; arr[i] = function () { console.log(i); } } */ // 這兩個 i 是兩個變量, 他們在不同的作用域裡, 互不影響 // 函數調用時, 由於函數內沒有聲明 i 而往上一層作用域查找, 故為 0 跟 1 ``` ### const > - 聲明常量(不能變) > #### 聲明具有塊級作用域 ```javascript= if (true) { const a = 10; if (true) { const a = 20; console.log(a); // 20 } console.log(a); // 10 } console.log(a); // a is not defined ``` > #### 聲明時, 必須附值 ```javascript= const PI; // Missing initializer in const declaration ``` > #### 常量聲明不可更改 > - 效率高, 因為JS解析引擎不用一直監控值的變化 > - 簡單數據類型: 不可改值 ```javascript= const PI = 3.14; PI = 100; // Assignment to constant variable. ``` > - 複雜數據類型: 不可改址 ```javascript= const ARR = [100, 200]; ARR[0] = 1; console.log(ARR); // (2) [1, 200] ARR = ['a', 'b']; // Assignment to constant variable. ``` ### 塊級作用域 > #### 一個 {} 就是一個塊級作用域 > - `if(){}` `for(){}` `function(){}` `let obj = {}` `{}` > - 對象的注意點 ```javascript= // 對象 let a = { 'a': 1, 'b': 2, } // 對象 ({'a':1, 'b':2}) // 一般的塊級作用域 { let a = 1; let b = 2; } // 對象的 { 不能放在行首, 否則就不是表示一個對象 { 'a': 1, // Unexpected token ':' 'b': 2, } ``` > - eval 注意點 ```javascript= eval('{"a":1, "b":2}'); // Unexpected token ':' // 因為 {"a":1, "b":2} 無法表達為一個對象, 造成格式錯誤 ``` ```javascript= // 解決 console.log(eval('({"a":1, "b":2})')); // {a: 1, b: 2} eval('var a = {"a":1, "b":2}'); console.log(a); // {a: 1, b: 2} ``` > #### 塊級作用域下的聲明 > - 在塊級作用域下聲明 var 跟 function 依舊是全局的 > - 在塊級作用域下聲明 let const 是私有的 ```javascript= { var a = 1; function b(){}; let c = 2; } console.log(a); // 1 console.log(b); // ƒ b(){} console.log(c); // c is not defined -> 外面訪問不到 ``` > - if 跟 for 的問題 ```javascript= console.log(a,b); // undefined undefined if (0) { console.log(1); // 沒有 var a = 1; function b(){}; let c = 2; } console.log(a,b); // undefined undefined console.log(c); // c is not defined // 你都沒有進去 if , 怎麼會有聲明 !!! ``` ```javascript= console.log(a,b); // undefined undefined if (1) { console.log(a,b); // undefined ƒ b(){} -> 一進去先對 function 附值 var a = 1; function b(){}; let c = 2; } console.log(a,b); // 1 ƒ b(){} console.log(c); // c is not defined // 以前 function 跟 var 的運作模式: // 1. hoisting 聲明 // 2. 一進到塊級作用域 function 馬上附值 // 3. 走過 var 的附值表達後才對 var 剛剛聲明的附值 // let 跟 const: 先聲明後使用 ``` > - 綁定點擊 ```htmlmixed= <body> <ul id="tmp"> <li>1</li> <li>2</li> <li>3</li> </ul> <script src='test.js'></script> </body> ``` ```javascript= // 以前的寫法 var lis = document.getElementById('tmp').getElementsByTagName('li'); for (var i = 0; i < lis.length; i++) { /* 出問題 lis[i].onclick = function () { console.log(i); // 3 // 1. 事件屬於異步, 點擊的時候 for 早就跑完了 // 2. var 在塊級作用域下聲明還是全局的, // 每次循環都把上一次的 i 值給改了, 循環完為 3 } */ /* 解法一 lis[i].index = i; lis[i].onclick = function () { console.log(this.index); } */ // 解法二 (function (i){ lis[i].onclick = function () { console.log(i); } })(i) } console.log(i); /* 拆分每次循環 var i = 3; // 0 -> 1 -> 2 -> 3 { lis[0].index = 0; lis[0].onclick = function(){console.log(this.index);} } { lis[1].index = 1; lis[1].onclick = function(){console.log(this.index);} } { lis[2].index = 2; lis[2].onclick = function(){console.log(this.index);} } - 如果 console.log(i) -> 每個作用域裡都沒有 i 而往全局找 -> 3 - 一般 function 會拆出來, 否則創建了三個重複的 function */ ``` ```javascript= // let 寫法 let lis = document.getElementById('tmp').getElementsByTagName('li'); for (let i = 0; i < lis.length; i++) { lis[i].onclick = function () { console.log(i); } } console.log(i); // i is not defined /* 每次 for 循環都會創建一個作用域, let 聲明會被綁在作用域裡 { let i = 0; lis[0].onclick = function () {console.log(i)}; } { let i = 1; lis[1].onclick = function () {console.log(i)}; } { let i = 2; lis[2].onclick = function () {console.log(i)}; } - console.log(i) 在對象本身就有了, 所以直接拿, 外面反而沒有~ */ ``` ## 小結: var, let, const 區別 > - var > - 函數級作用域 > - hoistiong > - 值可改 > - let > - 塊級作用域 > - 沒有 hoistiong > - 值可改 > - const > - 塊級作用域 > - 沒有 hoistiong > - 值不可改 > - 聲明必須附值 ## 解構 ### 陣列解構 > - 左右結構需一樣 > - 定義與賦值同步完成 ```javascript= // 定義賦值 同步完成 let [a,b] = [1,2]; console.log(a,b); /* 不同步 let [c,d]; // Missing initializer in destructuring declaration [c,d] = [1,2]; */ ``` > #### 解構附值 ```javascript= let arr = [1,2,3]; // 以前提取方法 console.log(arr[0], arr[1], arr[2]); // 1 2 3 // ES6 允許直接讓變量直接從陣列中提取值 [a,b,c,d] = arr; console.log(a,b,c,d); // 1 2 3 undefined let [e,f,g] = [1,2,3]; console.log(e,f,g); // 1 2 3 ``` > #### 設置默認值 > - 當聲明的變量拿到 undefined 時, 才會執行默認值的表達式 ```javascript= // 設置默認值 let [x,y=10] = [1,2]; console.log(x,y); // 1 2 let [a, b=(function () {console.log('haha');return 10})()] = [1, 2]; console.log(a,b) // 1 2 -> 沒有執行 b 的默認值的表達式 let [i, j=(function () {console.log('haha');return 10})()] = [1]; console.log(i,j) // haha -> 給 b undefined 時, 才會執行默認值的表達式 // 1 10 ``` > #### 省略附值 ```javascript= let [,,x] = [1,2,3]; console.log(x); // 3 ``` > #### 剩餘參數附值 ```javascript= let [x,...y] = [1,2,3]; console.log(x,y); // 1 [2, 3] ``` ### 對象解構 > #### 對象解構附值 ```javascript= // a c 是屬性名, b d 是變量名 // 將屬性對應的值賦予給變量 b d let {a:b, c:d} = {a:1, b:2, c:3, d:4}; console.log(b,d); // 1 3 console.log(a,c); // a is not defined -> a c 不是變量, 是對象的屬性名 // 好記得方法: 取別名 // 把 {a:1, c:3} 的變量名取成 b 跟 d ``` > #### 簡寫 > - 如果變量名跟屬性名相同, 可以縮寫 ```javascript= let {a,b} = {a:1, b:2}; // = {a:a, b:b} console.log(a,b); // 1 2 -> 提取的是後面的變量名 a b ``` > #### 設置默認 > - 跟陣列邏輯相同, 給 undefined 才走默認值 ```javascript= let {x,y=10} = {x:1}; console.log(x,y); // 1 10 let {a,b=10} = {a:1, b:undefined}; console.log(a,b); // 1 10 ``` > #### 混搭 ```javascript= let {x,y=10,z:[a,b,c]} = {x:1,z:[1,2,3]}; console.log(x,y); // 1 10 console.log(a,b,c); // 1 2 3 console.log(z); // z is not defined -> 屬性名 ``` > #### 注意 ```javascript= let x,y; [x,y] = [1,2]; // 這樣做沒有問題 console.log(x,y); // 1 2 let a,b; /* {a,b} = {a:1, b:2}; // Unexpected token '=' // 這樣做就有問題了, 因為 {a,b} 沒有被視為一個對象! */ ({a,b} = {a:1, b:2}); // 整個包起來就沒問題了 console.log(a,b); // 1 2 ``` ### 其他情況 > #### 右邊附值非陣列或對象 > - 會自動將值轉為對象 > - 注意 > - 陣列附值只接受有 length 的偽陣列 > - 對象在取值時, 必須要有對應的屬性, 否則 undefined ```javascript= // 陣列 let [a,b] = '12'; console.log(a,b); // 1 2 // let [c] = 1; // 1 is not iterable // console.log(c); // 因為 console.log(Object('12')); // String {"12"} // 0: "1" // 1: "2" // length: 2 -> 有 length // __proto__: String // [[PrimitiveValue]]: "12" console.log(Object(1)); // Number {1} -> 沒有 length // __proto__: Number // [[PrimitiveValue]]: 1 // 對象 let {d:e,f:g} = '12'; console.log(e,g); // undefined undefined -> 都沒有對應的鍵值 let {h:i} = 1; console.log(i); // undefined let {__proto__:j} = '12'; let {__proto__:k} = 1; console.log(j,k); // String{} Number{} ``` ### 函數參數 ```javascript= // 陣列參數 function fn(a,b,...c) { console.log(a,b,c); } fn(1,2,3,4,5); // 1 2 [3, 4, 5] // 對象參數 function fn2({a,b}) { console.log(a,b); } fn2({a:1, b:2}); /* 注意1. function fn3({a=1,b=2}){ console.log(a,b); } fn3(); // Cannot read property 'a' of undefined */ // 原因: // - 因為 fn3() 沒有傳參 // - 導致參數變成 {a=1, b=2} = undefined, // - 而在 undefined 裡當然找不到 a b 屬性 // 測試: // cosole.log(let {q=1} = undefined); // Cannot read property 'q' of undefined function fn4({a=1, b=2}={}) { console.log(a,b); } function fn5({a,b}={a:1, b:2}) { console.log(a,b); } fn4(); // 1 2 fn5(); // 1 2 fn4({}); // 1 2 fn5({}); // undefined undefined // 注意2. // - 因為傳了一個對象實參進去後, 形參不走默認值了 // - 變成 {a,b}={} // - 所以聲明了 a b 後, 沒有附值 ``` ## 箭頭函數 `(形參)=>{函數體}` > - 用來簡化函數定義 > - 箭頭函數是匿名的, 所以都會附址給變量 > - 如果參數只有一個, () 可省 > - 如果函數體只有一句話, 且那句話是 return, {} 可省 ```javascript= // 一般函數定義 let fn = function (a) { return a } // 箭頭函數定義 let fn2 = (b) => { return b } // 如果函數體只有一句話, 且該句的執行解果剛好是要 return, 那麼 return 跟 {} 可省 // 如果只有一個參數, 那 () 也可省 let fn3 = c => c console.log(fn(1), fn2(2), fn3(3)) // 1 2 3 ``` > #### this > - 箭頭函數不綁定 this ( 箭頭函數沒有自己的 this ) > - 箭頭函數中的 this 是該函數的上級作用域下的 this > - 即使用 bind 也不會改變箭頭函數的 this > 但是應該沒有人會用箭頭函數又想用 bind 來改ㄅ.. > - 整理 > - 普通函數的 this => 誰調用, this 指誰 > - 箭頭函數的 this => 函數定在哪, this 就指誰 > - bind, call, apply 的 this => 綁誰指誰 > - 簡單一句, 箭頭函式的內外 this 保持一致 ```javascript= // 傳統函數 function fn() { console.log(this); return function () { console.log(this); } } obj = { fn: fn, } obj.fn()(); // {fn: ƒ} // obj.fn()調用後, 根據誰調用指誰的定律, this 指向 obj, 並返回一個函數 // Window // 返回一個函數後再調用, 此時前面沒有 . 表示是被省略 Window 調用的 ``` ```javascript= function fn() { console.log(this); return () => { // 箭頭函數綁在這個fn() 函數裡, // 所以他的 this 視 fn() 的 this 而定 console.log(this); } } let obj = { fn: fn, } obj.fn()(); ``` ```javascript= // 這個案例應該更清楚些 /* document.onclick = ()=>{ console.log(this); // 這個 this 寫在全局, 所以 this 指向 Window } */ document.onclick = function () { console.log(this); // 這個 fn 被 document 調用, // 所以 this 指向 #document } ``` > #### 面試題 ```javascript= let obj = { fn: function () { console.log(this); }, fn2: ()=> { console.log(this); }, } obj.fn(); // {fn: ƒ, fn2: ƒ} // function 會產生作用域, 所以 this 指向的是函數調用者 obj.fn2(); // Window // 由於箭頭函數沒有自己的 this, // 又對象不會產生作用域 // 所以fn2被定義在全局作用域 ``` > #### 箭頭函數沒有 arguments ```javascript= let fn = (...args) => { console.log(args); console.log(arguments); } fn(); // [] // arguments is not defined ``` > #### 箭頭函數不能成為構造函數 ```javascript= let F = function () {} console.log(new F); // F {} function F2(){} console.log(new F2) // F2 {} let F3 = ()=>{} console.log(new F3) // F3 is not a constructor ``` ### 剩餘參數 `...args` > - `...` 接收剩餘參數 > - 剩餘參數後面為變量名, 一般都用 args > - 剩餘參數以陣列的形式存取 > - 作用類似 python 的 *args ```javascript= function fn(...args) { console.log(args); } let fn2 = (...apple) => { console.log(apple); // 變量隨便取也沒差, 只是別人要看的懂 } fn(1,2,3); // (3) [1 2 3] -> 以陣列的方式存取沒人接收的參數 fn2(1,2,3); // (3) [1 2 3] ``` ```javascript= // 求和函數練一下 function fn(...args) { let sum = 0; args.forEach(function (v) { // 參數遍歷 sum += v; // 參數加到變量中 }) return sum // 回傳變量值 } // 簡化 let fn2 = (...args) => { // ...args 不能省 let sum = 0; args.forEach(v => sum += v) // 一個參數一句話 -> 省 return sum } console.log(fn(1,2,3)); // 6 console.log(fn2(1,2,3)); // 6 ``` ```javascript= // 搭配解構 let arr = [1,2,3]; let [a, ...b] = arr; console.log(a,b); // 1 [2, 3] ``` ## 擴展運算符 ### 將陣列或對象轉為以逗號分隔的參數序列 > - 剛好與剩餘參數相反: 將逗號分隔的參數序列轉成陣列 > - 講白話就是把 \[] 拆掉, 然後你要幹嘛就幹嘛 ```javascript= let arr = [1,2,3]; console.log(...arr); // 1 2 3 // ...arr = 1,2,3 // console.log(1,2,3) -> 1 2 3 ``` > #### - 應用: 合併陣列 ```javascript= let arr = [1,2]; let arr2 = [3,4]; let arr3 = [...arr, ...arr2]; // ...arr = 1, 2; ..arr2 = 3, 4; console.log(arr3); // (4) [1, 2, 3, 4] ``` ```javascript= // 以前寫法 let arr = [1,2]; let arr2 = [3,4]; // concat 返回一個新陣列, 不改變原陣列 console.log(arr.concat(arr2)); // (4) [1, 2, 3, 4] console.log(arr, arr2); // (2) [1, 2] (2) [3, 4] // ... 搭配 push, push 可以接收多個參數 // push 會改變原陣列, 返回陣列長度 console.log(arr.push(...arr2)); // 4 -> 長度 console.log(arr); // (4) [1, 2, 3, 4] -> 改變原陣列 ``` > #### - 應用: 求陣列最大值 ```javascript= let arr = [11,24,66,4]; console.log(Math.max.apply(Math, arr)); // 66 console.log(Math.max(...arr)); // 66 ``` ### 將偽陣列轉換成陣列 ```htmlmixed= <body> <div></div> <div></div> <div></div> <script src='test.js'></script> </body> ``` ```javascript= console.log([...document.getElementsByTagName('div')]); // [div, div, div] console.log([...'123']); // ["1", "2", "3"] function fn(){ console.log(arguments) console.log(...arguments) console.log([...arguments]) } fn(1,2,3); // Arguments [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ] // 1 2 3 // ... 就是把 [] 給拆了, => console.log(1,2,3) // [1, 2, 3] // console.log([1,2,3]) ``` ## Array 方法 ### 類方法 `console.dir(Array)` ### `Array.of()` > #### 前情提要 > - 所有的類都是函數, 而 `Array()` 會將參數變成一個陣列來返回 ```javascript= console.log(Array('a','b','c')); // ["a", "b", "c"] console.log(Array('c')); // ["c"] console.log(Array(1,2,3)); // [1, 2, 3] console.log(Array(3)); // [empty × 3] -> 問題 // 如果 Array 只有一個參數且為數字, 那會返回 [empty x 數字] ``` > #### `Array.of()` > - 用法跟 `Array()` 一模ㄧ樣, 但解決了這個問題 ```javascript= console.log(Array.of(3)); // [3] ``` ### 陣列的 empty 問題 > #### empty > - 陣列的索引沒有任何值, 即空位 > - `null` , `undefined` 之類的都不是空位 > - 可以使用 in 來驗證有沒有值 ```javascript= let arr = [,undefined,null] console.log(arr.length); // 3 -> 空位還是有算長度的 console.log(0 in arr); // false -> 索引 0 是沒有值的 console.log(1 in arr); // true -> [1] [2] 都有 console.log(2 in arr); // true ``` > #### Js 對 empty 處理 > - ES5 會把空位跳過 > - ES6 會把空位補成 undefined ```javascript= let arr = [,undefined,null] // ES5 會把空位跳過 arr.filter((v,i)=>{ console.log(v,i); // undefined 1 // null 2 // 只循環兩次 }) for (let k in arr){ console.log(k); // 1 // 2 } // ES6 會把空位補成 undefined arr.find((v,i)=>{ console.log(v,i); // undefined 0 // undefined 1 // null 2 }) for (let k of arr){ console.log(k); // undefined // undefined // null } ``` > #### 面試題: > - 得到七個1 的陣列 ```javascript= let arr = Array(7).fill(1); // 產生七個空位陣列, 用 1 補滿 console.log(arr); // [1, 1, 1, 1, 1, 1, 1] ``` ### `Array.from()` `Array.from(arrayLike[, mapFn[, thisArg]])` > - 將偽陣列轉為陣列的方法 > `Array.from([array-like])` => `Array [xx,xx,xx]` > - `arrayLike` : 偽陣列 > - `mapFn` : Array.prototype.map 方法 > - 類似 map 的用法 : 執行callback, 對陣列的值做處理, 返回新陣列 > - `map(callback(curValue[, index[, array]]){}[, thisArg])` > - 因為偽陣列不是陣列, 所以 callback 第三個參數我測試是 undefined > - `thisArg` : 執行方法時, 改變 this 指向 ```javascript= let fakeArr = { '0': 1, '1': 2, 'length': 2, // 偽陣列必須要有這個, 否則轉出來是空陣列 } let obj = {}; let arr = Array.from(fakeArr, v=>v*2); let arr2 = Array.from(fakeArr, function (v, i, arr) { console.log(v, i, arr, this); // 1 0 undefined {} // 1 0 undefined {} // 注意1. // arr 是沒東西的, 只有兩個實參進來 // 注意2. // 如果 function callback 改箭頭函數, // 那第三個參數 (thisArg) 改 this 也沒用, 因為箭頭沒有 this }, obj) console.log(arr); // (2) [2, 4] console.log(arr2); // (2) [undefined, undefined] -> 我沒有 return 東西 // 只要轉換後有 length 幾乎都可以 console.log(Array.from('123')); // ["1", "2", "3"] console.log(Array.from(document.getElementById('tmp').getElementsByTagName('li'))); // [li, li, li] console.log(Array.from(1)); // [] -> 必須要有 length ``` ### 原型方法 `console.dir(Array.prototype)` ### copyWithin `arr.copyWithin(target[, start[, end]])` > - 複製某些位置的值來替換掉某些位置的值 > - 第一個參數為要從第幾個開始替換 > - 第二個參數為從第幾個開始複製 > - 第三個參數為複製到第幾個 (不包括該參數的位置) > - 會改變原陣列 ```javascript= let arr = [0,1,2,3,4,5,6,7,8,9] arr.copyWithin(5,3,5); console.log(arr); // [0, 1, 2, 3, 4, 3, 4, 7, 8, 9] // [0,1,2,3,4,5,6,7,8,9] // param1 = 5 -> 第五個位置開始替換 // param2,3 = 3,5 -> 替換成地 [3~5) 個參數, 意即 [3],[4] arr.copyWithin(7, 3); console.log(arr); // [0, 1, 2, 3, 4, 3, 4, 3, 4, 3] // [0, 1, 2, 3, 4, 3, 4, 7, 8, 9] // 第一個參數為 7 -> 第七個參數開始替換 // 替換成 3, -> 第三個參數沒寫默認為最後, 意即 [3],[4]...[9] // 超過直接截掉 // [0, 1, 2, 3, 4, 3, 4, 7, 8, 9] // 3, 4, 3, 4, 7, 8, 9 // [0, 1, 2, 3, 4, 3, 4, 3, 4, 3] ``` ### Array.prototype.fill() `arr.fill(value[, start[, end]])` > - 以指定值來替換陣列的某些值 > - 第一個參數為指定值 > - 第二三參數為頭尾, 包前不包後, 不寫預設為 0 跟 尾 > - 改變原陣列 ```javascript= let arr = [0,1,2,3,4,5] arr.fill('apple'); console.log(arr); // ["apple", "apple", "apple", "apple", "apple", "apple"] // 沒寫二三參 -> 全改 arr.fill('cat', 2, 4); console.log(arr); // ["apple", "apple", "cat", "cat", "apple", "apple"] // 二三餐包前不包後 [2], [3] ``` ### includes() `Array.prototype.includes(valueToFind[, fromIndex])` > - `fromIndex` 如果給負值, 那就會從 `length+(fromIndex)` 開始找 ```javascript= let arr = [1,3,5,7]; let res = arr.includes(3); console.log(res); // true res = arr.includes(10); console.log(res); // false res = arr.includes(3, 2); console.log(res); // false res = arr.includes(3, -3); // 4 + (-3) = 1 console.log(res); // true // ES5: some() res = arr.some(v => v == 3); console.log(res); // true res = arr.some(v => v == 10); console.log(res); // false ``` > ### callback 系列 > - 每個 function 的 this 都指向 window > - 除了 reduce 跟 reduceRight 以外, 都可以使用第二參數改變 this 指向 ### Arror.prototype.map `arr.map(function (value, item, arrayObj){})` > - 創建一個新陣列, 調用 callback 後, return 的結果一個一個丟進==新陣列== > - 進去幾個出來幾個 ```javascript= let arr = [1,2,3]; // 進去三個 let obj = {}; let arr2 = arr.map(function(v,i,a) { console.log(v,i,a, this) // 1 0 (3) [1, 2, 3] {} // 2 1 (3) [1, 2, 3] {} // 3 2 (3) [1, 2, 3] {} return v * 2 }, obj) console.log(arr2); // (3) [2, 4, 6] //=> 出來三個 ``` ### filter() `arr.filter(callback(element[, index[, array]])[, thisArg])` > - 過濾陣列 > - 遍歷陣列, 每次遍歷如果返回 true 就留下該值, false 就不留 > - 返回一個新陣列, 不改變原陣列 ```javascript= let arr = ['a',1,{a:1}]; arr2 = arr.filter(function (v,i,a) { console.log(v,i,a); // a 1 ["a", 1, {…}] // 1 1 ["a", 1, {…}] // {a: 1} 2 ["a", 1, {…}] return typeof v === 'number' }) console.log(arr); // ["a", 1, {…}] console.log(arr2); // [1] ``` ### find() `Array.prototype.find(callback(v,i,a)[, thisArg])` > - 參數意義都ㄧ樣 > - 找出『第一個』符合條件的(返回 true), 沒有找到 `undefined` > - 找到即返回該『==值==』, 結束循環 > vs. `some()` : 返回 `boolean` ```javascript= let arr = [ { id: 1, name: 'GodJJ', }, { id: 1, name: 'Bebe', }, ] let tmpArr1 = arr.find(v => v.id == 1); let tmpArr2 = arr.find(v => v.id == 2); console.log(tmpArr1); // {id: 1, name: "GodJJ"} -> BeBe 沒返回 -> 找到一個即停止循環 console.log(tmpArr2); // undefined -> 沒找到返回 undefined ``` ### findIndex `Array.prototype.findIndex(callback[, thisArg])` > - 返回 ==第一個== 符合條件的『==索引==』, 沒找到返回 -1 ```javascript= let arr = [1,3,5,7]; let index = arr.findIndex(v => v>=5); let index2 = arr.findIndex(v => v>=10); console.log(index); // 2 console.log(index2); // -1 ``` ### every() `arr.every(callback(element[, index[, array]])[, thisArg])` > - 遍歷陣列, 只要有一個是 false, 則立即返回 fasle ```javascript= let tmp = [1,2,3].every(v=> typeof v === 'number'); console.log(tmp); // true tmp = [1,'3',2].some(v=> { console.log(v); return typeof v === 'number' }) console.log(tmp); // 1 '3' // false ``` ### some() `arr.some(callback(element[, index[, array]])[, thisArg])` > - 遍歷陣列, 只要有一個是 true, 則立即返回 true ```javascript= let tmp = [1,'3',2].some(v=> { console.log(v); return typeof v === 'number' }) console.log(tmp); // 1 // true ``` ### Array.prototype.reduce() `arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])` > - `accumulator` : 累積值 > - 說白了就是一個過程 > - 算術時, 22+24+84+99+... 通常都會兩個兩個加, > - 46+84+99 => 130+99 => 219 > - 而這個兩個兩個加的和, 就是這個參數代表的意義 > - `currentValue` : 當前值 > - `initialValue` : 初始值 > - 返回值為最終的和 > - `Array.prototype.reduceRight()` 一模一樣, 只是是從右邊向左操作 ```javascript= // 有初始值 let tmp = [1,2,3].reduce((prev, item, index, array)=> { console.log(prev, item, index, array); return prev + item },10); console.log(tmp); // 10 1 0 [1, 2, 3] // 11 2 1 [1, 2, 3] // 13 3 2 [1, 2, 3] // 16 // 沒有初始值 -> 少一次循環, 第一個值會被拿去當初始值 tmp = [1,2,3].reduce((prev, item, index, array)=> { console.log(prev, item, index, array); return prev + item }); console.log(tmp); // 1 2 1 [1, 2, 3] // 3 3 2 [1, 2, 3] // 6 // 非數字類型也會做拼接 tmp = ['a','b','c'].reduce((prev, item, index, array)=> { console.log(prev, item, index, array); return prev + item }); console.log(tmp); // a b 1 ["a", "b", "c"] // ab c 2 ["a", "b", "c"] // abc ``` > - 練習: 求一個平均值 ```javascript= let arr = [33,52,34,11]; let ave = arr.reduce((tmp, value, item)=>{ if (item < arr.length-1) { // -1 是因為 tmp 將 [0] 拿來當底了 return tmp + value } else { return (tmp+value) / arr.length } }) console.log(ave); // 32.5 ``` ### Array.prototype.keys() `arr.keys()` > - 返回的是一個迭代器對象 > - 使用方法: 搭配 `for of` 迭代陣列 > - `for of` 為 ES6 的方法, 主要用來遍歷 Array > - vs. `for in` 遍歷 Object ```javascript= let arr = ['a','b','c'] let tmp = arr.keys(); console.log(tmp, arr); // Array Iterator {} ["a", "b", "c"] // - 返回一個陣列迭代器對象 // - 沒有對原陣列幹嘛 // for of 迭代陣列 // 會迭代每個值 // String for (let k of arr) { console.log(k, 'of'); // a of // b of // c of } // for of 使用 keys // 迭代每個索引 // 返回的索引是數字類型 for (let k of arr.keys()) { console.log(k, typeof k, 'of keys'); // 0 "number" "of keys" // 1 "number" "of keys" // 2 "number" "of keys" } // for in 迭代陣列 // 把陣列當成對象的方式來迭代 // 返回的是對象形式的 k:v 的 k // 所以返回的是 string 的 鍵值 for (let k in arr) { console.log(k, typeof k, 'in'); // 0 string in -> '0': 'a' 的 '0' // 1 string in // 2 string in } // for in 使用 keys() -> 沒有反應~~~ for (let k in arr.keys()) { console.log(k, typeof k, 'in keys'); } // 小結: // - 迭代陣列可以使用 for of // - 迭代對象可以使用 for in ``` ### Array.prototype.entries() `arr.entries()` > - ```javascript= let arr = ['a','b','c'] let tmp = arr.entries(); console.log(tmp); // Array Iterator {} -> 返回一個迭代器 // 迭代陣列返回陣列 [index, value] for (let k of arr.entries()) { console.log(k); // [0, "a"] // [1, "b"] // [2, "c"] } // 常用解構來拿值 for (let [i,v] of arr.entries()) { console.log(i,v); // 0 "a" // 1 "b" // 2 "c" } ``` ## Template literals ( Template strings ) > - 使用 `` 來代替 '' 或 "" > - 好處 > - 可以解析變量 `${}` > - 可以多行 > - 可以調用函數 ```javascript= let tempStr = `tmStr`; console.log(tempStr); // tmStr // --------解析變量----------------- // let obj = { name: 'GodJJ', play: 'AD', } // 傳統做法: 使用 + 拼接 console.log(obj.name + '打' + obj.play); // GodJJ打AD // 解析變量, 直接使用 ${} console.log(`${obj.name}打${obj.play}`); // GodJJ打AD // ----------換行--------------- // // 傳統換行: 使用 \n 之類的空白字符 console.log( obj.name + '\n' + obj.age + '歲' + '\n' + '打' + obj.play ) // Template strings 直接換 // 在寫一些 HTML 標籤就很方便 console.log(`${obj.name} ${obj.age}歲 打${obj.play} `) // ---------調用函數----------------- // function fn() { return '打我啊笨蛋' } console.log(`你說: ${fn()}`); // 你說: 打我啊笨蛋 ``` ## string 方法 ### includes `str.includes(searchString[, position])` > - 查找 str 有沒指定 str > - `position` 如果傳非數字, 會轉成數字 `Number()` > - 返回 boolean ```javascript= console.log('abcd'.includes('a')); // true console.log('abcd'.includes('a', 1)); // false console.log('abcd'.includes('a', '1')); // false console.log('abcd'.includes('a', null)); // true -> Number(null) -> 0 ``` ### startsWith() & endsWith() `str.startsWith(searchString[, position])` `str.endsWith(searchString[, length])` > - 判斷是否為 xx 開頭 / 結尾 > - 返回 boolean > - `startsWith` 第二個參數為第幾個位置, 亦即第幾個位置開始是否為 xx > - `endsWith` 第二個參數為長度, 意即 match 的長度 ```javascript= let str = 'GaLaGAGA'; console.log(str.startsWith('GaLa')); // true console.log(str.startsWith('LaGa')); // false console.log(str.endsWith('GAGA')); // true console.log(str.endsWith('LALA')); // false console.log('abcd'.startsWith('b', 1)); // true // 第一位開始的 str ('bcd') 是否為 'b' 開頭 console.log('abcd'.startsWith('b', 2)); // false console.log('abcd'.endsWith('cd', 3)); // false // 查找長度為 3 // 意即 'abc' 的結尾是否為 'cd' -> false console.log('abcd'.endsWith('cd', 4)); // true ``` ### repeat() `str.repeat(次數)` > - 重複 str > - 不可以是 負數 或 Infinity > - 小數向下取整 `Math.floor?` > - (0~-1) 之間, 全部都會取整為 0 ```javascript= console.log('a'.repeat(3)); // 'aaa' // console.log('a'.repeat(-3)); // Invalid count value console.log('a'.repeat(0.3)); // -> 取整數 0 console.log('a'.repeat(1.3)); // 'a' console.log('a'.repeat(-0.3)); // -> 取整 0 ``` ### padStart & padEnd `str.padStart(targetLength [, padString])` `str.padEnd(targetLength [, padString])` > - ES7 的方法 > - 補全字符串到指定長度, 如果長度超過, 返回原字符串 > - 第二參數為指定字符串, 如果沒給預設為 ' ' ```javascript= // 如果沒有給第二參數, 補全 ' ' 到長度 5 console.log('ab'.padStart(5)); // ' ab' console.log('ab'.padEnd(5)); // 'ab ' // 指定 'cd' 補到長度 5, 補滿即返回 console.log('ab'.padStart(5,'cd')); // 'cdcab' console.log('ab'.padEnd(5,'cd')); // 'abcdc' // 指定長度小於原字符串 console.log('abcde'.padStart(3)); // 'abcde' console.log('abcde'.padEnd(3)); // 'abcde' ``` ## Function ### 參數 > #### 參數默認值 ```javascript= // - 以前防止沒傳參數的做法 function fn(x) { x = x || 3; console.log(x); } // 問題: 參數傳轉 Boolean 會變成 false 的值時, 還是會使用預設值 fn(0); // 3 fn(null); // 3 fn(undefined); // 3 fn(''); // 3 fn(NaN); // 3 // - 參數直接寫默認值 // 只有傳 undefined ( 或沒傳 ) 會走默認值 function fn2(x=3) { console.log(x); } fn2(0); // 0 fn2(null); // null fn2(undefined); // 3 fn2(''); // '' fn2(NaN); // NaN ``` > - 一般有默認值得形參都會放後面 ```javascript= // 因為如果我要傳實參給後面的形參, 那這個參數用起來就很麻煩, 因為每次都要傳 undefined function fn3(x=1, y) { console.log(x,y); } // fn3(,10); // Unexpected token ',' fn3(undefined, 10); // 1 10 ``` > #### 剩餘參數 ```javascript= function fn4(...args) { console.log(arguments); // 偽陣列 console.log(args); // 陣列 } fn4(1,2,3); // Arguments [1, 2, 3, callee: (...), Symbol(Symbol.iterator): ƒ] // __proto__: Object // [1, 2, 3] // __proto__: Array(0) ``` > - 參數默認會影響函數的 length 屬性 > - length 紀錄形參個數 ```javascript= function fn(x,y) {} console.log(fn.length) // 2 function fn2(x,y=1){} console.log(fn2.length); // 1 function fn3(x, ...args){} console.log(fn3.length); // 1 -> 剩餘參數也不計在函數長度裡 ``` > - 剩餘參數必須在最後 ```javascript= let fn = (a,b,...args,c)=>{} // Rest parameter must be last formal parameter fn(1,3,55,33,22,44); ``` > - 練習: 搭配拆解陣列來玩 ```javascript= let fn = (...args)=>{ // args = [xx,xx,xx] // 再擴展運算把 [] 拆掉當成調用 fn2() 的參數 fn2(...args); // xx,xx,xx } let fn2 = (...args)=>{ console.log(args); } fn(33,442,2,3); // [33, 442, 2, 3] ``` > #### 參數解構附值 > - 簡單寫一下, 詳細寫在解構附值的地方 ```javascript= // 參數陣列解構 function fn([x,y]) { console.log(x,y) } fn([1,2]) // 1 2 // 參數對象解構 function fn2({a:b, c:d}) { console.log(b,d); } fn2({a:1, c:2}); // 1 2 ``` > #### 參數默認值作用域 ```javascript= var a = 1; function fn(x=a,y=x) { console.log(x,y); var a = 10; } fn(); // 1 1 fn(2); // 2 2 // 調用函數 fn() 創造一個私有作用域後 // - 一開始會在私有作用域聲明形參的變量並賦值 // - 此時會先從實參開始找有沒有 // - 找不到後再去全局找 // - 真的沒有就報錯 => not defined -> 注意是報找不到歐 // fn() 沒有參數, 表示傳了 undefined 進去後, 開始走默認值a, // 此時開始找 a 變量有沒有, 沒有! 所以找全局 a = 1, 故 x = a = 1; // 接著聲明 y , y 也走默認值 x, 而此時私有變量 x 剛剛已經聲明可以用了 // 故 y = x = 1; // fn(2); -> 有對第一個參數傳 2, x 沒有走默認值, 直接拿下 2 => x = 2 // y 走默認值 y = x = 2; ``` ```javascript= // !!! 我還沒搞懂為什麼 !!! var x = 1,y = 2; function fn(x=x,y=y) { console.log(x,y); var x = 10, y = 20; } fn(); // Cannot access 'x' before initialization // 換全局換名字就可以了 // 如果換名字就可以, 表示他是會往全局找的啊, 那為啥... // let z = z // Cannot access 'z' before initialization // 從這個來看, 表示他應該會先處理右邊? // 那參數 x=x 是否應該也是先處理右邊, 而 x 在全局就搜得到了, 所以應該是 x = x = 1 // 但是報錯了~~~~ ``` ```python In [1]: a = 10 In [2]: def fn(a=a): ...: print(a) ...: In [3]: fn() # 10 -> python 正常 ``` ### 函數名 ```javascript= // - 各種創建函數的名稱 function fn() {} console.log(fn.name); // 'fn' let fn2 = function () {} console.log(fn2.name); // 'fn2' console.log((function () {}).name); // '' let fn3 = fn.bind(null); console.log(fn3.name); // 'bound fn' let fn4 = new Function(); console.log(fn4.name); // 'anonymous' // 複習: // new Function('形參', '函數體'); // 面試題: // 有一個字串: let str = '[1,2]'; // 請使用 let arr = new Function() 的方法讓 arr 得到一個陣列 let str = '[1,2,3]'; let arr = (new Function('return' + str))(); console.log(arr); // [1,2,3] // 解答: // (function (){ // return [1,2,3] // })() ``` ## Object ### 簡寫 ```javascript= let a = 10, b = 20; let c = 'wow'; // - 對象裡面的 k:v 簡寫 // 如果k = v, 那就可以寫一個就好了 let obj = {a,b}; // {a:a, b:b} console.log(obj); // {a: 10, b: 20} // - 對象裡面的函數簡寫 let obj2 = { fn(){}, // fn: fn(){} } console.log(obj2); // {fn: ƒ} // - 使用變量的值當屬性名 let obj3 = { c:a, // c: 10 -> 直接寫, 會被拿去轉成字串當屬性名 [c]:a, // wow: 10 -> 找到該變量, 拿值 [c+'IntheWorld']:a // wowIntheWorld: 10 -> 拼接也沒問題~ } console.log(obj3); // {c: 10, wow: 10, wowIntheWorld: 10} // 記法: obj[c] ``` ### 函數 > #### 前情提要 > - `Object()` 本身會將參數做成一個對象返回 ```javascript= console.log(Object(1)); // Number {1} -> 對象 console.log(Object('1')); // String {"1"} -> 偽陣列 console.log(Object(true)); // Boolean {true} -> 對象 console.log(Object(null)); // {} console.log(Object(undefined)); // {} console.log(Object(NaN)); // Number {NaN} console.log(Object([1])); // [1] -> 陣列 console.log(Object({a:1})); // {a: 1} -> 對象 console.log(Object(function(){})); // ƒ (){} -> 函數 ``` > - `console.dir(Object)` ### `Object.is()` > #### 前情提要 > - `===` 的問題 ```javascript= // - NaN不等於任何人 console.log(NaN === NaN); // false // - -0 = 0 console.log(-0 === 0); // true ``` `Object.is(value1, value2);` > - is 解決了這個問題 > - 判斷兩個值是否相同 ```javascript= console.log(Object.is(NaN, NaN)) // true console.log(Object.is(-0, 0)) // false ``` ### `Object.assign()` `Object.assign(target, ...sources)` > - 淺拷貝對象 > - 將後面參數拷貝一份給目標參數, > 如果後面有多個參數, 依序拷貝 > - 屬性名重複, 後值蓋前值 > - 直接改變第一個參數對象 > - 返回第一個參數對象的址 ```javascript= let obj = {a:1}; let obj2 = {a:2, b:20}; let obj3 = {a:3, c:30}; let obj4 = Object.assign(obj, obj2, obj3); console.log(obj, obj2, obj3); // {a: 3, b: 20, c: 30} {a: 2, b: 20} {a: 3, c: 30} // 會直接改變第一個參數的對象值 console.log(obj4); // {a: 3, b: 20, c: 30} => 返回第一個參數的址 ``` > - 深拷貝問題 ```javascript let a = {a:1, b: {c:2}} let b = Object.assign({}, a); b // {a: 1, b: {…}} b===a // false b.b === a.b // true => 淺拷貝 // MDN 作法 let c = JSON.parse(JSON.stringify(a)) c // {a: 1, b: {…}} a === c // false a.b === c.b // false => 深拷貝 ``` ### ES7 對象的擴展運算符 ```javascript= let obj = {a:1}; let obj2 = {a:2, b:20}; let obj3 = {a:3, c:30}; let obj4 = {...obj,...obj2,...obj3}; console.log(obj4); // {a: 3, b: 20, c: 30} ``` ### `Object.getOwnPropertyDescriptor()` `Object.getOwnPropertyDescriptor(obj, prop)` > - 查詢指定對象的指定屬性的描述符 > - 屬性是由 name 跟 屬性描述符 (property descriptor) 組成 > - property descriptor 包含: > - `value` : 該屬性值 > - `writable` : 該屬性值可否更動 > - `get` `set` -> 還沒看懂 > - `configurable` : 該屬性描述可不可被改, 屬性可否被刪除 > - `enumerable` : 該屬性會不會被遍歷 ```javascript= console.log(Object.getOwnPropertyDescriptor('123', 'length')); /* value: 3 writable: false -> 值不能被改, 所我下面試著改值看看 enumerable: false configurable: false */ var str = '123'; console.log(str.length = 0); // 0 -> 我有改歐~ console.log(str); // '123' -> 沒變~~ let arr = [1,2,3]; console.log(Object.getOwnPropertyDescriptor(arr, 'length')); /* value: 3 writable: true -> 可以改 enumerable: false configurable: false */ arr.length = 0; console.log(arr); // [] -> 清掉了~~ ``` ### `keys()` `values()` `entries()` `Object.keys(obj)` `Object.values(obj)` `Object.entries(obj)` > - 返回對象所有==可遍歷==屬性的 鍵/ 值/ \[鍵,值] 組成的陣列 ```javascript= let arr = [1,2,3]; // 這是一個陣列對象 console.dir(arr); // 0: "a" // 1: "b" // 2: "c" // length: 3 // __proto__: Array(0) // 下面都指只有返回三組 // 因為 'length' 是不能遍歷的 console.log(Object.keys(arr)); // ["0", "1", "2"] console.log(Object.values(arr)); // ["a", "b", "c"] console.log(Object.entries(arr)); // [["0", "a"], ["1", "b"], ["2", "c"]] console.log(Object.getOwnPropertyDescriptor(arr, 'length')); // enumerable: false ``` ### set & get 對象方法 ```javascript= // 測試 let obj = { test:'test', set name(v) { console.log(v, 'set'); this.test = v; // 設置 name 屬性時會將值傳到這個形參 return 'setReturn' // 感覺不到有什麼用~~ }, get name(){ console.log('get'); return 'getReturn' // 如果沒有這個, 返回 undefined, // 表示 obj.name = 'haha' 並沒有附值給 name 屬性 // 而是將值傳進 set 方法的形參而已 // 要將 name 屬性賦值, 必須依靠 get 的 return } } console.log(obj.name = 'haha'); // haha set // haha console.log(obj.test); // haha console.log(obj.name); // get // getReturn // 整理邏輯 let obj = { _name: null, set name(v){ // this.name = v //=> 死循環!!! 因為不斷的設置 name 屬性 this._name = v }, get name(){ return this._name } } obj.name = 'haha'; // 將值傳到形參 v, 再將值賦給 _name 屬性 console.log(obj.name); // 調用時返回 _name 屬性 ``` ## Symbol > - 基礎數據類型 > - 類似 String ### 創建 > - 使用 `Symbol()` 函數來創建 > - Symbol 可以丟一個參數來描述這個 Symbol 數據 ```javascript= let sym = Symbol(); let sym2 = Symbol('haha'); let sym3 = Symbol(1); console.log(sym, sym2, sym3); // Symbol() Symbol(haha) Symbol(1) console.log(typeof sym); // symbol ``` ### 特色 > #### 每個 Symbol 數據都是不同的 > - 即使表面上看起來一樣 ```javascript= let str = 'a'; let str2 = 'a'; console.log(str == str); // true let sym = Symbol('p'); let sym2 = Symbol('p'); console.log(sym == sym2); // false ``` > #### Symbol 無法進行運算 ```javascript= console.log(Symbol(1)+1); // Cannot convert a Symbol value to a number // 拼接也是運算, 所以不行 console.log(Symbol(1)+'1'); // Cannot convert a Symbol value to a string ``` > #### Symbol 無法轉成數字 ```javascript= Number(Symbol(1)); // Cannot convert a Symbol value to a number ``` > #### Symbol 可以轉成字符串 ```javascript= console.log(String(Symbol(1))); // 'Symbol(1)' console.log(Symbol(1).toString()); // 'Symbol(1)' ``` > #### Symbol 可以轉成 Boolean ```javascript= console.log(!Symbol(1)); // false // 描述符對 Symbol(false) 整體的值好像沒有差別 // 轉 Boolean 就是 true console.log(Boolean(Symbol(false))); // true ``` ### 用法 > - 因為每個數據都不同, 所以常用於對象的屬性名上, > 避免被別人修改值, 又或者新增屬性名時, 跟別人一樣而改到別人的值 > - Symbol() 當成屬性名時, 是==匿名==且==無法被枚舉==的 > - 訪問方法1. 創建時指向的變量 > - 訪問方法2. `Object.getOwnPropertySymbols()` ```javascript= let sym = Symbol(); let obj = { [sym]: 'test', 'a': 1, } // 無法被枚舉~~ for (let k in obj) { console.log(k); } // a console.log(Object.getOwnPropertyNames(obj)); ['a'] // 訪問的方法 // 1. 直接訪問創建時指向的變量 console.log(sym); // 2. Object.getOwnPropertySymbols() 方法 console.log(Object.getOwnPropertySymbols(obj)); ``` ### `Symbol.for()` > - 使用 `Symbol.for()` 創建時, > 會先查找有無一樣使用 `Symbol.for()` 創建且參數相同的 > 如果找到就返回該對象, 如果沒有就創建一個 ```javascript= let sym = Symbol.for('sym'); let sym2 = Symbol.for('sym'); let sym3 = Symbol('sym'); let sym4 = Symbol('sym'); console.log(sym === sym2); // true -> 兩個址是一樣的 !!! console.log(sym3 == sym4); // false -> Symbol() 跟誰都不一樣 console.log(sym == sym3); // false ``` ### `Symbol.keyFor()` > - 查找使用 `Symbol.for()` 創建時的參數, 沒有就 undefined ```javascript= let sym = Symbol.for('sym'); let sym2 = Symbol('sym2'); // 不是使用 for 創建 console.log(Symbol.keyFor(sym)); 'sym' console.log(Symbol.keyFor(sym2)); undefined // undefined ``` ## set > - 類似陣列 > - 只有value, 沒有 key > - 會去重複 ### 生成 `new Set([iterable])` > - 參數接受陣列或類陣列(只要有 iterable API 都行) > - 有 iterable API : 陣列, argumnets, 元素集合, Set, Map, String ```javascript= let s = new Set(); console.log(s); // Set(0) {} console.log(s.size); // 0 s = new Set([1,2,3,3]); console.log(s); // Set(3) {1, 2, 3} -> 去掉重複的 3 console.log(s.size); // 3 ``` ### Set 轉 Array > - 擴展運算符 ```javascript= let s = new Set([1,2,3,3]); console.log(s); // Set(3) {1, 2, 3} let arr = [...s]; // 擴展運算 console.log(arr); // (3) [1, 2, 3] ``` ### Set 方法 > #### `add(v)` : > - 添加值, 返回 Set 實例本身 > - 參數一次只能加一個, 兩個以上後面都無視 > #### `delete(v)` : > - 刪除值, 返回 boolean, 表示有無刪除成功 > #### `has(v)` : > - 查詢值的有無, 返回 boolean 表示有無 > #### `clear()` : > - 清除所有值, 沒有返回值 ```javascript= var s = new Set(); // add() var a = s.add('G').add('o').add('d').add('J').add('J'); console.log(s); // Set(4) {"G", "o", "d", "J"} console.log(a); // Set(4) {"G", "o", "d", "J"} // delete() var d = s.delete('G'); var d2 = s.delete('A'); console.log(d, d2); // true false console.log(s); // Set(3) {"o", "d", "J"} // has() var h = s.has('J'); var h2 = s.has('A'); console.log(h,h2); // true false // clear() var c = s.clear(); console.log(s); // Set(0) {} console.log(c); // undefined ``` > #### `forEach(cbFn(v,i,a){}, this)` > #### `keys()` > #### `values()` > #### `entries()` > - 用法一模一樣 > - 差別在於 value 跟 index 都是當前的 value > 因為 Set 只有 value 沒有 key ```javascript= var s = new Set(['G', 'O', 'D']); var obj = {}; s.forEach(function (v,i,s) { console.log(v,i,s, this); // G G Set(3) {"G", "O", "D"} {} // O O Set(3) {"G", "O", "D"} {} // D D Set(3) {"G", "O", "D"} {} // callcack 一樣三個參數, thisArg 一樣可以改 this 指向 }, obj) // 只有 value 沒有 k ~~~ for (let k of s.keys()) { console.log(k); // G // O // D } console.log('----'); for (let v of s.values()) { console.log(v); // G // O // D } console.log('----'); for (let [v,k] of s.entries()) { console.log(v,k); // G G // O O // D D } console.log('----'); ``` ### 應用: 去重 ```javascript= let arr = [1,2,2,6]; function rmRp(arr) { // return [...new Set(arr)] return Array.from(new Set(arr)) } console.log(rmRp(arr)); ``` > - 其他去重法 https://github.com/YvetteLau/Step-By-Step/issues/31 ### 應用: 聯集, 交集, 差集 > - 聯集 : 合併去重 > - 交集 : A 裡包含 B 者 > - 差集 : 聯集 - 交集 ```javascript= let arr = [1,2,3,6]; let arr2 = [1,4,6,9]; function add(arr, arr2) { return [...new Set([...arr,...arr2])] } console.log(add(arr, arr2)); //  [1, 2, 3, 6, 4, 9] function and(arr,arr2) { return arr.filter(v=>arr2.includes(v)) } console.log(and(arr,arr2)); // [1, 6] function not(arr,arr2) { return add(arr, arr2).filter(v=>!and(arr,arr2).includes(v)) } console.log(not(arr, arr2)); // [2, 3, 4, 9] ``` ```javascript= // 物件去重 let json1=[ {id:1,name:"aaa"}, {id:2,name:"bbb"}, {id:3,name:"ccc"}, ] let json2=[ {id:1,name:"aaa"}, {id:2,name:"bbb"}, {id:4,name:"ddd"}, ] let json = json1.concat(json2); let newJson = []; for(item1 of json){ let flag = true; // 標記是否重複, false 為重複 // 循環新陣列 for(item2 of newJson) { // 判斷是否重複 if(item1.id == item2.id) { flag = false; } } // 沒有重複就推進去 if (flag) { newJson.push(item1); } } console.log("newJson",newJson); ``` ## Map `new Map([[k1,v1],[k2,v2]])` > - Map 參數是一個陣列, 陣列的值是包著 key 跟 value 的陣列 > - Map 對象保存 key, value > - vs. Object: > - 對象的 key 必須為 String 或 Symbol, 非字串也都會轉成字串 > Map 任何類型都能成為 key > - 對象是無序的, Map 是有序的 > - Map 通過 size 屬性來記錄鍵值組個數, 對象則無 ```javascript= let arr = [1,2]; let o = {a:1}; let obj = { 'a': 1, b:2, undefined: 3, null: 4, [arr]: 5, [o]: 6, true: 7, 1:8, } console.log(obj); let m = new Map([[1,1], [null,2],[[1,2],3],[{},4],[undefined, 5]]) console.log(m); ``` <img src='https://i.imgur.com/L5oMrmp.png' style='height: 200px'/> <img src='https://i.imgur.com/ZPCxcZa.png' style='height: 200px'/> > - 屬性名如果重複, 後面的值會把前面蓋掉, 該對象依樣 ```javascript= let m = new Map([[1,1],[1,2]]); console.log(m); // Map(1) {1 => 2} // [[Entries]] // 0: {1 => 2} // key: 1 // value: 2 // size: 1 // __proto__: Map ``` ### 方法 > #### `get` `set` `has` `delete` `clear` > - `get(k)` > - 獲取 value > - `set(k,v)` > - 如果已經有 k, 修改值 > - 如果沒有 k, 新增 > - 會直接改變原對象, 且返回原對象的址 > - `has(k)` > - 判斷 key 有無對應 value > - 返回 Boolean > - `delete(k)` > - 刪除對應的 k,v > - 返回 Boolean 表示刪除是否成功 > - `clear()` > - 清空 > - 沒有返回值 ```javascript= let o = {}; let a = []; let m = new Map([[o,1], [a,2], [undefined, 3], [null, 4]]); console.log(m.get(o)); // 1 console.log(m.get(a)); // 2 console.log(m.get(undefined)); // 3 console.log(m.get(null)); // 4 console.log(m.set(o,5)); // Map(4) {{…} => 5, Array(0) => 2, undefined => 3, null => 4} console.log(m.set(1,6)); // Map(5) {{…} => 5, Array(0) => 2, undefined => 3, null => 4, 1 => 6} console.log(m); // Map(5) {{…} => 5, Array(0) => 2, undefined => 3, null => 4, 1 => 6} console.log(m.has(1)); // true console.log(m.has(true)); // false console.log(m.delete(1)); // true console.log(m.delete(1)); // false console.log(m); // Map(4) {{…} => 5, Array(0) => 2, undefined => 3, null => 4} console.log(m.clear()); // undefined console.log(m); // Map(0) {} ``` > #### `forEach` `keys` `values` `entries` > - 搭配 `for...of...` 使用 > - 每次循環都會返回一組 \[k,v] ```javascript= let o = {}; let a = []; let m = new Map([[o,1], [a,2], [undefined, 3], [null, 4]]); m.forEach((v,i,a)=> { console.log(v,i,a); // 1 {} Map(4) {{…} => 1, Array(0) => 2, undefined => 3, null => 4} // 2 [] Map(4) {...} // 3 undefined Map(4) {...} // 4 null Map(4) {...} }) console.log('------') for (let k of m.keys()){ console.log(k); // {} // [] // undefined // null } console.log('------') for (let v of m.values()){ console.log(v); // 1 // 2 // 3 // 4 } console.log('------') for (let [k,v] of m.entries()){ console.log(k,v); // {} 1 // [] 2 // undefined 3 // null 4 } ``` ### 面試題: 將一個 Array 變成 Map > - Array 的 key 是索引 > - 拿索引當Map 的 k 即可 ```javascript= let arr = ['a','b','c']; let map = new Map(); for (let [k,v] of arr.entries()) { map.set(k,v); } console.log(map); // 0: {0 => "a"} // 1: {1 => "b"} // 2: {2 => "c"} ``` ## Proxy `new Proxy(target, handler);` > - 對目標對象攔截操作, 以此改變原本預設的操作 > - 參數 > - `target` : 目標對象, (對象都行, 甚至 Proxy 也行) > - `handler` : 攔截後要做的事, 總共有十三種方法 ### handler > #### `get` `set` `has` > - `get(target, propKey, receiver)` : > - `target` : 目標對象 > - `propKey` : 被指定操作的屬性名 > - `receiver` : 當前 Proxy 實例, this > - 獲取對象屬性的值時, 觸發這個處理 > - `set(target, propKey, value, receiver)` : > - `value` : 賦予的值 > - 新增或修改屬性時, 觸發 > - `has(target, propKey)` : > - 查詢是否有指定 k 時, 觸發 ```javascript= let obj = {a:1, b:2}; let proxy = new Proxy(obj, { // 以下以操作原本該有的操作為主 get(t, p, r){ console.log('get', arguments); // get Arguments(3) [{…}, "a", Proxy, ...] // get Arguments(3) [{…}, "b", Proxy, ...] // get Arguments(3) [{…}, "c", Proxy, ...] return t[p] // get return 什麼就是什麼, 沒有 return 就undefined }, set(t,p,v,r){ console.log('set', arguments); // set Arguments(4) [{…}, "a", 3, Proxy, ...] t[p] = v; // 把拿到的值賦給指定的對象的屬性名 return 'set' // set 設return 好像沒什麼效果? }, has(t,p){ console.log('has', arguments); // has Arguments(2) [{…}, "a", ...] // has Arguments(2) [{…}, "__proto__", ...] // has Arguments(2) [{…}, "z", ...] if (p.startsWith('_')) { // 如果是私有變量名, 返回 false return false } return p in t // 如果有就返回 true } }) console.log(proxy.a); // 1 console.log(proxy.b); // 2 console.log(proxy.c); // undefined console.log(proxy.a = 3); // 3 console.log(obj.a); // 3 // obj 改了~ // 注意, 我直接對原對象操作時, 是不會觸發這些方法的 console.log('a' in proxy); // true console.log('__proto__' in proxy); // false console.log('z' in proxy); // false ``` > #### `apply` > - `apply(target, object, args)` > - `object` : 指定 this 對象 > - `args` : 傳進來的實參 > - 執行的時候(函數直接執行, call, apply, ...) 觸發 ```javascript= function fn() { console.log(this); } let proxy = new Proxy(fn, { apply(target, object, args){ console.log('apply', arguments); // 判斷有沒有指定改變 this 指向 if (object) { // target.call(object, ...args); // 可以用現成的 // 也可以自己寫, // 最好加到原型上, 否則調用後才刪除這個對象, // console.log(this) 的時候會看到 object.__proto__.fn = target; object.fn(); delete object.__proto__.fn } else { target(...args); } } }) let obj = {}; proxy(1,2,3); // apply Arguments(3) [ƒ, undefined, Array(3), ...] // Window proxy.call(obj, 4,5); // apply Arguments(3) [ƒ, {…}, Array(2), ...] // {} proxy.apply(obj, [6,7]); // apply Arguments(3) [ƒ, {…}, Array(2), ...] // {} ``` ## class > - 以前構造函數 > - 這套是大神們所研究出來的 SOP, 而非官方的, 所以使用起來才那麼麻煩 ```javascript= // 父構造函數 function Father(x,y) { this.x = x; this.y = y; } // 添加共用方法要寫到原型上 Father.prototype.show = function () { console.log(this.x, this.y); } // 子構造函數繼承 function Son(x,y) { Father.call(this,x,y); // 使用 call 拿到 Father 實例的東西 } Son.prototype = new Father(); // 使用 Father 的原型方法 Son.prototype.constructor = Son; // 別迷失了自己 let s = new Son(5,6); s.show(); // 5 6 ``` ```javascript= // - 函數: return 簡單數據類型 function fn() { this.a = 10; return 1 // return 簡單數據類型, 不會影響實例 } // - fn 的原型 fn.prototype.test = function () { console.log(this.a); } // - 函數: return 複雜數據類型 function fn2() { this.a = 10; return {b:10,} // return 引用數據類型, 會影響實例對象 } // - 構造函數 let fnn = new fn(); console.log(fnn); // fn2 {a: 10} // - 調用函數 console.log(fn2()); // 1 let fnn2 = new fn2(); console.log(fnn2); // {b: 10} let fnn = new fn(); fnn.test(); // 10 console.dir(fnn.constructor); // ƒ fn() ``` - class 基本上替代了這麻煩的問題 ```javascript= class Cls { constructor() { this.a = 10; // return {b:10} } // 添加方法就直接寫 test () { console.log(this.a); } } let cls = new Cls console.log(cls.a); // 10 cls.test(); // 10 console.log(typeof Cls); // functin -> Class 依然是 function Cls(); // Class constructor Cls cannot be invoked without 'new' // 只是不能直接調用 ``` ### class 的 name 問題 > - 當類被指向時, 從外面只能間接使用類 ```javascript= // - 沒有被指向的類 class Cls { constructor() { console.log(Cls.name); } } let c = new Cls(); // Cls 如果沒有對類指向時, 可以直接使用類名 // - 被一個變量指向的類 let cls2 = class Cls2{ constructor() { console.log(cls2.name); console.log(Cls2.name); } } // let c3 = new Cls2(); // Cls2 is not defined // 但如果對類指向時, 外面就無法直接找到類名了 // 必須透過變量找到該類 let c2 = new cls2(); // Cls // Cls // 但是類裡面還是可以直接使用類名 // 且不論直接使用類還是使用變量來調用類名, 都是同一個類名, // 我也不知道我在寫什麼, // 反正就是指向同一個對象, 在類裡面兩個變量都能用, 類名都是原類名 console.log(cls2.name); // Cls2 //=> 在外面透過變量找名字, 一樣是類名而非變量名 // 構造函數也是 let fn = function Fn() {} new Fn(); // Fn is not defined ``` ### class 執行 > - 類雖然不能直接執行, > 但是類可以直接實例後執行 ```javascript= // 實例 new class{} 後會返回一個實例對象, // 接著 () 執行對象, 相當於執行 constructor() 函數 // 寫在一起就變這樣: new class{ constructor(){ console.log(...arguments); } }('我','是','a','r','g'); // 我 是 a r g ``` ### class 不會 hoisting ```json= new Cls(); // Cannot access 'Cls' before initialization class Cls{} ``` ### class 靜態 (static) 方法 > - 類就相當於的原型 > - 所有寫在類裡的東西都會在實例的 \_\_proto__ 而被實例繼承 > - 如果想要寫在 Class 本身, 讓能讓 Class 本身調用, > 那就要在方法前面加 `static` > - 簡易圖: > <img src='https://i.imgur.com/WStNf4a.jpg' style='width: 300px'/> ```javascript= class Cls{ constructor(...args) { console.log(args, this); } fn() { console.log('dynamic', this); } static fn2() { console.log('static', this); } } let c = new Cls(1,2); // [1, 2] Cls {} console.dir(c); // __proto__ // constructor: class Cls // name: "Cls" // fn2: ƒ fn2() // fn: ƒ fn() // __proto__: Object c.fn(); // dynamic Cls {...} // __proto__: Object -> this 指向類實例 // c.fn2(); // c.fn2 is not a function -> 找不到!! Cls.fn2(); // static class Cls{類本身} // this 指向類本身 // 靜態方法類實例不能用! // 類似於 Array.of() 等方法 // 繼承的話當然也可以訪問的到, 而 Cls2 的實例類當然也訪問不到 class Cls2 extends Cls{} Cls2.fn2(); ``` ### class 原型上的方法不可枚舉 ```javascript= // 構造函數的原型上所添加的東西是可以枚舉的 function Fn(){ this.a = 100; } Fn.prototype.b = function () {}; Fn.prototype.c = 200; let f = new Fn(); for (let k in f) { console.log(k); // a b c -> 是可枚舉 } console.dir(f); // a: 100 // __proto__ // b: ƒ () // c: 200 // constructor: ƒ Fn() // __proto__: Object // 類的原型上添加的方法是不能玫舉的 class Cls{ constructor() { this.a = 100; } dFn(){} } let c = new Cls(); for (let k in c) { console.log(k); // a -> 只有 a } console.dir(c); // a: 100 // __proto__: // constructor: class Cls // dFn: ƒ dFn() // __proto__: Object console.log(Object.getOwnPropertyDescriptor(Cls.prototype, 'dFn')) // enumerable: false console.log(Object.getOwnPropertyDescriptor(c, 'a')) // enumerable: true ``` ### 繼承 > - 使用 `extends` 表示繼承 > - 子類必須調用一次 `super()` 來拿到 this > - `super()` 將相當於用 `call()` 調用父類的 constructor > 意即其實 super 只能將屬性拿到, 方法是靠 extends 拿到的 ```javascript= class A { constructor(x) { this.x = '老爸的x' + x; console.log(this); } AFn() { console.log(this); console.log(this.x); } static ASFn() { console.log(this); } } class B extends A { constructor(x) { // console.log(this); // 還沒 super調用之前, 不能使用 this, 否則會報錯 super(x); // 執行super 相當於執行了父類的 constructor, 但是 this 指向子類 // 就類似於 A.constructor.call(B) // 執行完後該實例就有 A類實例後的屬性方法了 this.x = '兒子的x' + x; //如果要修改就後來再寫就好了 } AFn() { // 修改方法 this.x = 'test'; super.AFn(); // 如果重寫方法後想要再調用父類方法, 一樣使用 super } static ASFn() { console.log(12345); super.ASFn(); } } let a = new A(3); // A {x: "老爸的x3"} // x: "老爸的x3" // __proto__: // constructor: class A // ASFn: ƒ ASFn() // name: "A" // AFn: ƒ AFn() // __proto__: console.dir(a); // A a.AFn(); // A {x: "老爸的x3"} // 老爸的x3 console.log('-----------------'); let b = new B(3); // B {x: "老爸的x3"} // x: "兒子的x3" // __proto__: A console.dir(b); // B b.AFn(); // B {x: "兒子的x3"} // 兒子的x3 B.ASFn(); // 12345 // class B extends A {...} ``` > #### 問題: class 能繼承以前的寫法嗎? > - 可以, super 找得到函數都行 ```javascript= function Father(x,y) { this.x = x; this.y = y; } Father.prototype.show = function () { console.log(this.x, this.y); } class Son extends Father{ constructor(x,y) { super(x,y); } } let s = new Son(5,6); s.show(); // 5 6 -> 可以~ ``` ## Promise `new Promise( function(resolve, reject) {...} /* executor */ );` > - Promise 的參數為一個 executor 函數, 只要實例馬上執行 > - executor 函數有兩個形參( resolve 跟 reject ), > 這兩個形參也都是函數, 且為 callback > - 當實例 Promise 時, 該實例的狀態會設為 pending, > 而 Promise 在執行完 executor 函數後會變成兩種狀態 ( fulfilled 或 rejected ) 其一 > - 在 executor 函數調用 resolve 函數參數時, 會將 Promise 狀態轉成 fulfilled > - 在 executor 函數調用 reject 函數參數時, 會將 Promise 狀態轉成 rejected > - 如果在執行 executor 函數時, 產生錯誤, Promise 狀態會直接變成 rejected > (這是MDN寫的, 可是我只要在 executor 函數掛掉, 整個就掛掉了...) ### Promise.prototype.then `p.then(onFulfilled[, onRejected]);` > - 這兩個參數都是 callback, 等 Promise 狀態改變, 才會調用對應的 callback > - 當 Promise 的狀態為 Fulfilled 時, 調用第一個參數, > - 當 Promise 的狀態為 Rejected 時, 調用第二個參數, > 且如果沒有設定實參時, 預設也會有固定的錯誤訊息傳進來 > - 當 Promise 的狀態沒有改變 ( pending ) 時, then 兩個參數都不會調用 > - 但有些太明顯的錯誤會直接掛掉, 例如 const不賦值, double let,... > - Promise 執行順序: > executor函數 -> 同步對列 -> then中的回調 ```javascript= let p = new Promise((resolve, reject)=>{ console.log(0); d; resolve(); reject(e); }); /* 測試整理: * 1. 當執行 executor 時產生錯誤, * 會直接將 Promise 狀態改為 rejected 而執行 onRejected * 2. 如果都沒有產生錯誤, Project 就會看你調用哪個參數來決定 Project 狀態 * 3. 如果都沒有改變狀態, 就啥都不幹, 執行完同步任務就沒了 * 4. 但如果錯誤太明顯, 例如 double let , 程序會直接掛掉, 不會管你 * 0 4 2 ReferenceError: d is not defined at test.js:4 at new Promise (<anonymous>) at test.js:2 我還能執行 */ p.then(()=>{ console.log(1); console.log(); },(e)=>{ console.log(2); console.log(e); console.log('我還能執行'); }) console.log(4); ``` ### executor 裡放異步操作 > - 在 div 裡添加圖片 ```htmlmixed= <body> <div></div> <script src='test.js'></script> </body> ``` ```javascript= let box = document.querySelector('div'); function loadImg(url) { return new Promise((resolve, reject)=> { console.log(0); // 1. 創建一個圖片對象 let img = new Image(); // 2. 把url參數傳進去 img.src = url; // 3. 註冊事件, 成功調用 resolve, 失敗調用 reject img.onload = ()=>{ resolve(img); } img.onerror = (e)=>{ reject(e); } }) } console.log(1); loadImg('https://www.nationalgeographic.com/content/dam/animals/thumbs/rights-exempt/mammals/r/raccoon_thumb.ngsversion.1485815402351.adapt.1900.1.JPG').then((img)=>{ box.appendChild(img) console.log(img); },(e)=>{ console.log(e); }) console.log(2); loadImg('hngsversion.1485815402351.adapt.1900.1.JPG').then((img)=>{ box.appendChild(img) console.log(img); },(e)=>{ console.log(e); }) console.log(3); /* 最後的結果 * 1 0 2 0 3 Get net::ERR_FILE_NOT_FOUND Event -> 這坨是調用 reject 時會傳進來的實參 isTrusted: true type: "error" target: null currentTarget: null eventPhase: 0 bubbles: false cancelable: false defaultPrevented: false composed: false timeStamp: 74.85000003362074 srcElement: null returnValue: true cancelBubble: false path: [img] __proto__: Event <img src='...'/> * * * 先執行了同步隊列的 1, 接著進去 executor log了一個 0 後註冊了兩個異步隊列 * 接著執行同步對列2, 進去executor log了一個 0 後再註冊了兩個異步對列 * 最後執行同步對列3 * * 後面分別觸發 executor 裡的註冊事件, 一次成功一次失敗 * / ``` ### Promise.prototype.catch `p.catch(onRejected);` > - 捕獲錯誤用的, 作用跟 `then()` 第二個參數 ( onRejected ) 一樣 > - 差別在於即使 executor 函數執行後狀態為 fulfilled > 而執行 `then` 參數函數而錯誤時, catch 會攔截而執行 > 但 `then` 不會報錯而又執行自己 > - 故常見寫法是 `then` 直接不寫第二參, 在後面接著設 catch 來攔截 > `p.then(()=>).catch(()=>{})` ```javascript= let p = new Promise(resolves=>{ console.log(1); d resolves(); }) p.then(()=>{ console.log(2); d; }, (e)=>{ console.log(5); console.log(e); d; }).catch(e=>{ console.log(3); console.log(e); }) console.log(4); /* 1. 執行 executor 函數時, 而改變狀態, 依照狀態執行對應的 then 參數函數 * 但如果在執行 then 參數而報錯時, 程式就會掛掉 * 2. 所以可以再多設置一個 catch 屬性 * 當執行 then 而報錯時, catch 會攔截錯誤 * 或者then 沒有設置第二參數時, onRejected 也會變成 catch 設定的參數 1 4 5 ReferenceError: d is not defined at test.js:4 at new Promise (<anonymous>) at test.js:2 3 ReferenceError: d is not defined at test.js:14 */ ``` ### Promise.all `Promise.all([Promise1, Promise2, ...])` > - Promise.all 也會產生一個帶有 Promise 狀態的對象, > - 參數為 Promise 實例組成的陣列 > - 當所有參數的實例狀態==都==為 onFulfilled 時, > `Promise.all()` 產生的對象狀態就會變成 onFulfilled, 而執行對應的 callback, > 傳進來的實參為所有陣列監控的執行成功結果, 用陣列裝起來 > - 當有一個參數實例狀態為 onRejeted 時, > Promise.all 對象狀態馬上會變為 onRejected 而執行 rejeted callback > 不會再往下監控, 意即馬上返回錯誤訊息給 then, 後面也錯也不會被執行到 > - 當沒有參數實例為 onRejeted 但有參數實例的狀態還在 pending 時, > Promise.all 對象狀態依舊會保持 pending ```javascript= let p1 = new Promise((resolve, reject)=> { console.log('p1'); // reject('p1 OnRejected'); resolve('p1 OnFulfilled'); }) let p2 = new Promise((resolve, reject)=> { console.log('p2'); // reject('p2 OnRejected'); resolve('p2 OnFulfilled'); }) let p3 = new Promise((resolve, reject)=> { console.log('p3'); // resolve('p3'); }) console.log(p1,p2,p3); let pAll = Promise.all([p1,p2,p3]); console.log(pAll); pAll.then(res=>{ // onFulfilled console.log(res); // 當成功時, 傳進來的是三個成功的結果, 用陣列裝 }).catch(e=>{ // onRejected console.log(e); }) /* 全部都沒錯誤訊息 Promise {<resolved>: "p1 OnFulfilled"} Promise {<resolved>: "p2 OnFulfilled"} Promise {<resolved>: "p3"} => pAll.then.catch // ["p1 OnFulfilled", "p2 OnFulfilled", "p3"] */ /* p1 跟 p2 狀態都為 OnRejected Promise {<rejected>: "p1 OnRejected"} Promise {<rejected>: "p2 OnRejected"} Promise {<resolved>: "p3"} => pAll.then.catch // p1 OnRejected 第一個就返回了~~~ */ /* 三個沒有狀態為 rejected, 但還有 pending 時 Promise {<resolved>: "p1 OnFulfilled"} Promise {<resolved>: "p2 OnFulfilled"} Promise {<pending>} => 不會執行任何其他操作, 等 pending 開獎~~~ */ ``` ### Promise.race `Promise.race([p1,p2,p3])` > - 用法跟 all 一樣, > - 差別在於 race 是只要有 Promise 實例改變, 直接 return ```javascript= let p1 = new Promise((resolve, reject)=> { console.log('p1'); // reject('p1 OnRejected'); resolve('p1 OnFulfilled'); }) let p2 = new Promise((resolve, reject)=> { console.log('p2'); reject('p2 OnRejected'); resolve('p2 OnFulfilled'); }) let p3 = new Promise((resolve, reject)=> { console.log('p3'); resolve('p3'); }) console.log(p1,p2,p3); let pRace = Promise.race([p1,p2,p3]); pRace.then(res=>{ console.log(res); }).catch(e=>{ console.log(e); }) console.log(pRace); // p1 OnFulfilled ``` ## Generator > - 讓函數暫停執行 > - 形式: > - function 跟 函數名間有 * > - 函數裡有 yeild > - 執行 Generator 函數後不會執行 code, 而是返回一個 Generator 實例 > - Generator 實例裡的 `next()` 是用來執行 code 的 > - 每次 `next()` 都會執行到 yeild 結束, 立馬返回, 下次從 yeild 下一個動作開始 > - `next()` 有返回值, 紀錄 yeild 返回的東西與 function 是否執行完了 > - 優點, 可以決定什麼時候執行函數的哪些部分 > 例如在某個異步操作調用 next(), 當觸發異步時, 執行 next() 等 ```javascript= // function* fn(){ //=> 特色1. function 跟 函數名 之間有個 *, // 在哪無所謂, 空格是給人看的 function *fn(){ console.log(1); yield; //=> 特色2. 函數體有個 yield console.log(2); yield 'a'; } let gen = fn(); console.log(gen); /* * 特色3. Generator 函數執行後不會執行裡面的 code * 而是返回一個 Generator 實例 * fn {<suspended>} __proto__: Generator [[GeneratorLocation]]: test.js:4 [[GeneratorStatus]]: "closed" [[GeneratorFunction]]: ƒ *fn() [[GeneratorReceiver]]: Window */ console.log('one'); // one console.log(gen.next()); /* * 特色4. Genertor 函數執行是調用實例裡的 next() * 特色5. * - 最重要的特色 * - Genertor 函數不會全部執行完, 而是執行到 yield; 表達式之前(包含表達式) * - 然後返回一個對象, 包含 * - value => yeild 後面的值, 沒有就返回 undefined * - done => 函數執行完沒, 還沒就 false * 1 {value: undefined, done: false} */ console.log('two'); // two console.log(gen.next()); /* 2 {value: "a", done: false} */ console.log('three'); // three console.log(gen.next()); // {value: undefined, done: true} console.log(gen.next()); // {value: undefined, done: true} ``` ### yield > - 可以傳參數, 也可以有返回值 > #### 傳參 > <img src='https://i.imgur.com/9NEqTTK.jpg' style='width: 300px;'/> > - 每次 next 執行, 只會執行到 yeild 結束就返回 > - 所以如果要傳參, 要塞在後面那個 next ```javascript= function *fn(){ console.log(1); let a = yield; console.log(2,a); } let gen = fn(); console.log(gen.next('haha')); console.log(gen.next('wowo')); console.log(gen.next()); console.log(gen.next()); /* 1 {value: undefined, done: false} 2 "wowo" //=> 從結果可以證明傳參要放在下次 {value: undefined, done: true} {value: undefined, done: true} {value: undefined, done: true} */ ``` > #### 返回值 > - 如特色5 所述, next() 會有一個返回值, 其中的 value 就是 yeild 後面的東西 > 所以其實可以把 yeild 看成 return (執行完就把後面東西返回) > - 另外一個返回值就是 return, next() 在最後一次如果有 return, 也會返回到 next.value 裡 ```javascript= function *fn(){ console.log(1); let a = yield 'yeild'; console.log(2,a); return 'return' } let gen = fn(); console.log(gen.next('haha')); // 1 // {value: "yeild", done: false} console.log(gen.next('wowo')); // 2 "wowo" // {value: "return", done: true} ``` ### 其他 > #### 箭頭函數無法寫生成器 > - 要在 function 跟 變量名中間加 *, 箭頭函數沒有 function... ## async > - async 是函數對象 > - 執行 async 函數會返回 Promise 對象 > - 意即在 async 函數內執行的成功失敗, 都會改變 Promise 的狀態 > - async 成功執行時, 函數裡返回的東西會傳到 onResolve callback 函數中 ```javascript= async function fn(){ // b; // 執行有誤時, 返回的狀態就是 rejected return 'hhaa' // 成功執行的話, 返回的狀態就是 resolves } let a = fn(); // 執行 async 函數, 返回 Promise 對象 console.log(a); /* 執行有誤 Promise {...} __proto__: Promise [[PromiseStatus]]: "rejected" [[PromiseValue]]: ReferenceError: b is not defined at fn */ /* 正常執行 Promise {<resolved>: "hhaa"} //=> 把 return 返回給 resolved __proto__: Promise [[PromiseStatus]]: "resolved" [[PromiseValue]]: "hhaa" */ a.then((resolve)=>{ console.log(resolve) }).catch((e)=>{ console.log(e); }) /* 執行有誤 ReferenceError: b is not defined at fn (test.js:4) at test.js:9 */ /* 正常執行 hhaa */ ``` ### await > - `await` 後面可以放 > - `async()` > - `function()` > - `promise` > - `await` 只能在 `async()` 裡面 > #### 先整理前面的想法 > - 調用 Promise 時, 會馬上執行他的參數函數(executor), > 接著返回一個 Promise 實例對象 > - 當調用該對象的 `then` 方法或 `catch` 等方法時, > 會依據 Promise 狀態執行不同參數函數 ```javascript= let p = new Promise((res, rej)=>{ console.log(1); // rej(456); res(123); console.log(2); }) console.log(p); // Promise {<rejected>: 456} // 或 Promise {<resolved>: 123} p.then(res=>{ // Promise 狀態為 resolved 時, 走 then 一參 console.log('p.thne', 3); console.log(res); }).catch(e=>{ // Promise 狀態為 rejected 時, then 有二參走二參, // 沒有二參走 catch => 因為表示二參還是出錯, 不管哪個參數出錯, catch 都會接 console.log('p.catch', 4); console.log(e); }) ``` > - await > - 會直接拿到原本要傳給 resolve 或 rejected 的參數 > - 並判斷 p 的狀態, > - 如果是 resolve 就繼續執行, > - 如果是 rejected 就直接執行 then 二參或 catch, 並將參數傳進去 > ```javascript= let p = new Promise((res, rej)=>{ console.log(1); // b; // rej(456); res(123); console.log(2); }) console.log(p); // async function fn(){ console.log(5); let tmp = await p; console.log(6, tmp); return 'a' } fn().then(res=>{ console.log('fn()', 7); console.log(res); }).catch(e=>{ console.log('fn()', 8); console.log(e); }); /* exeutor 正常執行 , Promise 狀態為 resolved 時 1 2 Promise {<resolved>: 123} 5 6 123 => 這裡沒有 undefined, 表示 p 先拿到參數後才執行 log(6, tmp) => 原本 p.then 是異步執行, 這裡不是, 否則 tmp 是 undefined 才對 fn() 7 a */ /* exeutor 正常執行 , Promise 狀態為 rejected 時 1 2 Promise {<rejected>: 456} 5 => 沒有執行 log(6, tmp), => 也就是說當 await p 發現 p 是 rejected 狀態時, 直接跳二參或 catch => 並直接將參數傳到二參 或 catch fn() 8 456 */ /* exeutor 執行到報錯時, Promise 狀態當然直接 rejected 時 1 Promise {<rejected>: ReferenceError: d is not defined} 5 fn() 8 ReferenceError: d is not defined at test.js:4 at new Promise (<anonymous>) at test.js:2 */ ``` > - await 後面不是只能放 promise, 隨便一個值也能放, > 只是經常放異步操作而已, 例如 promise, generator, 另一個 await 也行 ```javascript= async function fn(){ console.log(1); let a = await 123; // 隨便放個值也能執行 console.log(a); } fn(); // 1 // 123 ``` ### 小結 > - Promise, Generator, Async/Await 本質上還是異步(回調), 只是寫起來方便 > - 用同步的寫法寫異步 ## esmodule > - nodeJS 用的, 暫略 ## 其他 > ### `Promise`/ `Generator`/ `async/await` > - 我看到有個老師用 JQ 介紹 Promise, Generator, Async/Await, > 現在我還不會 JQ , 先把他抄起來 > #### 同步與異步 > - 同步: 書寫簡單 > - 異步: 性能高 ```javascript= // 異步 $.ajax({ url: '/banner_data', succes(banner) { $.ajax({ url: '/user_data', succes(user) { $.ajax({ url: '/item_data', succes(item) { // .... }, error() { console.log('數據獲取失敗') } }); }, error() { console.log('數據獲取失敗') } }); }, error() { console.log('數據獲取失敗') } }); // 同步 let data1 = $.ajax({url: '/banner_data',}); let data2 = $.ajax({url: '/user_data',}); let data3 = $.ajax({url: '/item_data',}); ``` > #### Promise > - `$ vi 1.txt 2.txt 3.txt` 假設有三個文件可以讀取 ```javascript= let p = new Promise(res, rej){ $.ajax({ url: '1.txt', succes(json) { // 加載成功, 調用 res() res(json) }, error(err) { // 加載失敗, 調用 rej() rej(err) } }) } p.then(json=>{ console.log('succes'); }, err=>{ console.log('error'); }); ``` > ##### Promise.all() ```javascript= let p = new Promise(res, rej){ $.ajax({ url: '1.txt', succes(json) { res(json) }, error(err) { rej(err) } }) } let p2 = new Promise(res, rej){ $.ajax({ url: '2.txt', succes(json) { res(json) }, error(err) { rej(err) } }) } let p3 = new Promise(res, rej){ $.ajax({ url: '3.txt', succes(json) { res(json) }, error(err) { rej(err) } }) } Promise.all([p,p2,p3]).then(arr=>{ let [j1, j2, j3] = arr; // 把三個文檔資料拿到 console.log('succes'); }, err=>{ console.log('error'); }); ``` > #### JQ.ajax.then() ```javascript= // ajax 有返回值, 返回值為一個跟 promise 兼容的對象 let j = $.ajax({ url: '3.txt', dataType: 'json', succes(json) { console.log('succes') }, error() { console.log('error') } }) console.log(j); ``` ```javascript= // 其中一個方法就是 then $.ajax({ url: '3.txt', dataType: 'json', }).then(json=>{}, err=>{}) ``` > #### 合體! ```javascript= Promise.all([ $.ajax({url: '1.txt', dataType: 'json',}), $.ajax({url: '2.txt', dataType: 'json',}), $.ajax({url: '3.txt', dataType: 'json',}), ]).then(arr=>{ let [j1, j2, j3] = arr console.log('succes') }, err=>{ console.log(err) }) ``` > #### promise 問題 > - 異步操作帶有邏輯時, Promise 不好用 ```javascript= // 讀取到的資料是 vip 與 不是 vip 的操作結果不同 $.ajax('1.txt', function (user) { if (user.vip) { let data = $.ajax({url: '2.txt', dataType: 'json',}); console.log(data); } else { let data = $.ajax({url: '3.txt', dataType: 'json',}); console.log(data); } }) ``` > ### async/await ```javascript= async function fn () { try { let date1 = await $.ajax({url: '1.txt', dataType: 'json'}); let date2 = await $.ajax({url: '22.txt', dataType: 'json'}); // 故意出錯 let date3 = await $.ajax({url: '3.txt', dataType: 'json'}); console.log('succes'); } catch(e) { console.log('error'); throw new Error(e); // 拋出錯誤訊息 } } fn() ``` ```javascript= // 帶邏輯的異步 (async ()=>{ let data1 = await $.ajax('user_info'); // 根據資料 // 判斷 if (user.vip) { let data = await $.ajax({url: '2.txt', dataType: 'json',}); console.log(data); } esle { let data = await $.ajax({url: '3.txt', dataType: 'json',}); console.log(data); } }() ``` ### 追求性能的迷思 > #### Q. 可以為了性能而盡量不用語法糖嗎? > - 對前端來說, 0.001s 跟 0.000001s 的差別並不大 > - 對前端來說: 用戶體驗 >= 兼容性 > 可維護性(可讀性) > 性能(在可接受範圍下) > - 性能又可分為 網絡性能(加載) 跟 執行性能 > - 如果為了很小的執行性能來讓檔案變大, 導致加載變慢, 可能不值得 > - 編譯不會佔多少時間 > - async/await 是系統級的東西, 性能部會差 > - 都選擇了 JS 還關心性能? ```javascript= // 寫法一 $.ajax({ url: '1.txt', succes(banner) { // ... 套三層 略 }, error() { console.log('error') } }); // 寫法二 (async function () { try { let date1 = await $.ajax({url: '1.txt', dataType: 'json'}); let date2 = await $.ajax({url: '22.txt', dataType: 'json'}); // 故意出錯 let date3 = await $.ajax({url: '3.txt', dataType: 'json'}); console.log('succes'); } catch(e) { console.log('error'); throw new Error(e); // 拋出錯誤訊息 } })() // 為了讓程序更快選擇寫 寫法一 而讓程序變大, 對前端來說可能不值得 ``` ## [babel](https://babeljs.io/) > - 主要是用來編譯的 > - 安裝 babel 可以用 npm 來裝 > - npm (Node Package Manager), 就是字面上的意思 > - 安裝 [nodeJS](https://nodejs.org/en/) 時,就會順帶裝上了 > ### npm 安裝 babel > - 首先先開一個文件夾來管理 > - 進到文件夾後 `$ npm init` > - 這個動作會創建一個 `package.json` 檔案 > - 他會問很多問題後建立一個 json 檔來管理 > - 接著安裝 babel `$ npm install --save-dev @babel/core @babel/cli` > - `--save-dev` 的意思就是把安裝什麼東西寫進 package.json 裡 > - `-D` 為 `--save-dev` 的簡寫 ```jsonld= // 安裝前 { "name": "babel", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" } // 安裝後 { "name": "babel", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "devDependencies": { "@babel/cli": "^7.7.7", // 多了這兩個剛剛安裝的東西 "@babel/core": "^7.7.7" } } ``` > - 如此, 如果要在其他電腦工作, 或者 babel或其他工作用環境檔案不見, > 打 `$ npm i` 後, npm 就會去這個 json 查看 `'devDependencies'` > 來將原本使用的版本載下來 > - `i` 為 `install` 的簡寫 ```shell $ rm -rf node_modules package-lock.json #=> 我把剛載下來東西給清了 $ tree #=> 只留 package.json . └── package.json $ npm i #=> 安裝, 其他都沒打 $ ls #=> 剛剛載的又載回來了 node_modules package-lock.json package.json ``` ### 簡化運行腳本 > - 在 `package.json` 裡的 `"scripts"` 可以添加鍵值來簡化運行 > - 原本運行可能需要打一些參數 > `$ node test.js --port=80 -s` > - 當我寫進 `package.json` 的 `scripts` 後 > `$ npm run start` 效果一樣, 省下打一堆參數的麻煩 ```jsonld { "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node test.js --port=80 -s", // 寫 "build": "babel src -d lib" // 官網寫的, 主要想寫 -d 的意義 // -d: 輸出的意思, 亦即 // babel 編譯文件夾 輸出到 輸出文件夾 }, } ``` ### 最後一件事 > - 必須創建一個 `.babelrc` 檔, 裡面寫上 `{"presets": ["@babel/preset-env"]}` ```shell $ ls node_modules package-lock.json package.json $ vi .babelrc ``` ```jsonld { "presets": ["@babel/preset-env"] } ``` > - 並安裝 `@babel/preset-env` > `$ npm i @babel/preset-env -D` > - preset 是預設的意思, 簡單說就是依照 `@babel/preset-env` 的配置執行 ```shell $ cat package.json { // ..., "devDependencies": { "@babel/cli": "^7.7.7", "@babel/core": "^7.7.7", "@babel/preset-env": "^7.7.7" } } ``` > #### 開始編譯 ```shell= $ # 查看所有配置 $ ls -a . .. .babelrc js node_modules package-lock.json package.json $ cat cat js/test.js #=> 寫了一個 ES6 的 JS 檔在 JS 資料夾中 let [a,b] = [1,2]; const tmp = ()=>{ console.log(a+b); }; tmp(); $ cat package.json { "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "babel js -d tmp" }, "devDependencies": { "@babel/cli": "^7.7.7", "@babel/core": "^7.7.7", "@babel/preset-env": "^7.7.7" } } $ # 開始編譯 $ npm run build Successfully compiled 1 file with Babel. $ ls js node_modules package-lock.json package.json tmp $ cat tmp/test.js # 編譯輸出檔~~ "use strict"; var a = 1, b = 2; var tmp = function tmp() { console.log(a + b); }; ```