0%

JavaScript ES6 課程筆記

使用let 與 const 宣告變數

ex: (var 宣告)

1
2
3
4
5
console.log(mom); // undefined
var mom = '老媽';
(function(){
console.log(mom); // 老媽
})();

ex: (let 宣告)

1
2
3
4
5
console.log(mom);  // "ReferenceError: Cannot access 'ming' before initialization"
let mom = '老媽';
(function(){
console.log(mom); // 老媽
})();

以上這兩個宣告方法的差異可以看出來,用let宣告是沒有Hoisting的特性,
所以,導致在let定義變數之前,就取用該變數的話,會跳出在初始化該變數之前無法調用該變數的錯誤,
var宣告是有Hoisting的特性,
所以,在var定義變數之前,就取用該變數的話,只會跳出undefined的值。

var 與 let 作用域的不同

var的作用域是function scope
let的作用域是block

ex: (var 作用域)

1
2
3
4
5
6
7
8
function varMing() {
var ming = '小明';
if(true){
var ming = '傑哥';
}
console.log(ming); // 傑哥
}
varMing()

以上範例可以看到,if判斷式的範圍跟外層的var ming = '小明'作用域範圍是相同的,
因為,它們都是存在同一個function scope裡面(即函式varMing裡面),不要被if判斷式的花括號給誤導了,
所以,再重新定義一次變數ming是會影響最後變數ming的數值。

ex:(let 作用域)

1
2
3
4
5
6
7
8
function varMing(){
let ming = '小明';
if(true){
let ming = '傑哥';
}
console.log(ming); // 小明
}
varMing()

以上這個範例可以看到,最終結果是小明,
原因是let的作用域是用block來區分,所以,if判斷式中的作用域跟外層的作用域是分開的,
所以,我們在if中再次對ming變數做定義,是不會影響到外層變數ming的值的。

function block 與 block scope 的不同 - 經典範例

ex: (var 範例)

1
2
3
4
5
6
for(var i=0; i!=10; i++){
console.log(i);
setTimeout(function(){
console.log('這是第'+i+'次的執行');
}, 10);
}

以上的範例,你會看到最終setTimeout裡面執行的結果,
都是這是第10次執行。
這是因為,在for中我們用var去宣告i,導致這個i是一個全域變數。
setTimeout是一個非同步的函式,等到你的for執行完之後,才去執行
setTimeout,這個時候,它會吃到全域變數的i的值,也就是已經累加完的結果,
也就是10。

ex: (let 範例)

1
2
3
4
5
6
for(let i=0; i!=10; i++){
console.log(i);
setTimeout(function(){
console.log('這是第'+i+'次執行');
}, 10);
}

這邊我們改以let來定義for迴圈中的i,因為,let的block scope的特性,所以,該i變成一個區域變數,
此時,setTimeout的i就可以正常地被依序綁定,for迴圈遍歷當下的i值囉~

const

const就是宣告一個常數,而常數是不能被更換的。
ex:

1
2
const ming = '你好嗎';
ming = '我不好'; // Error

以上這個範例會報錯喔,它會說常數ming被重新賦值了。

const 與物件

雖然,我們上面有提到常數是不能被更改的。
但是,用const所定義的物件,該物件的的屬性是可以被更改的喔。
ex:

1
2
3
4
5
6
7
const family = {
mom: '老媽',
me: '小明',
sister: '小紅'
}
family.father = '老爸';
console.log(family);

以上的結果是不會報錯的喔,因為,在JS中傳遞物件,是傳參考,並不是傳值,
所以,只要我們沒有修改該物件的參考,要為它新增或更改屬性值都是沒問題的喔。

什麼叫改參考?

那要什麼叫改參考呢,我們修改一下上面的範例
ex:

1
2
3
4
5
6
const family = {
mom: '老媽',
me: '小明',
sister: '小紅'
}
family = {}; // Error

以上,我們將family在指向另一個空物件中,這樣就會改到參考,也就自然會報錯囉~

展開與其餘參數

合併陣列 - 傳統寫法 v.s ES6寫法

ex: (傳統寫法)

1
2
3
4
let groupA = ['小明', '杰倫', '小紅'];
let groupB = ['老媽', '老爸'];
let groupAll = groupA.concat(groupB);
console.log(groupAll);

如此,就可以將groupA和groupB合併在groupAll中。

ex: (ES6 寫法)

1
2
3
4
let groupA = ['小明', '杰倫', '小紅'];
let groupB = ['老媽', '老爸'];
let groupAll = [...groupA, ...groupB];
console.log(groupAll);

如此,會得到一樣的結果。

ES6中的…到底在幹麻啦?

那ES6寫法中的...到底在做什麼呢?
你直接用console.log(…groupA);
你會發現它會直接返回groupA的各個元素值,由此可知,...的作用在於
將該陣列的元素值依序取出來,再return回去。
所以,我們在合併陣列的時候,對groupA和groupB使用...,其實就是將它們的元素都取出來,
再全部塞進去groupAll中囉。

淺複製觀念

...的操作,會涉及到淺複製的觀念。
若今天我們想要將某個陣列的值,複製到另一個陣列裡面。
ex:

1
2
3
4
let groupA = ['小明', '杰倫', '小紅'];
let groupB = groupA;
groupB.push('老爸');
console.log(groupA); // 小明 杰倫 小紅 老爸

會造成groupA新增一個元素的原因,是因為,在JS中傳遞陣列和傳遞物件一樣,
都是傳參考,所以,當你改變groupB時,連帶地也會更改到原本的groupA的內容囉。

ex:

1
2
3
4
let groupA = ['小明', '杰倫', '小紅'];
let groupB = [...groupA];
groupB.push('老爸');
console.log(groupA); // 小明 杰倫 小紅

以上我們透過...的操作,只有將groupA的元素值取出來,再塞到新陣列groupB中,
所以,groupB是一個全新的陣列,跟groupA是沒關係的,最終,groupB怎麼操作都是不會
干擾到groupA的喔,而這邊...的操作就是一個淺複製的觀念喔。

類陣列觀念 - NodeList

當我們使用querySelectorAll所回傳的NodeList就是一個類陣列。
ex:

1
2
3
4
5
6
7
8
9
10
11
---HTML---
<ul>
<li></li>
<li></li>
<li></li>
</ul>

---JavaScript---
let doms = document.querySelectorAll('li');
console.log(doms);
console.log(doms.concat); // Error

以上這個範例,你可以看到doms是一個類陣列,你打開它的__proto__屬性,
會看到它能使用的方法比一般的陣列還要少。
concat是陣列的功能,你叫類陣列去調用該方法的話,是會報錯的喔。

若我們想要把類陣列轉換成陣列的話,是可以藉用...的功能的
ex:

1
2
3
4
5
6
7
8
9
10
11
12
13
---HTML---
<ul>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>

---JavaScript
let doms = document.querySelectorAll('li');
console.log(doms); // NodeList
let newDoms = [...doms];
console.log(newDoms.concat()); // 不會報錯

以上範例藉用...將doms的元素值提出來,塞到新陣列newDoms中,
此時,再用newDoms取使用concat方法是沒問題的喔,因為,newDoms是一個陣列喔。

類陣列觀念 - arguments

當你傳入的參數的數量,多於函式原本定義的參數數量的時候,
這些多出來的參數,都會被存入到一個叫arguments的類陣列中。
所以,你直接對arguments使用專屬於陣列的某些方法的時候,是會報錯的喔。
所以,我們使用的手法一樣,利用...arguments內的元素存到另一個陣列中,
由這個新的陣列去操作陣列的方法,就ok了。
ex:

1
2
3
4
5
6
7
8
9
10
11
var orginCash = 1000;
function updateEasyCard(){
let arg = [...arguments];
let sum = arg.reduce(function(accumulator, currentValue){
return accumulator + currentValue;
}, 0);
console.log('我有' + sum + '元');
}
}
updateEasyCard(0); // 我有1000 元
updateEasyCard(10, 50, 100, 50, 5, 1, 1, 1, 500); // 我有 718 元

可以看到以上範例我們將arguments的內容透過...,複製到新的陣列arg中,
再由arg去做陣列的操作,就沒問題了。

其餘參數 - 將傳入參數存成一個陣列

很多時候,我們傳入函式的參數的數量是不一定的。
此時,我們就可以透過...的方式直接將所有傳入的參數,都存在同一個陣列中,
接著,我們就可以在該函式裡面透過那個陣列,來操作傳入的參數的內容囉。
注意~~~~這邊...是會直接把這些傳入的參數,匯集成一個陣列喔,並不是類陣列喔!!
ex:

1
2
3
4
function MoreMoney(...money){
console.log(money); // [100, 200, 300]
}
MoreMoney(100, 200, 300);

若同時,要傳入其他的參數也是ok的喔,彼此之間是不會被干擾的喔。
ex:

1
2
3
4
function MoreMoney(ming, ...money){
console.log(ming, ...money) // '小明', [100, 200, 300]
}
MoreMoney('小明', 100, 200, 300);

解構

解構賦值 - 對陣列操作的各種方法

傳統賦值的寫法 v.s 解構賦值的寫法

ex: (傳統寫法)

1
2
3
4
let family = ['小明','杰倫','小紅',老媽, 老爸];
let ming = family[0];
let jay = family[1];
let Hong = family[2];

以上的傳統寫法,就是將陣列中相對應的元素取出來之後,再分別賦值到獨立的變數中。

ex: (解構賦值寫法)

1
2
3
let family = ['小明','杰倫','小紅',老媽, 老爸];
let [ming, jay, Hong, mom, father] = family;
console.log(ming, jay, Hong, mom, father); // 小明 杰倫 小紅 老媽 老爸

解構賦值 - 賦值個數 少於 被賦值元素的數量

ex:

1
2
3
let family = ['小明','杰倫','小紅',老媽, 老爸];
let [ming, Jay, Hong] = family;
console.log(ming, Jay, Hong); // 小明,杰倫,小紅

也是沒問題的,它就依序賦值前面的值,不去理後面那些沒有出現的元素。

解構賦值 - 賦值個數中間落下一項

ex:

1
2
3
let family = ['小明','杰倫','小紅',老媽, 老爸];
let [ming, Jay, , mom, father] = family;
console.log(ming, Jay, mom, father); // 小明,杰倫,老媽,老爸

這邊你就可以看到,就是落下的那一項就跳過,接著,後面的變數就再依序賦值。
但要特別注意的是,你落下的那一項,還是要留逗號給它隔開喔,不然,編譯器會不知道是哪一個被落下了。

解構賦值 - 交換兩個變數的值

ex:

1
2
3
4
let Goku = '悟空';
let lufi = '路飛';
[Goku, lufi] = [lufi, Goku];
console.log(Goku, lufi); // 路飛, 悟空

而解構賦值,可以快速直接交換,就不用透過第三方來暫存其中一個人的值,來做互換。

解構賦值 - 取出字串中的每一字元

解構賦值可以將字串中的字元逐一取出來
ex:

1
2
3
let str = '基紐特攻隊';
let [a,b,c,d,e] = str;
console.log(a,b,c,d,e); // 基 紐 特 攻 隊

以上就可以依序將字串的字元依序存到對應的變數中。

解構賦值 - 對物件操作的各種方法

取出其中一個值,並附在新的變數名稱上

ex:

1
2
3
4
5
6
7
let family = {
ming: '小明',
mom: '老媽',
father: '老爸'
}
let {ming} = family;
console.log(ming); // 小明

先用let 創出一個物件,再將這個物件塞入一個屬性ming,
此時,family物件的第一個值會被取出來,並賦予到被新物件中的那個ming上面,
此時,該ming的值就是小明。
注意~~~這個新物件內的屬性名稱必須要跟被複製物件的其中一個屬性的名稱一樣喔,不然,它會undefined。
以上面的例子,我們想要複製family物件中的ming屬性的話,新物件的屬性也必須是ming,這用編譯器才知道你想要複製的是原始物件的哪一個成員屬性值。

接著,我們將提出來的屬性值變換它的名稱,並將它的值存到這個變換過名稱後的屬性中
ex:

1
2
3
4
5
6
7
let family = {
ming: '小明',
mom: '老媽',
father: '老爸'
}
let {ming:Goku} = family;
console.log(Goku); // 小明

以上範例,我們將新物件中,存取到family物件中第一個屬性值的ming,先將它的屬性名稱改為Goku,接著,它的值就換傳遞到Goku中囉。所以,這個Goku的值,就會是小明囉。

延伸問題

ex:

1
2
let {ming:Goku, family: [,mom]} = {ming:'小明', family:['小明', '老媽', '老爸']};
console.log(Goku, mom); // 小明, 老媽

以上這個範例,是將解構賦值的物件操作和陣列操作混在一起用喔。
在新的物件中,先用ming和family分別存取到原先物件中的ming屬性和family屬性的值,
接著,再將ming屬性改名成Goku並將其值賦予Goku,所以,此時,Goku的值為’小明’。
接著,新物件中的family屬性是一個陣列,那我們也用解構賦值的技巧,直接原先陣列的依序賦予到新物件中的family陣列中,所以,變數mom的值會是老媽。

預設值

陣列預設值

ex:

1
2
let [ming='小明', jay='杰倫'] = ['阿明'];
console.log(ming, jay); // 阿明 杰倫

原本 ming 預設值是小明,後面有在被重新賦予值,所以,它的值就會被更改為阿明。
而 jay 沒有被更改到,所以,就保持原本的預設值。