# [JS102] 升級你的 JavaScript 技能:ES6 + npm + Jest 課堂筆記 > 本文為「Lidemy」上的【JS102】的課程學習筆記,內容幾乎皆參照課程的內容、並同時參考其他同學的筆記,做為自我整理使用,如有錯誤歡迎糾正,非常感謝。 ## 參考資料 1. [JavaScript 102 ─ 重點整理](https://hackmd.io/@Yu040419/H1-QdS6o4) by Yu040419 2. [JS102 筆記](https://hackmd.io/DTvUQGMcQMeGPPJx99oNyA) by Cian 3. [JS102:升級你的 JavaScript 技能:ES6 + npm + Jest 筆記](https://medium.com/@eilin0603/js102-升級你的-javascript-技能-es6-npm-jest-筆記-68fe66b1df96) by RZ ## 1. 模組化 Module ### What? 將大功能切分成小模組,避免互相影響及方便 Debug。模組有可能為 node.js 內建,或者是自己寫的。 ![img](https://i.imgur.com/mWJJ3wc.jpg) ### How? 引用模組語法:`require()` ``` let abc = require('os'); //引用 'os' 模組放入變數 abc 裡 ``` os = Operating System(作業系統) ### 查詢作業系統 語法:`.platform()` ``` let abc = require('os'); console.log(abc.platform()); //window 就會出現 win32 ``` ### 如何出借模組 首先須先新增一 Js 檔案,將此檔案作為模組的輸出端,輸出你想要輸出的涵式(此為輸出 double 這個涵式) #### 1. 單純輸出 輸出端語法:`module.exports` / `exports.物件鑰匙` ``` function double(n) { return n * 2 } module.exports = double;//要輸出的東西 ``` 輸入端語法:`require(./檔案名稱)` ``` let double = require('./test.js'); //.js可省略,會自己找同黨名的 module console.log(double(3)); //輸出 6 ``` mudule.exports 後面等於不僅可以放函式,也可以放陣列、字串、數值、物件等。放函式就是輸出函式、放陣列就是輸出陣列。 #### 2. 將眾多功能結合在物件中 當不只要輸出一個東西 ##### 方法一:用 object 把東西包起來,比較直覺 輸出端 (test.js) ``` var obj = { double: function(n){ return n*2 }, triple: function(n){ return n*3 } } module.exports = obj//可以接任何東西 ``` 輸入端 (code.js) ``` var obj = require('./test.js') //自己的位置加./(?) console.log(obj.double(2),obj.triple(5)) // 4 15 ``` ##### 方法二:把 export 當成空物件,輸出就一定會是 object。 ``` exports.double = function(n){ return n*2 } export.triple = function(n){ return n*3 } ``` ## 2. NPM(Node Package Manager) ### What? **node.js 預設管理 JS 套件的系統**,透過此管理各種引用別人寫好的套件 ( Package ),裝 node.js 的同時也會一並自動安裝。 ### How? [NPM官網](https://www.npmjs.com/),從官網內找到欲使用的功能,將下載指令輸入 cmder,欲使用此功能的檔案務必與下載下來的套件(都放在 node_module 資料夾內)在同一層。這樣才可以順利使用。 就算在子資料夾裝套件,她也會自己去找 parent (也就是一開始 init 有 package.json 的地方) 如果那裏有。如果還是很堅持在裝在子資料夾,若很堅持就在你要的資料夾,再下一次 init ,讓他產生出 package.json #### 下載 left-pad 套件 `npm install <left-pad> --save` 安裝套件,安裝完成的套件會統一放在 node_modules 資料夾內。 ex: `npm install mathjs` 裝完後會跑出 `package-lock.json`和 `.viminfo` `package-lock.json`:紀錄各個套件的依賴關係 (dependency) 資料夾 `node_modules`:一併放入下載的套件所需的其他依賴套件。不需要 push 上 Github ,而是每次 clone 下來根據 `package-lock.json` 先 install 所需的套件 ##### `--save`的用意 為了要將此套件寫入 package.json 內。 > 補充資訊: > > 從 npm 5 以後,--save 就已經變成預設的選項了,因此 npm install 不加 --save 也是可以的喔,一樣會把資訊寫到 package.json 裡面去! ### package.json #### What? **記錄這個專案用了什麼套件的檔案。**裡面會詳細**記錄子專案使用的套件**、專案建立者、名稱等等。 #### Why? 因為一個專案可能會 import 很多的 package ,每次放上去的資料量可能會很龐大。所以比較有效率的做法是把所有有用到的 package 存在 package.json ,不用真的附上去,要使用的人載下檔案後把 package.json 的東西也一起引入就好了。 #### How? ##### `npm init` 輸入後看需求輸入使用者名稱、版本…等等。接著使用 `ls` 確認資料夾內檔案,即會發現一個檔案名稱為 package.json。 如果將下載的套件都寫入 package.json 的話,就可以將 node_module 資料夾刪掉,再 commit 到 GitHub 上。或是將 node_module 放入 ignore 資料夾內。 ##### .json 檔內 scripts 使用說明 ![img](https://i.imgur.com/O87Yt2n.jpg) scripts 內可輸入任意快速指令。如圖第一個 key 為 start ,要求為執行 node index.js,所以接下來在 cmder 內輸入`npm run start`,即可執行 node index.js。 (ps: 如果語法有用到 ES6 的 impot/export 需要 Babel 轉譯的話,就把 start 裡的東西改成 `npx babel-node index.js`) ##### 常見scripts應用 下載專案下來之後,有些時候需要清除檔案或是新增任何東西時,都可以先進來 .json 檔案內,在 scripts 內新增一個 key,並在元素內新增多個指令,即可在 cmder 內一鍵完成所有指令。 ##### 補充說明 .json 檔案內都是物件形式,但內部的 key 較特別的地方在於說需要使用**雙引號**括起來。 ## 3. Yarn ### What? 與 npm相似的另一個套件管理工具,由 Facebook 開發而成。 [Yarn 官網](https://yarnpkg.com/en/) ### How? #### install #### syntax-相關指令對照 | yarn | npm | function | | :------------------------ | ---------------------------- | :--------------------------------------------- | | `yarn init` | `npm init` | 啟用,會在專案資料夾內新增一個 package.json 檔 | | `yarn` | `npm install` | 根據 json 下載 dependencies | | `yarn -v` | `npm -v` | 查看版本號 | | `yarn add <package-name>` | `npm install <package-name>` | 下載套件 | | `yarn run <scriptName>` | `npm run <scriptName>` | 等同 | | `yarn run start` | `npm run start` | 快速打開 .json 裡寫的 start 所需打開的執行檔 | ## 4. Jest 測試語法 ### What? [Jest](https://jestjs.io/docs/en/getting-started) 是一個協助測試程式資料的現成套件 (frameork)。 ### Why? 初學者通常怎樣測試 code? - 用 console.log() - 想一些 edge case(邊界條件)讓程式跑跑看 - 試著讓測試結果一目了然 缺點 - 很難規模化 - 不好執行 ### How? #### Install 照著[官網指示](https://jestjs.io/docs/en/getting-started)用 npm 或者 yarn 下載 #### 怎麼寫測試? 0. 準備好兩個檔案,**被測試的檔案(index.js)**和**使用 Jest 作為測試的檔案 (index.test.js)** > PS: 因為 **被測試的檔案** 和 **測試的檔案** 要分開。而**作為測試的檔案**通常會命名為「test.js」,這樣 Jest 套件會優先搜尋這些檔案做測試。 1. **被測試的檔案(index.js)** 裡用 `module.exports = <functionName>` 把 function exports 出來 ![image-20200703162809913](https://i1.wp.com/lailalaii.files.wordpress.com/2020/07/image-20200703162809913.png?ssl=1) 2. 在**測試的檔案 (index.test.js)** 中 require **被測試的檔案(index.js)**,把要測試的東西按照下列語法規則寫下來。 ![image-20200703162653224](https://i2.wp.com/lailalaii.files.wordpress.com/2020/07/image-20200703162653224.png?ssl=1) - 最外圍的 `describe( ' 描述這個區塊是要測試什麼 ' , function () {要測試的東西 })` 功用是,標示涵蓋在 function 裡面的` test()` ,都是在測試同一個 function,只是用不同引數 (不同的input,尤其是 edge case) 在測試。 因為有可能一個檔案要同時測試一個以上的 function,因此利用 describe 會比較有結構。所以 describe 非必要,但用了會比較方便管理。 - `test( '描述這個測試 ' , function() {expect().toBe()})` 。test ()是 Jest 提供的測試函式。test 裡面有兩個參數:`"描述這個測試"` 和` function() {}` - function 裡面有兩個參數: `expect() `和 `toBe()` - `expect()` 裡面填入要測試的函式的引數 - `toBe()` 裡面填入正確結果 3. 呼叫進行測試 - 第一種辦法: 先在 package.json 裡加入以下物件 ``` scripts: { "test": "jest" } ``` 接著在 CMDer 輸入**`npm run test`** 就會自己尋找有 test.js 的檔案,自動測試結果。 - 第二種辦法: 在 package.json 裡加入以下物件 ``` scripts: { "test": "jest index.test.js" } ``` 和上面相比,差在這個我們直接輸入我們要測試的檔案的名稱。 跟電腦說當我輸入 test 就要 jest index.test.js這個檔案 - 第三種辦法: 在 CMDer 直接輸入 `$ yarn run <測試的檔案>` - 第四種辦法(新版本的 npm 有內建):**`npx jest <測試的檔名>`** 成功畫面 ![Imgur](https://i.imgur.com/A7ESive.png) - 如何加顏色? 在 package.json 裡的 "script" 內容改為 "test": "jest --colors" #### Test-driven development,TDD 先寫測試再寫程式 概念: - 先寫測試再寫 function 的一種開發流程 - 每改完一次就跑測試,看錯的結果是什麼,再回去改。 - test case 要盡可能詳細,要盡可能把所有的邊界條件都放進來 ## 5. ECMAScript 2015 (ES6) 更多 ES6 語法,[詳見此](https://github.com/DrkSephy/es6-cheatsheet)。以下簡單介紹幾個常見的 ES6 語法。 ### 變數宣告 let, const #### let vs const ​ let:可改變,變數只能在作用域當中使用。 ​ const( constant 常數 ):值不可改變。這裡的值要想成是存取的記憶體位置。 ​ (補充資料:[從博物館寄物櫃理解變數儲存模型](https://medium.com/@hulitw/variable-and-frontdesk-a53a0440af3c) by Huli) ​ ![](https://i1.wp.com/lailalaii.files.wordpress.com/2020/07/image-20200703165636434.png?ssl=1) ![image-20200703165926926](https://i1.wp.com/lailalaii.files.wordpress.com/2020/07/image-20200703165926926.png?ssl=1) ​ 左:不能改號碼,一個格子只有一個號碼 `TypeError: Assignment to constant variable.` ​ 右:物件改的是小房間裡的東西,格子裡的號碼(存取的記憶體位置)還是一樣 #### let vs var 作用域 (scoped):變數的生存範圍 變數的運行機制,從最近的往上找 let 和 var 的差別 - 作用域 ​ let, const 作用域: block ​ var作用域: function > ​ 好習慣:不變的東西用 const 宣告,盡量用 let 而不是 var 宣告,因為生存範圍小比較容易 debug ### 字串拼接 #### What? 把 string 用拼接的方式接起來,就叫做字串拼接 #### Why? 1. 原本要印多行字串時,要在字串間加入`\n`才會印多行 2. 如果拼接的字串太多,很容易出現單雙引號混用等語法問題 #### How? 用「``」取代「’’」 - 因為 ES5 語法內在多行拼接的時候需要再加上換行,換成「``」就可以直接進行多行拼接。 ``` let str = ` Hello, world, ` ``` - 用用類似內嵌的方式 `${}` 把變數鑲嵌進來,也可以放程式碼 ex: `${name.toUpperCase()}` ``` function sayHi(name, age) { console.log(`Hi, my name is ${name.toUpperCase()}. I'am ${age} years old. Nice to meet you.`) } sayHi('Nick', 25) /*expected answer: Hi, my name is NICK. I'am 25 years old. Nice to meet you. /* ``` ### 解構 Distructuring #### What? & Why? 呼叫或取 array 或 object 裡的東西時更容易,節省之前要一個個宣告變數的時間。 #### How? 透過解構讓陣列及物件更簡單的被呼叫及執行。更多請看[此](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment)。 - Array ``` let [元素名稱1, 元素名稱2, 元素名稱3] = 原本的陣列名稱 let arr = ['apple', 'banana', 'orange']; let [a, b] = arr; //原本: let a = arr[0], let b = arr[1] console.log(b); //expected answer : banana 會自己去對應,可以只取前幾個 ``` - Object 解構內的物件 key 名稱需與原物件 key 名稱相同。 `let {物件當中的key1, key2} = 物件名稱` ``` let fruitPrice = { apple: 20, //key 名稱 apple banana: 15, orange: 25 }; let {apple, banana, orange} = fruitPrice; console.log(banana); // expected answer : 15 ``` ### 展開 Spread Operator (...) #### What? 將 array 或 object 當中的元素**去除**陣列及物件的**框架**,將元素**複製**到新的陣列或物件中。更多請看[此](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax)。 (複製的好處是抄寫一份同樣的資料到另一個櫃子卻不會和原本的資料互相干擾) #### How? - Array `... + 欲複製的陣列名稱` (可放在陣列中的任一位置) ``` let fruit = ['apple', 'banana', 'orange']; let other = ['pear', 'grape', 'lemon', fruit]; let other1 = ['pear', 'grape', 'lemon', ...fruit]; console.log(other); //expected answer : [ 'pear', 'grape', 'lemon', [ 'apple', 'banana', 'orange' ] ] console.log(other1); //expected answer : [ 'pear', 'grape', 'lemon', 'apple', 'banana', 'orange' ] ``` - Objet `... + 欲複製的物件名稱` (可放在物件中的任一位置) ``` let fruitPrice = {apple: 25, banana: 15, orange: 20 }; let otherFruitPrice = {pear: 30, grape: 35, lemon: 10, fruitPrice }; let otherFruitPrice1 = {pear: 30, ...fruitPrice, grape: 35, lemon: 10 }; console.log(otherFruitPrice); /*expected answer: { pear: 30, grape: 35, lemon: 10, fruitPrice: { apple: 25, banana: 15, orange: 20 } } */ console.log(otherFruitPrice1); /*expected answer: { pear: 30, apple: 25, banana: 15, orange: 20, grape: 35, lemon: 10 } */ ``` #### 記憶體存放位置 因為展開比較像是複製的概念,所以說兩者記憶體內容存放位置不一樣 (不同櫃子),只是存放內容一樣而已。 而過去的直接將陣列或物件放進來時,這樣就會指向同一個記憶體位置,所以存放位置會一樣。 - Array ``` let fruit = ['apple', 'banana', 'orange']; let otherFruit = ['pear', 'grape', 'lemon', fruit]; let otherFruit1 = [...fruit]; console.log(otherFruit[3] === fruit); //answer:true console.log(otherFruit1 === fruit); //answer:false //記憶體存放位置不一樣 (不同櫃子) ``` - Object ``` let otherFruitPrice = {pear: 30, grape: 35, lemon: 10, fruitPrice }; let otherFruitPrice1 = { ...fruitPrice }; console.log(otherFruitPrice.fruitPrice === fruitPrice); //expected answer: true console.log(otherFruitPrice1 === fruitPrice); //expected answer: false ``` #### 在 object 裡展開 array ``` let arr = [1, 2, 3]; let result = { ...arr, c:3 } // { '0': 1, '1': 2, '2': 3, c: 3 } ``` #### 展開衝突時 如果有以數字命名的物件性時,物件內的東西會指向最後給他的東西 ``` let arr = [1, 2, 3]; let obj = { 0:'o', 1:'p' } let result = { ...arr, ...obj, c:3 } console.log(result); // { '0': 'o', '1': 'p', '2': 3, c: 3 } ``` ### 反向展開 Rest Parameters (...rest) #### What? 這邊常和解構共同使用。 解構時不想要每個都定義的話,直接用 Rest Parameters 將剩餘的元素全放入解構的陣列或物件中,呼叫 Rest Parameters 時也會一次呼叫到所有剩餘被放入的元素。 需特別注意的是,Rest Parameters 只能放在 array, object 和 function 中的**最後一個**,不能夾在中間。 更多請看[此](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Functions/rest_parameters)。 #### How? - Array 此案例中的 Rest Parameters 取名為 other。 ``` let fruit = ['apple', 'banana', 'orange']; let otherFruit = ['lemon','grape', ...fruit]; let [l, ...other] = otherFruit; console.log(otherFruit); //answer:[ 'lemon', 'grape', 'apple', 'banana', 'orange' ] console.log(l); //answer:lemon console.log(other); //answer:[ 'grape', 'apple', 'banana', 'orange' ] ``` - Object 此案例中的 Rest Parameters 取名為 rest。 ``` let fruitPrice = { apple: 10, banana: 15, orange: 20 }; let fruitPrice1 = { grape: 50, ...fruitPrice, pear: 25 }; let {grape, ...rest} = fruitPrice1 console.log(fruitPrice1); //answer: { grape: 50, apple: 10, banana: 15, orange: 20, pear: 25 } console.log(grape); //answer: 50 console.log(rest); //answer: { apple: 10, banana: 15, orange: 20, pear: 25 } ``` - Function 此案例中的 Rest Parameters 取名為 numbs。 ``` function multiple(a, b, ...numbs) { return a * b * numbs[1] } console.log(multiple(2, 3, 4, 5, 6)); //answer: 2 * 3 * 5 = 30 ``` ### 預設值 Default Parameters #### What? 語法:`__=__`(加個等號) 在參數上先加入預設值,如果在使用陣列和物件時沒有放入元素、使用函式時沒有放入引數,那樣電腦就會依照先前所設的預設值跑出結果。更多[詳見此](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters)。 #### How? - Array ``` let fruit = ['apple', 'banana', 'orange']; let [a, b, o, g = 'grape'] = fruit; //g 預設值為'grape' console.log(b); //answer:banana console.log(g); //answer:grape ``` - Object ``` let fruitPrice = { apple: 10, banana: 15, orange: 20 }; let {apple, banana = 20, grape = 40} = fruitPrice // banana 預設值為 20,grape 預設值為 40 console.log(apple); //answer:10 console.log(banana); //answer:15 //有定義 console.log(grape); //answer:40 //沒定義,所以輸出預設值 ``` - Function ``` function multiple(a, b = 1, c = 5) {  // b 預設值為 1,c 預設值為 5 return a * b * c } console.log(multiple(2, 3)); //answer: 2 * 3 * 5 = 30 ``` ### 箭頭函式 (`=>`) #### What? & Why? 把原本的 function 和 return 以=> 符號簡化,是 Syntax Sugar 語法的其中之一,在長串語法中可以讓語意的**易讀性更高**。 更多說明詳見[Mozilla](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Functions/Arrow_functions) ![img](https://i.imgur.com/StuXdqb.jpg) 以上的語法上,上下兩種語法呈現的效果相同,但下方的箭頭函式可快速呈現簡易且易讀的語法。但如果說在一個語法內需要包含多個條件及程式時,還是建議寫一般函式。 ### 輸出與輸入 Export and Import (用 babel 跑) #### Before ``` // export function add(a,b){ return a + b } module.exports = add // import let add = require('./utils') console.log(3,5) ``` #### After ``` // export export function add(a,b){ // 這裡前面加了 export return a + b; } export const PI = 3.14; // 這裡前面加了 export // import import {add, PI} from './utils' // 新語法 console.log(3,5) ``` > 現在的 node 還沒辦法跑 ES6 的 import/export 代碼 > 用 `bable-node`指令來跑 #### 其他變形: ``` // export function add(a,b){ return a + b; } let const PI = 3.14; export{ add, PI } ``` 如果想要以其他名稱 export 出去 ``` export{ add as addFunction, PI } // 記得 import 時也要用這個新名稱才找得到是誰 ``` 一次全部 import ``` import * as utils from './utils' // * 就是指所有東西 console.log(utils.addFunction(3,5), utils.PI); ``` 總結 Export 幾種方式: 1. export function 函式名稱(){} export function add(){},使用 import {add} 引入 2. export{函式名稱} export { add },與上面那種一樣 3. 預設值default 用法 `export default function add()`,使用 import add 引入,不需要加大括號 如果想要用其他名字,可以用 as 取別名,例如說 export { add as addFunction }輸出端 ``` export const default double => n * 2 //預設輸出就是輸出 double 這個函式 export const PI = 3.1415926 ``` ##### 輸入端 ``` import double, {PI} from './index'; // 預設值不需要加大括號 console.log(double(2), PI); //answer: 4 3.1415926 ``` 4. 可以用 import * as utils from ‘utils’ 把全部都 import 進來 ### Babel 安裝與使用 #### What? - 執行 JavaScript ES6 語法的編譯器 compiler - 是一個前端開發的工具 - [官方](https://babeljs.io/docs/en/next/babel-node.html)的安裝說明,及簡易使用步驟。 #### Why? 因為前端開發的變化速度比瀏覽器還要快,所以為了對 code 進行編譯,透過 Babel 讓原本不支援的環境也可以順利執行。實作: 參考[bable](https://www.lidemy.com/courses/402888/lectures/6219705)影片 - ES6/7/8 => Babel => ES5(或其他更舊的) #### How? 1. 安裝必要套件:`npm install --save-dev @babel/core @babel/node @babel/preset-env` 2. `npm install --save @babel/present-env` 跟 babel 說要轉成多舊的語法 3. 新增 .babelrc 檔案,在此檔案填入以下內容。 跟 bebel 說我要怎麼用這個套件(?) ``` { "presets": ["@babel/preset-env"] } ``` 4. `npx babel-node <檔案名稱>`即可執行 ES6 語法。 注意不要用在 production 上,因為效能不太好。如果要 production 通常會先 compile 成舊版,再去 node