--- tags: Vue3 新手夏令營 --- # Vue3 新手夏令營 - 課前章節 JS 必備觀念 ## this 指向 ### 傳統函式 this 的指向相當複雜 在傳統函式中大部分(幾乎95%)都是與調用的方式有關 主要看的是,在調用該函式時,函式的前方是否帶有物件 以下使用各種範例做說明: - 一個函式中包含多少參數,請參考以下程式碼。 ```javascript= var a = '全域' function fn(params) { console.log(params, this, window, arguments); debugger; } // 上述可知 一個函式中包含 console.log 中這四個參數, // 且在該範例中 this 指向 全域物件 ``` - this 的指向為何,請參考以下程式碼。 ```javascript= var obj = { name: '小明', fn: function(params) { console.log(params, this, window, arguments); debugger; } } obj.fn() // 這裡是用 obj.fn 去調用該函式 所以此時的 this 會指向 obj ``` - 傳統函式中的 this 只與調用方式有關,請參考以下程式碼。 ```javascript= var someone = '全域'; function callSomeone() { console.log(this.someone); } callSomeone(); // 此為簡易呼叫,詳見下方補充。 // 函式前方不帶任何物件 所以此時 this 指向全域物件 ``` > 【補充】 > simple call(簡易呼叫):函式前方不帶任何物件,這種呼叫方式就是 simple call(簡易呼叫)。 - 各種運用變化 1. 把函式放在物件裡面宣告,請參考以下程式碼。 ```javascript= var obj = { someone: '物件', callSomeone() { console.log(this.someone); } } obj.callSomeone(); // 這裏用 obj.callSomeone 呼叫函式 所以 this 指向 obj ``` 2. 把函式寫在全域中 再到物件裡獲取 後通過物件調用函式,請參考以下程式碼。 ```javascript= function callSomeone() { console.log(this.someone); } var obj2 = { someone: '物件2', callSomeone } obj2.callSomeone(); // 這裏用 obj2.callSomeone 呼叫函式 所以 this 指向 obj2 ``` 3. 把函式寫在全域中 再到物件裡獲取 後通過物件調用函式,看誰調用就是指向誰,請參考以下程式碼。 ```javascript= function callSomeone() { console.log(this.someone); } var wrapObj = { someone: '外層物件', callSomeone, innerObj: { someone: '內層物件', callSomeone, } } wrapObj.callSomeone(); // 這裏用 wrapObj.callSomeone 呼叫函式 所以 this 指向 wrapObj wrapObj.innerObj.callSomeone(); // 這裏用 wrapObj.innerObj.callSomeone 呼叫函式 所以 this 指向 wrapObj.innerObj ``` 4. 把函式寫在全域中 再到物件裡的方法中調用該函式 最後一樣通過物件調用函式,請參考以下程式碼。 ```javascript= function callSomeone() { console.log(this.someone); } var obj3 = { someone: '物件 3', fn() { callSomeone(); // 補充:平常不會這樣去取用 this } } obj3.fn(); // 這裡因為 callSomeone 被調用時前方不帶任何物件所以 this 指向全域 ``` 5. 使用 setTimeout 回調函式調用,知識點為『回調函式大多是簡易呼叫』,請參考以下程式碼。 ```javascript= var obj4 = { someone: '物件 4', fn() { setTimeout(function () { console.log(this.someone); }); } } obj4.fn(); // setTimeout 是 callback function(回調函式) // 而 callback function(回調函式)大部分都是簡易呼叫 // 所以 setTimeout 的 this 會指向到全域 ``` ### 箭頭函式 箭頭函式沒有自己的 this 箭頭函式的 this 會指向它外層作用域的 this 當箭頭函式的外層找不到任何函式時,箭頭函式的 this 就會指向全域 以下使用各種範例做說明: - 箭頭函式的縮寫,請參考以下程式碼。 ```javascript= const arr = [1, 2, 3, 4, 5]; const filterArr = arr.filter(item => item % 2); // 這裡在取有餘數的值(單數) console.log(filterArr); // 輸出結果是 [1, 3, 5] // 箭頭函式寫法:首先把傳統函式改成箭頭函式 只需要把 function 拿掉 並在 () 後方加入箭頭 => 即可 // 當沒有參數時 就寫 () => {...} // 一個參數時可寫成 item => {...} // 多個參數時寫 (a, b) => {...} // 當函式中是 return 某個簡單的東西可以省略 return 與 {} 並將其改寫成一行 如上代碼 ``` - This 綁定的差異,請參考以下程式碼。 ```javascript= // 活用觀念,將內層改為箭頭函式 var name = '全域' const person = { name: '小明', callName: function () { console.log('1', this.name); setTimeout(() => { console.log('2', this.name); console.log('3', this); }, 10); }, } person.callName(); // 已知在不改成箭頭函式時 setTimeout 中的 this 會指向全域 // 但假設把 setTimeout 改成箭頭函式(如上) this 就可以指向外層函式的 this 即指向 person ``` - 陷阱題1,請參考以下程式碼。 ```javascript= var name = '全域' const person = { name: '小明', callName: () => { console.log(this.name); }, } person.callName(); // 首先看到箭頭函式 就知道 this 會指向他外層的 this // 但這裡外層沒有其他函式 表示外層直接就是全域 所以 this 會指向全域 ``` - 陷阱題2,請參考以下程式碼。 ```javascript= var name = '全域' const person = { name: '小明', callMe() { const callName = () => { console.log(this.name); }; callName(); } } person.callMe(); // 承上題 看到箭頭函式 就找外層 這裡外層是 callMe // callMe 的 this 指向 person 所以 callName 就也指向 person ``` - 實戰手法,確保 this 指向 obj4 的兩種方式 1. 把 this 先指向其他變數 如 `const vm = this;` 這個 vm 在 Vue 中意指 ViewModel,請參考以下程式碼。 ```javascript= var someone = '全域'; var obj4 = { someone: '物件 4', fn() { const vm = this; setTimeout(function () { console.log(vm.someone); }); } } obj4.fn(); // 在開頭有先宣告 vm = this 所以 vm 會指向 obj4 ``` 2. 直接使用箭頭函式,請參考以下程式碼。 ```javascript= var someone = '全域'; var obj4 = { someone: '物件 4', fn() { setTimeout(() => { console.log(this.someone); }); } } obj4.fn(); // 箭頭函式看外層的 this,所以看 fn 的 this,就會指向 obj4 ``` ## 關注點分離 關注點分離主要就是把資料跟畫面做分離 這裏以原生 JS 實作 todo list 來說明 主要會分成三大類別: 畫面(HTML) 資料(data) 方法(幫畫面與資料做溝通的各種 function) 以下開始撰寫程式碼: >首先我們用物件的方式來建立元件 component >component 的結構會有以下部分: >- 資料(data) >- 方法、觸發器(render, delItem) >- 生命週期(init) ```javascript= const component = { data: [ // 資料 "第一個資料", "第二個資料", "第三個資料", ], delItem(id) { // 事件觸發器 this.data.splice(id, 1); this.render(); }, render() { // 渲染畫面的方法 const list = document.querySelector("ul"); let str = ""; this.data.forEach((item, i) => { str += `<li>${item} <button type="button" class="del" data-id="${i}">刪除</button></li>`; }); list.innerHTML = str; const btns = document.querySelectorAll(".del"); btns.forEach((item) => { item.addEventListener("click", (e) => { // 這裏要記得用箭頭函式確保你取得的 this 指向 component this.delItem(e.target.dataset.id); }); }); }, init() { // 生命週期,進入畫面時第一次會觸發的方法 this.render(); }, }; component.init(); ``` 基本上 Vue 會接觸到的結構就是資料、事件觸發器以及生命週期, 渲染方法會透過 Vue.js 去處理,所以在 Vue.js 基本上是不會用到渲染方法的。 以上就是關注點分離的一個基礎概念。 ## 物件參考的特性 以下使用各種範例做說明: - 物件是以傳參考的形式賦值,請參考以下程式碼。 ```javascript= const person = { name: '小明', obj: {} } const person2 = person; // 首先我們建立一個新物件等於原物件 person2.name = '杰倫'; // 然後修改他的屬性 console.log(person2 === person) // 輸出結果為 true // 物件傳參考的特性會讓兩個物件指向同一個記憶體位置 // 假設我們又建立一個 obj2 = person.obj 如下: const obj2 = person.obj; obj2.name = 'person > obj > name'; // 此時輸出 person 就會發現 person.obj 同樣增加了 name 屬性 // 且值為 'person > obj > name' ``` - 陷阱(常見的錯誤)請參考以下程式碼。 ```javascript= const fn = (item) => { item.name = '杰倫'; } const person = { name: '小明', obj: {} } fn(person); console.log(person); // name 變成杰倫了 這也是由於物件傳參考的特性 ``` >【補充】 >關於這樣的錯誤,ESLint 也有建議不要這麼做(但你是有經驗的開發者時,下方也有提供關閉的方式) >https://cn.eslint.org/docs/rules/no-param-reassign - 解決方案 1. 淺層拷貝,請參考以下程式碼。 ```javascript= const person = { name: '小明', obj: {} } // 方法一 複製某物件的所有內容到新的物件身上 const person2 = Object.assign({}, person); // 方法二 用展開把物件拷貝過來 const person3 = {...person}; // 以上兩方式都屬於淺拷貝 // 淺層拷貝是指他只會複製第一層的內容 第二層起 若也是物件 就還是傳參考的特性 ``` 2. 深層拷貝,請參考以下程式碼。 ```javascript= const person = { name: '小明', obj: {} } const person2 = JSON.parse(JSON.stringify(person)); // 深層拷貝的方式為 把物件先轉成字符串 再把字符串轉回物件 // console.log(person === person2) 會輸出 false 兩者已無任何關聯 ``` ## Promise 以下使用各種範例做說明: - 非同步的觀念 ```javascript= function getData() { setTimeout(() => { console.log('... 已取得遠端資料'); }, 0); } const component = { init() { console.log(1); getData(); console.log(2); } } component.init(); // 上述執行順序會是 1 > 2 > ... 已取得遠端資料 // 原因是 setTimeout 屬於非同步行為 // 而 JS 中的所有非同步行為都會在最後才執行 // 更正確的說法,Promise 是為了解決傳統非同步語法難以建構及管理的問題,如有興趣可搜尋 "callback hell js"(JS 回調地獄) ``` - Promise 實例,請參考以下程式碼。 ```javascript= // 在此不需要學習 Promise 的建構方式,僅需要了解如何運用即可 const promiseSetTimeout = (status) => { return new Promise((resolve, reject) => { setTimeout(() => { if (status) { resolve('promiseSetTimeout 成功') } else { reject('promiseSetTimeout 失敗') } }, 0); }) } // 首先建立一個函式 該函式回傳一個 promise 的方法 // promise 中會傳入兩個參數 分別是 resolve 跟 reject 他們各自會回傳一個結果 // 我們可以看到在建立的函式裏有傳入一個 status 參數,他表示一個狀態 // 基本上執行過程就是判斷這個 status 狀態 // 當 status 狀態為成功時會執行 resolve,失敗則執行 reject // 成功可用 .then() 執行後續代碼,失敗則只能用 .catch() 接收 ``` - 基礎運用,請參考以下程式碼。 ```javascript= const promiseSetTimeout = (status) => { return new Promise((resolve, reject) => { setTimeout(() => { if (status) { resolve('promiseSetTimeout 成功') } else { reject('promiseSetTimeout 失敗') } }, 0); }) } promiseSetTimeout(true) .then(function(res) { console.log(res); }) // 調用 promiseSetTimeout 函式 然後傳入 status 為 true // 此時會返回 resolve 結果 我們可以通過 .then() 來接收該結果 // 這裡設回傳的結果為參數 res 然後使用 console.log(res) 查看書處內容 // 最後就會得到『promiseSetTimeout 成功』 ``` - Promise 的串接方法,請參考以下程式碼。 ```javascript= const promiseSetTimeout = (status) => { return new Promise((resolve, reject) => { setTimeout(() => { if (status) { resolve('promiseSetTimeout 成功') } else { reject('promiseSetTimeout 失敗') } }, 0); }) } promiseSetTimeout(true) .then(function(res) { console.log(1, res); return promiseSetTimeout(true); }) .then(function(res) { console.log(2, res); }) // 我們在 .then() 中可以使用 return 帶入另一個非同步行為 // 接著就可以繼續使用 .then() 來做串接 // 此時輸出就會是 1 promiseSetTimeout 成功 > 2 promiseSetTimeout 成功 // 這樣做他就可以實現依序執行,先執行第一次調用,再執行 return 的非同步行為 ``` - Promise 失敗捕捉,請參考以下程式碼。 ```javascript= const promiseSetTimeout = (status) => { return new Promise((resolve, reject) => { setTimeout(() => { if (status) { resolve('promiseSetTimeout 成功') } else { reject('promiseSetTimeout 失敗') } }, 0); }) } promiseSetTimeout(false) .then(function(res) { console.log(res); }) .catch(function(err) { console.log(err); }) // 在執行 promise 時要有 .then() 方法接收成功結果, // 也必須有 .catch() 方法接收失敗結果,這樣才是正確的使用方式哦 ``` - 元件運用,請參考以下程式碼。 ```javascript= const promiseSetTimeout = (status) => { return new Promise((resolve, reject) => { setTimeout(() => { if (status) { resolve('promiseSetTimeout 成功') } else { reject('promiseSetTimeout 失敗') } }, 0); }) } const component = { data: {}, init() { promiseSetTimeout(true) .then((res) => { this.data = res; console.log(this.data); }); }, }; component.init(); // 上述過程是在元件初始化時,就把非同步的資料寫回 data 的方式 // 這是在實戰中非常常見的結構 ``` - 實戰取得遠端資料,請參考以下程式碼。 ```javascript= // 這裡要記得引入 axios 套件後再撰寫程式碼哦 // https://github.com/axios/axios 是 axios 的文件連結 // 以下使用 randomuser api 做測試(https://randomuser.me/api) axios.get("https://randomuser.me/api") .then((res) => { console.log(res.data.results); }) .catch((err) => { console.log(err.response); }); // 首先要知道 axios 是基於 promise 所建立的 // 所以一樣可通過 .then() 和 .catch() 取得成功與失敗結果 // 使用方式就直接寫 axios.get('傳入網址').then(...).catch(...) 即可 // 記得每次除了 .then() 外都要捕捉錯誤 // Axios 錯誤捕捉技巧因有對 err 進行過封裝 // 所以回傳失敗內容須通過 err.response 才能獲取到 ``` ## ES Module JavaScript 的模組化,就是可以將檔案拆分 進行匯入、匯出等等。 在過去這種模組化都是需要透過一些工具來進行編譯的 但近幾年許多瀏覽器陸續都開始支援模組化的功能了 以下介紹如何在瀏覽器下進行這種原生的模組概念: 1. script 標籤必須加上 type="module" ,該程式碼才能進行匯出與匯入 2. 要先 export 匯出 才能做 import 匯入 3. 匯出分成兩種方式:預設匯出以及具名匯出。 4. 預設匯出:常見的匯出方式,通常用於匯出物件,在 Vue 開發中可用來匯出元件。如 `export default {...}` 5. 具名匯出:用於匯出已定義的變數、物件、函式等,專案開發中通常用於 『方法匯出』,第三方的框架、函式、套件很常使用具名定義 『方法』。如 `export const a = xxx; export function b() {...};` 6. 匯入方法分成三種方式:預設匯入、具名單一匯入、具名全部匯入。 7. 預設匯入:因為預設匯出沒有名字,所以可以為它命名 如 `import $ from "./jquery.js"`(注意:因為傳參考的特性,因此元件無法重複利用,但在 Vue.js 中會解決此問題) 8. 具名匯入 - 單一匯入:建議寫法,如 `import { a, b } from "./xxx.js";` 通過 a 或 b 調用 9. 具名匯入 - 全部匯入:不建議寫法,如 `import * as all from "./xxx.js";` 通過 all.a 或 all.b 調用 10. SideEffect:通常用在舊的函式庫,因為裡面直接是立即函數,所以不需匯出就可直接匯入。如 jQuery 可直接 `import "./jquery.js";` ## ES Module 延伸 基本上每個 `<script type="module"></script>` 的作用域都是獨立的, 假設故意把變數或函式寫成 window.xxx 的話就可以在其他 module 中通過 window.xxx 獲取到(平常不太會這樣做,了解一下這個觀念即可) 但如果一個是 module 另一個是普通 script 就獲取不到 window.xxx 另外在網路上可看到許多套件都逐漸釋出了 ESM 版本給大家使用 如果條件允許的話是可以直接使用 import 方式進行載入的 像之後的 Vue.js 就已經有釋出 ESM 的形式了(可以到 [cdnjs](https://cdnjs.com/) 中查找 Vue) 有些 cdn 的名稱是 vue.esm-xxx 這種的就是 ESM 的版本了 像 vue.esm-browers 就是專門給瀏覽器使用的 這邊我們就用 vue.esm-browers 練習載入 ESM 先到 [Vue3 官網 - 介紹](https://v3.vuejs.org/guide/introduction.html#declarative-rendering)拷貝初始化的程式碼如下: ```htmlembedded= <div id="counter"> Counter: {{ counter }} </div> ``` ```javascript= const Counter = { data() { return { counter: 0 } } } Vue.createApp(Counter).mount('#counter') ``` 接著把他們改成使用 module 方式匯入: ```htmlembedded= <div id="counter"> Counter: {{ counter }} </div> <script type="module"> // 建立 module // 通過具名匯入的單一匯入方式把 createApp 載入進來 import { createApp } from "https://cdnjs.cloudflare.com/ajax/libs/vue/3.1.4/vue.esm-browser.min.js"; const Counter = { data() { return { counter: 0, }; }, }; // 把 Vue.createApp 改成 createApp 即可運作 createApp(Counter).mount("#counter"); </script> ``` # 6/2 直播錄影檔 - 那個 let, const, var 到底差在哪? 以下使用各種範例做說明: - 為什麼要宣告變數,請參考以下程式碼。 ```javascript= function fn() { a = 0; } fn(); // 舉例來說像上方程式碼沒有宣告 a 但可以取到 a 的值 // 這個 a 就稱為一個全域屬性 // 但由於很難知道 a 的來源是哪,所以我們不推薦這樣做 ``` - 基於上述深入講解,請參考以下程式碼。 ```javascript= // 假設這是第 1 行程式碼 function fn() { a = 0; } fn(); // 然後這是第 300 行程式碼 function fnB() { a = 1; } fnB(); console.log(a); // 上方結果 a 會是 1 // 但當程式碼很多行 我們有可能會忘記前面使用過 a // 進而導致一些很難除錯的問題 ``` - 所以上述的正確撰寫方式應該如下 ```javascript= // 假設這是第 1 行程式碼 function fn() { var a = 0; } fn(); // 這是第 300 行程式碼 function fnB() { var a = 1; } fnB(); console.log(a); // 上方結果會是 a is not defined // 因為每個函式都有自己的作用域 // 所以兩個函式的 a 都只在自己的作用域中 不會互相衝突 // 這也是我們要宣告變數的原因 當我們沒有宣告變數的時候就可能造成全域的污染 ``` - 全域、區域污染,請參考以下程式碼。 ```javascript= a = 0; function fn() { a = 1; } fn() console.log(a) // 上方結果為 1 因為他們沒有被宣告成變數 兩個就都是全域屬性 ``` 1. 屬性可以被刪除,請參考以下程式碼。 ```javascript= a = 0; console.log(a); // 輸出 0 console.log(window); // 全域物件 且裡面會有 a 屬性 delete window.a // 用 delete 刪除屬性 a console.log(a); // 輸出 a is not defined console.log(window); // 全域物件的 a 也不見了 ``` 2. 變數無法被刪除,請參考以下程式碼。 ```javascript= var b = 1; console.log(window) // 全域物件 且裡面有 b delete window.b // 刪除屬性在這裡不起作用了 因為 b 是變數不是屬性了 console.log(window); // 所以這裡 b 還是存在 console.log(b); // 可以輸出 1 ``` - var 的作用域,請參考以下程式碼。 ```javascript= function fn() { var a = 1; debugger; // 這個可以讓程式碼停在這一行 並開啟 Sources 功能 // 此時可以看到右邊的 Scope(作用域)裡面有一個 Local 就是他目前的函式中有哪些東西 } fn(); ``` 1. 全域跟函式中各自宣告 a,兩者不會互相影響,因為函式有自己的作用域,請參考以下程式碼。 ```javascript= var a = 0; function fn() { var a = 1; console.log('local', a) // 1 } fn(); console.log('全', a) // 0 ``` 2. 全域宣告了 a,函式把 a 的值改成 1,此時函式中的 a 就等於全域中的 a ,請參考以下程式碼。 ```javascript= var a = 0; function fn() { a = 1; } fn(); console.log('全', a) // 1,在函式中 全域的值被改變了 ``` - var 的辭法作用域 1. var 作用域在程式碼寫完的當下就確定了,請參考以下程式碼。 ```javascript= var a = 0; function fnA() { console.log(a); } function fnB() { var a = 1; } fnB(); // 他有自己的作用域在函式裡面,所以不會影響到全域 fnA(); // 這裡就很清楚,輸出結果是全域的 0 ``` 2. 上方的進階題,請參考以下程式碼。 ```javascript= var a = 0; function fnA() { console.log(a); } function fnB() { var a = 1; fnA(); } fnB(); // 這裏要清楚一件事: // 我們不是看在哪調用 fnA() 去決定 a 值 // 而是看 fnA() 被創建在哪、函式中是否有宣告 a // 沒有就找 function fnA() {...} 的外層 // 所以上面這題就指向全域的 a,輸出為 0 // 這邊也說明一個概念: // 你不需要執行代碼後才去找 a ,而是在程式碼寫完的當下就已經可以確定 a 的值了 ``` - var 特性 1. function 作用域與重複宣告,請參考以下程式碼。 ```javascript= function fn() { var a = 1; var a = 0; } fn(); // 作用域只在函式裡 且 var 重複宣告也不會報錯 在該函式外會取不到 a ``` 2. 為 let 埋下的梗....請參考以下程式碼。 ```javascript= { var b = 2; } console.log(b); // {} 是一個 block,沒任何作用,就只是一個 block,先說明程式碼可以這樣寫不會報錯 // 然後在 block 裡面用 var 宣告變數 b,在 block 外也可以獲取到這個變數 b ``` 3. for 迴圈,請參考以下程式碼。 ```javascript= for (var i = 0; i < 10; i++) { console.log(i); // 這裡會輸出 0~9 } console.log(i); // 這裡會輸出 10,因為 i 超過 9 才會跳出 for 迴圈 ``` 4. for迴圈 + setTimeout,請參考以下程式碼。 ```javascript= for (var i = 0; i < 10; i++) { setTimeout(() => { console.log(i); }, 0); } // 輸出 10 因為 setTimeout 屬於非同步行為 // JS 默認在所有事件結束後才會執行非同步行為 // 所以執行 setTimeout 時 i 已經跳出 for 了 ``` 5. hoisting(提升),請參考以下程式碼。 ```javascript= console.log(a); // 輸出 undefined var a = 1; console.log(a); // 輸出 1 // 我們在初始化的過程中嘗試去取值 // 即在 var a = 1; 上一行 console.log(a) // 會取得 undefined,這就是 hoisting // 另外 a is not defined 是表示我們完全沒宣告就取 a ``` - let 與 var 差異 1. let 屬於 block 作用域,請參考以下程式碼。 ```javascript= { let a = 1; } console.log(a); // 上方 console.log 會出錯 a is not defined // 這是因為 let 的作用域只存在 block 內,不像 var 還能在外層取到值 ``` 2. function 也是 block 所以離開函式無法取到 a 請參考下方程式碼。 ```javascript= function fn() { let a = 1; } console.log(a); // 這個也會出錯,a is not defined // let 的作用域主要就是看 {},所以有看到 {} 都會影響到他的作用域 ``` 3. for ```javascript= for (let i = 0; i < 10; i++) { console.log(i); // 這裏會輸出 0~9 setTimeout(() => { console.log(i); // 這裏也輸出 0~9 }, 0); } console.log(i); // 會出錯 // 這邊我們知道在 for 迴圈裡使用 var 時 setTimeout 都是輸出 10 // 但 let 卻可以正常輸出 0~9 // 原因就是 let 屬於 block 作用域 這讓每個 i 都獨立存在於 block 內 // 這也是為何 let 比 var 穩定很多的原因 // 另外這是一個常見考題 下次遇到就知道怎麼做囉~~(用 let 啦哪次不用) ``` 4. let 宣告的變數不會出現在 window 上,請參考以下程式碼。 ```javascript= var a = 0; let b = 1; console.log(window) // 上方輸出後打開 window 這個全域物件會看到 a 但不會看到 b ``` 5. let 無法重複宣告同一個變數,請參考以下程式碼。 ```javascript= let a = 0; let a = 1; console.log(a) // 會報錯 Identifier 'a' has already been declared // 已經宣告就過不能再重複宣告 ``` 6. let 沒有 hoisting 但有暫時性死區(TDZ),請參考下方程式碼。 ```javascript= // 變數的部分 console.log(a) let a = 0; // 這樣做會報錯 Cannot access 'a' before initializatio // 你不能在初始化之前獲取他 需在宣告的下一行才執行操作 ``` ```javascript= // 函式的部分 function fn(a) { console.log(a) var a = 2; console.log(a) } fn(1) // 上方程式碼可以正常執行,但若把上方的 var 改成 let 就會出錯 // 你以為宣告前可以獲取 a 但其實不行,這就是使用 let 宣告時的暫時性死區 // 他會使你無法取到第一個 console.log(a) 的值 ``` - const 特性,請參考下方程式碼。 ```javascript= // 我們可以像下方這樣重新賦值 let a = 0; a = 1; a = 2; console.log(a); // 輸出結果為 2 // 但不可以像下面這樣 const b = 0; b = 1; console.log(b); // 會報錯 Assignment to constant variable // 因為用 const 宣告的是常數 常數是不能被重新賦值的 ``` - 物件傳參考的特性 1. 只需修改屬性值的話 就可以用 const 宣告的物件,請參考下方程式碼。 ```javascript= const a = { name: '卡斯伯' } a.name = 'Ray' ``` 2. 修改整個物件的話 就不可以用 const 宣告,請參考下方程式碼。 ```javascript= const a = { name: '卡斯伯' } a = { name: 'Ray' } // 結論:可以用 const 就用 const 不要都用 let 宣告 ``` # 6/9 直播錄影檔 - 物件傳值?傳參考? 以下使用各種範例做說明: - 入門考題 變數傳值的部分 ```javascript= var person = '小明'; var person2 = person; console.log(person === person2) // true // 這邊是 傳值 把小明那個 person 記憶體複製一份給 person2 // 所以 person2 會得到 person 的值 小明 // 接下來我們修改 person2 的值如下 person2 = '杰倫'; console.log(person, person2); // 小明 杰倫 // 兩個就是各自的記憶體位置 不會互相影響 ``` - 要開始傳參考了 這是物件的形式 ```javascript= var person = { name: '小明' } var person1 = person; // 把兩個物件的記憶體位置指向同一個地方 console.log(person === person1) // 這裡會輸出 true(我知道你知道) // 然後我們再來改一下 person1 物件的 name person1.name = '杰倫' ; console.log(person.name); // 噠噠~這裡會輸出杰倫哦 // 這就是物件傳參考啦 他們的記憶體指向同一個地方 // 所以 person1 跟 person 對應的記憶體位置是同一個 // 改 person1 物件中屬性的時候 person 物件就也會跟著變囉 // 這是最常見的狀況 ``` - 來看看陣列是否也有這個問題吧 ```javascript= var member = ['爸', '媽']; var member2 = member; member2.push('小三'); console.log(member); // 有小三 // 這是因為 JS 裡面只有物件這個型別 沒有陣列 陣列也是物件 ``` - 再來看看函式有沒有這個問題 ```javascript= function fn(name) { return `${name}被抓到了` } var fn2 = fn; fn2.magicName = '奇怪的東西'; console.log(fn === fn2); // true // 上面為 true 是因為 JS 裡面只有物件這個型別 沒有函式 函式也是物件 console.dir(fn2); // console.dir 主要用於顯示物件,他會顯示物件中詳細的每個屬性及 prototype 的資訊 // 所以用 console.dir 就可以看到 fn2 中有 函式 以及 magicName ``` - 利用傳參考的特性把物件名稱或函式改成中文字 ```javascript= function 函式(name) { return `${name}被抓到了` } console.log(函式('漂亮阿姨')); var 名偵探 = console; 名偵探.柯南 = console.log; 名偵探.柯南(函式('漂亮阿姨')) // 由於傳參考的關係 上方這些寫法都是可以動的 ``` - let/const ```javascript= var 名偵探 = console; 名偵探.柯南 = console.log; const person = { name: '小明' } person.name = '杰倫'; 名偵探.柯南(person) // 上方這種因為傳參考特性 可以用 const 宣告物件(因為只改物件中的屬性值 沒有改整個物件指向) // 這種就建議使用 const 宣告而不要用 let or var 做宣告 // 但如果是要指向新的物件 如下 person = {}; // 這樣在一開始宣告 person 就要使用 let ,如果用 const 就會報錯 // 結論:若只是改變屬性值 建議就直接使用 const 宣告了 ``` - let/const ```javascript= const family = ['爸', '媽', '小三']; family.forEach((item, key) => { if(item === '小三') { family.splice(key, 1) } }); console.log(family); // 小三被拿掉了 // 這裡因為是修改陣列中的其中一個 所以也是傳參考 可以用 const 哦 ``` - function 改物件屬性 ```javascript= function fn(item) { item.name = '杰倫'; } const person = { name: '小明', } fn(person); console.log(person); // name = 杰倫 // 可以清楚看到這裡只有一個物件 就是 person // 所以在函式中也只是改變這個物件的屬性值而已 還是傳參考~ // 這也是實戰中需要注意的部分 盡量不要把傳入函式的物件屬性值改掉 因為會改到原始的物件哦 ``` <!-- - 實戰中常見的狀況 ```javascript= function fn(item) { const newItem = { name: '杰倫' } // item = newItem; // item.name = newItem; Object.keys(item).forEach(key => { 名偵探.柯南(key); item[key] = newItem[key]; }) console.log('item', item); } const person = { name: '小明', } fn(person); console.log(person); // 1小明 , 2. 杰倫 // 3 傳參考很棒 4. 傳參考很煩 // #1 淺層複製 // const person = { // name: '小明' // } // const person2 = { ...person }; // person2.name = '杰倫'; // console.log(person2.name, person.name); // console.log(person === person2); // #2 深層複製 // var person = { // name: '小明', // family: { // name: '小明家', // members: ['爸', '媽'] // } // } // const person2 = JSON.parse( JSON.stringify(person)); // person2.name = '杰倫'; // person2.family.name = '杰倫家' // console.log(person, person2); // console.log(person.family === person2.family); // 1 相等、2 不相等 // 很難打的按 3 // 擴展 // const person = { // name: '小明', // fn: function() { // console.log(`我叫作 ${this.name}`); // } // }; // const person2 = { // ...person, // name: '杰倫', // }; // person.fn(); // person2.fn(); // 1. 小明 , 2. 杰倫 // 3. 小明 , 4. 杰倫 // const family = [{name: '爸'}, { name: '媽' }]; // family.forEach((item, key) => { // const newItem = { // name: '杰倫' // }; // family[key] = newItem; // console.log('family[key]', family[key]); // console.log('item', item); // }); // console.log(family); // 請問在此有加上 杰倫 ㄇ? // 1有杰倫, 2.沒有杰倫 // 真地獄 // var person = { // name: '小明' // } // person.person = person; // console.log(person.person === person.person.person); // true 3 or false 4 var a = { x: '小明' } var b = a; a.y = a = { x: '杰倫' }; // console.log(b === a); // 1一樣、 2不壹樣 console.log('a:', a ); console.log('b:', b ); // 1. a 結果 // 2. { x: '杰倫'} // 3. 其它 console.log(b.y === a); // 1一樣、 2不一樣 ``` -->