---
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不一樣
```
-->