前言
本節內容包含下述子章節:
使用 let 與 const 宣告變數
在以往我們都是使用 var 來宣告變數,先來講 let 的部分,假設今天我們的程式碼如下:
console.log(mom);
var mom = '老媽';
(function () {
console.log(mom);
})();
這時候會依序輸出: undefined、老媽
但如果我們把 var 改成 let ,輸出就會錯誤,這是因為 var 宣告時,已經在記憶體佔了一個位置,只是我們還沒賦予值,
所以第一次在 console 的時候才會顯示 undefined 而當第二行我們賦予值的時候才被輸出出來,但是 let 宣告的話,
依序從上而下,還沒有發現宣告的變數賦予值,所以才會出現錯誤
第二種情況,是作用域的部分:
function varMing () {
var ming = '小明';
if (true) {
var ming = '杰哥';
}
console.log(ming);
}
varMing();
這時候在 console 輸出會出現杰哥,但是如果改成 let 宣告的話,輸出就會是小明,並不會被 if 的判斷式取代掉。
var 的作用域是 function scope ,let 的作用域是 block 。
簡單來說 let 的作用域是 {} 大括號之間的部分。
第三種情況是 for 迴圈,宣告變數 i 的時候:
for (var i = 0; i < 10; i++) {
console.log(i);
setTimeout(function () {
console.log('這執行第' + i + '次');
}, 10);
}
在這樣的情況下, console 會直接輸出,這執行第 10 次,並不是我們預期的,這執行第 1 次,這執行第 2 次,
這執行第 3 次….。這是因為 i 已經被宣告為全域變數了,他會在迴圈內累加之後再輸出,我們在 console 內輸入 window.i
就會發現有數字輸出,如果改成 let 宣告 i 變數,就會發現結果如我們所預期,這是因為 let 的作用域是 block 是在 {}
之間,且在 console 輸入 window.i 就會是 undefined。
接著我們來說明 const:
const ming = '鐵錚錚男子漢';
當我們宣告一個變數使用 const 的時候,就不能去改動它的值,這會出現錯誤,如果這時候我們把 ming 改成其他的內容:
ming = '男子漢';
就會發現在 console 會跳錯,但是如果是更改物件的屬性呢:
const family = {
mom: '老媽',
me: '小明',
sister: '小橙'
};
family.father = '爸爸';
會發現是可以的,因為物件的屬性是傳參考特性就不影響,如果把宣告的物件改成空物件,這時候就會出錯了。
補充說明:
var 宣告的變數,往往都會是全域變數,進而會影響到開發,舉個例子:
在 for 迴圈的時候,或者 if 判斷式的時候
// for 迴圈
for(var i=0; i<10; i++){
console.log(i);
}
console.log(i);
// if 判斷式
if(true){
var feedBack = '同意';
}
console.log(feedBack);
在上述兩種情況,會發現在 for 迴圈外和 if 判斷式外,用 console 查看在內部宣告的變數,仍然可以看到,
這是因為 var 宣告的變數為全域,為了讓變數可以在 {} 的作用域內,這時候就可以採用 let 來做宣告。
假設沒有 let 的情況下,該如何解決既有的這個問題?
可以採取立即函式!
(function(){
for(var i=0; i<10; i++){
console.log(i);
}
}());
console.log(i);
這時候 console 就會跳錯,這也是為了限制作用域的部分。
展開與其餘參數
用 … 將陣列中的值一個個取出來再 return 回去
let groupA = ['小明', '杰倫', '阿姨'];
let groupB = ['老媽', '老爸'];
let familyAll5 = groupA.concat(groupB) //ES5合併陣列
let familyAll6=[...groupA,...groupB] //ES6合併陣列
JavaScript 的物件或是陣列的儲存方式是記錄記憶體位置,因此當將 groupA 指給 groupB 對 groupB 進行賦值時,
是使用淺複製的方式將 groupA 所記錄的記憶體位置傳給 groupB,最終會導致當對 groupB 進行操作時,
groupA 也會後受到影響。
let groupA = ['小明', '杰倫', '阿姨'];
let groupB = groupA
groupB.push('阿明');
console.log(groupA); //["小明", "杰倫", "阿姨", "阿明"]
為了避免這種影響到互相影響發生,可以改採深複製的方式,借助上一節所提到的展開語法,將 groupA 值一一取出後,
放入一個新的陣列中,處理物件也是相同宣告方法。
let groupB = [...groupA]
在使用 Node.childNodes 或 document.querySelectorAll 時會回傳包含指定節點的子節點的集合,此集合稱為 Node List,
是種類似陣列的資料結構,但是不能使用陣列的部份方法,因此又被稱為類陣列。
若要將 Node List 轉成陣列,一樣是使用展開語法
let myArray = [...myNodeList]
就是不定長度參數的宣告,如果你傳入參數多於你宣告的個數,多餘的部份會變成名為 arguments 的類陣列物件
function updateEasyCard() {
let arg = [...arguments];
let sum = arg.reduce(function (accumulator, currentValue) {
return accumulator + currentValue;
}, 0);
console.log('我有 ' + sum + ' 元');
}
updateEasyCard(0); // 我有 0 元
updateEasyCard(10, 50, 100, 50, 5, 1, 1, 1, 500); // 我有 718 元
解構
上面認識 ES6 的展開與其餘,這邊內容將著重在介紹什麼是 ES6 的解構賦值(destructuring),
我們可以把解構賦值想像成是鏡子的概念,將右方資料往左邊送,然後依照「順序」或「屬性」對上特定值,
如同鏡射的概念,但不會左右顛倒。而實務上的幫助在於透過更簡短的程式碼來達成相同效果,除了使用便利外,
更能有效提升程式碼的閱讀性。
解構賦值常用於陣列和物件當中,兩者最大的差異為:
陣列的解構賦值強調的是「順序」,而物件的解構賦值強調的則是「屬性名稱」,屬性名稱必須相互對應才能夠取得到值。
過去想要將陣列內的元素取出,賦與其他變數值的時候,只能透過一個一個給值的作法,程式碼如下:
let team = ['基紐', '吉斯', '巴特'];
let Ginyu = team[0];
let Jeice = team[1];
let burter = team[2];
console.log(Ginyu, Jeice, burter); //基紐, 吉斯, 巴特
若是以 ES6 解構賦值時,程式碼如下:
let team = ['基紐', '吉斯', '巴特'];
let [Ginyu, Jeice, burter] = ['基紐', '吉斯', '巴特'];
console.log(Ginyu, Jeice, burter); //基紐, 吉斯, 巴特
透過陣列解構賦值強調的「順序」概念,將變數依序傳入,如此,變數則會直接完成賦值 Ginyu='基紐'、Jeice='吉斯'、
burter='巴特'。
當左方輸入的變數多於右方所給的值
顧名思義,多出來的那個變數會被賦予 undefined 的值,程式碼如下:
let team = ['基紐', '吉斯', '巴特'];
let [Ginyu, Jeice, burter, Peter] = ['基紐', '吉斯', '巴特'];
console.log(Ginyu, Jeice, burter, Peter); //基紐, 吉斯, 巴特, undefined
當左方輸入的變數小於右方所給的值
此時,只有被指定到的變數會有相對應的值,而少掉的變數則會直接空過來,程式碼如下:
let team = ['基紐', '吉斯', '巴特'];
let [Ginyu, , burter] = ['基紐', '吉斯', '巴特'];
console.log(Ginyu, burter); //基紐, , 巴特
陣列解構(展開語法)
我們會將 team 的值一一賦予到變數上,因此,Ginyu='基紐'、Jeice='吉斯',而此時剩餘的 team 值則會被賦予到 others
變數。
let team = ['基紐', '吉斯', '巴特', '彼得'];
let [Ginyu, Jeice, ...others] = team;
console.log(Ginyu, Jeice, others);
結果如下:
陣列解構(變數交換)
由於前面提到的解構賦值(鏡射)的概念,下面範例中直接將右邊的變數交換到左邊,可以看到是同時交換變數,
所以在互換變數值上是非常方便的。
let Ginyu = '基紐';
let Jeice = '吉斯';
[Ginyu, Jeice] = [Jeice, Ginyu];
console.log(Ginyu, Jeice); //吉斯, 基紐
陣列解構(字串拆解)
如同前面提到陣列解構的「順序」特性,若是遇到字串則會將字串拆解成一個一個字元,並依序賦予到左方的變數。
let str = '阿爾史卡特斯';
let [a, s, d, f, g] = str;
console.log(a, s, d, f, g); //阿, 爾, 史, 卡, 特, 斯
如同之前所提,陣列是使用「順序」作為索引值的對應。但物件則是其「屬性名稱」來做對應,因此在物件解構賦值中
沒有「順序」性的存在。
基礎用法:
let obj = {
player: '基紐',
jerseyNumber: 12
};
let { player, jerseyNumber } = obj;
console.log(player); // 基紐
console.log(jerseyNumber); // 12
物件解構賦值的簡寫看似簡單,如同上面這段程式碼中 let { player, jerseyNumber } = obj ,但實際上完整寫法如下:
let { player: player, jerseyNumber: jerseyNumber } = obj;
它會根據前面的屬性名稱來對應要給的值,但值其實是賦與給冒號 : 後面的變數,如:player 和 jerseyNumber 。
換個寫法,相信你會更清楚,此時 py === '基紐' ,而 jN === 12 。
let { player: py, jerseyNumber: jN } = { player: '基紐', jerseyNumber: 12 };
因此,相對的,當冒號前的屬性名稱對應不到物件中的屬性名稱時,則會出現 undefined 的情況,如程式碼:
let { player } = { py: '基紐' };
console.log(player); // undefined
在物件解構賦值中,冒號前是用來對應物件的屬性名稱,冒號後才是真正建立的變數名稱和被賦值的對象。
透過以下範例來加深印象:
- 重新賦予變數名稱
如以下 wang 在取得值後,將變數名稱改為 chen,因此 chen === '大王' ,而原先的 wang 變數就會消失,
因此回傳 not defined 的訊息。
let Team = {
wang: "大王",
jay: "周董",
chia: "恰恰"
};
let { wang: chen } = Team;
console.log(wang); // 錯誤, not defined
console.log(chen); // 大王
- 混合使用練習
讓我們混合解構賦值的「陣列」和「物件」實際來練習一次吧。
let {
player: chia,
team: [ , jay ]
} = {
player: "恰恰",
team: ["大王", "周董", "大師兄"]
};
console.log(chia, jay);
請問此時答案是什麼,說明如下:
player取得右方的"恰恰"後,並將變數名稱改為chiateam取得球隊陣列後,第一個值"大王"被放入一個空變數,接著自然會將第二個值套用在jay上囉!
- 預設值
為了避免值沒有賦予造成 undefined ,可以使用預設值避免此問題。
- 物件案例
team 屬性當中的變數 chia 會被預設值為 恰恰 ,如下方程式碼。
let { team: chia = "恰恰" } = {}
console.log(chia); //chia: "恰恰"
- 陣列案例
第一個變數 chia 會被重新賦值為 皮皮,第二個則會用預設值為 周董 。
let [ chia = "恰恰", jay = "周董" ] = ["皮皮"];
console.log(chia, jay); //皮皮 ,周董
縮寫
在過去相同名稱的物件如果要賦予在另一個屬性上,必須寫成 屬性: 物件,這個邏輯很直覺,不過在 ES6 中
如果物件名稱與屬性名稱相同時,則不需要寫兩次,可以改成寫一次即可。
let Frieza = '弗利沙'
const GinyuTeam = {
Ginyu: '基紐',
Jeice: '吉斯',
burter: '巴特',
// ...
}
// 原本寫法
const newTeam = {
GinyuTeam: GinyuTeam,
Frieza: Frieza
}
// 縮寫
const newTeam = {
GinyuTeam,
Frieza
}
這段也很常在 webpack、Node.js 引用外部套件時使用,如果有使用相關工具開發應該會常看到如以下:
import Vue from 'vue'
import App from './App'
import router from './router'
// 將套件由 './App' 路徑載入,並使用 App 這個變數名稱
new Vue({
el: '#app',
router,
template: '<App/>',
components: { App }
});
// 縮寫方式載入及使用
function 這個詞彙如果使用在物件內,也可以省略 :function,省略後的語意是沒有變化的。
//以往的寫法
const newTeam = {
showPosture: function () {
console.log('我們是 基紐特戰隊')
}
}
//縮寫的寫法
const newTeam = {
showPosture () {
console.log('我們是 基紐特戰隊')
}
}
若搭配解構使用的話,原先我們把新的變數 newTeam 指向 GinyuTeam 這個物件,再對 newTeam 變數新增一個 ming 屬性
並賦予值 小明 ,這時候原來的 GinyuTeam 物件也會跟著改動,因為物件是傳參考特性,如下:
const GinyuTeam = {
Ginyu: {
name: '基紐'
},
Jeice: {
name: '吉斯'
},
burter: {
name: '巴特'
},
}
const newTeam = GinyuTeam
newTeam.ming = '小明';
我們可以跟解構搭配使用,不去更動原本的 GinyuTeam 物件,並新增一個 ming 屬性,寫法如下:
const newTeam = {
...GinyuTeam
}
newTeam.ming = '小明';
console.log(newTeam, GinyuTeam);
這時候如果用 console 來比較兩者,就會發現其中的差異性了
箭頭函式與傳統函式
一般傳統函式的寫法:
var callSomeone = function (someone) {
return someone + '吃飯了'
}
改成箭頭函式的寫法:
var callSomeone = (someone) => {
return someone + '吃飯了'
}
在 return 只有單行的時候,我們可以縮寫的更精簡:
var callSomeone = (someone) => someone + '吃飯了';
在參數的部分我們甚至可以省略 ( ) ,變成:
var callSomeone = someone => someone + '吃飯了';
如果沒有參數代入,( ) 是不可以省略的:
var callSomeone = () => someone + '吃飯了';
另外箭頭函式是沒有 arguments 變數的,如果是一般傳統函式,呼叫函式時參數帶入一個不用參數的 function 時,
這時候還有 arguments 變數可以來存放:
const updateEasyCard = function () {
let cash = 0;
console.log(arguments);
}
updateEasyCard(10, 50, 100, 50, 5, 1, 1, 1, 500);
但在箭頭函式,並沒有 arguments 這個變數,如果我們把它改成箭頭函式:
const updateEasyCard = () => {
let cash = 0;
console.log(arguments);
}
updateEasyCard(10, 50, 100, 50, 5, 1, 1, 1, 500);
這時候是會跳錯的,但是我們使用前面講的其餘的方式來存放這些呼叫函式的參數:
const updateEasyCard = (...arg) => {
let cash = 0;
console.log(arg);
}
updateEasyCard(10, 50, 100, 50, 5, 1, 1, 1, 500);
這時候就會正常顯示了
然而箭頭函式跟一般傳統函式最大的差異性是 this 的使用上,會指向不一樣的地方,先用傳統函式來撰寫,
會發現 this 指向的都是 auntie 這個物件,如我們預期:
var name = '全域阿婆'
var auntie = {
name: '漂亮阿姨',
callName: function () {
console.log('1', this.name); // 1 漂亮阿姨
setTimeout(function () {
console.log('2', this.name); // 2 全域阿婆
console.log('3', this); // 3 window 物件
}, 10);
},
}
auntie.callName();
補充一下, setTimeout 用的其實是在 window 物件下,所以它的 this 會指向全域的部分,
如果改成箭頭函式:
var name = '全域阿婆'
var auntie = {
name: '漂亮阿姨',
callName: () => {
console.log('1', this.name); // 1 全域阿婆
setTimeout(() => {
console.log('2', this.name); // 2 全域阿婆
console.log('3', this); // 3 window 物件
}, 10);
},
}
auntie.callName();
會發現它的 this 都是指向 window ,為了避免這樣的狀況,我們在使用 Vue 時的 methods 盡量採用傳統函式,
不然 this 會指向其他物件。那箭頭函式到底用在哪呢?
其實我們可以如下使用:
var auntie = {
name: '漂亮阿姨',
callName () {
var vm = this;
setTimeout(() => {
console.log(vm);
}, 10);
}
}
auntie.callName();
主要是在 callName 這個 function 內先宣告一個 vm 變數指向這個物件,這樣 setTimeout 就會指向這個物件了
字串模板 Template String
在以往我們在字串的組合上會比較冗長,如下:
const people = [
{
name: '小明',
friends: 2
},
{
name: '阿姨',
friends: 999
},
{
name: '杰倫',
friends: 0
}
];
let originString = '我叫做 ' + people[0].name;
let originUl = '<ul>\
<li>我叫做 ' + people[0].name + '</li>\
<li>我叫做 ' + people[1].name + '</li>\
<li>我叫做 ' + people[2].name + '</li>\
</ul>';
在 ES6 的字串組合上方便許多,像是上面的 originString 把它改寫一下:
let newString = `我叫做 ${ people[0].name }`
輸出結果會是一樣的,主要是 、 反引號,以及 $ 搭配 {} 的使用方式,{} 內放置變數即可
在搭配標籤的組合上也可以改寫成:
let newUl = `
<ul>
<li>我叫做 ${people[0].name} </li>
<li>我叫做 ${people[1].name} </li>
<li>我叫做 ${people[2].name} </li>
</ul>
`
而 {} 內不只可以放置變數,也可以放置 JS 語法,我們在把上面的標籤組合改寫一下:
newUl = `
<ul>
${people.map((person) => { return `<li>我叫做 ${person.name}</li>` })}
</ul>
`
我們在 ul 底下寫了一個 .map() 的方法,用迴圈的方式 return 每個 li
結果像這樣:
這裡補充一下 .map() 這個方法以及 .forEach() 有什麼差別,首先我們先宣告一個陣列:
let fruits = ['apple', 'banana', 'strawberry'];
接著我們用 各別使用 .map() 和 .forEach() :
fruits.map(function(item){
console.log('map', item);
});
// map apple
// map banana
// map strawberry
// [undefined, undefined, undefined]
fruits.forEach(function(item){
console.log('for', item);
});
// for apple
// for banana
// for strawberry
// undefined
一樣都可以用迴圈的型式讀取到每筆資料,但最後的 undefined 的資料狀態不一樣,.map() 是陣列型式
那兩者的區別在哪呢?
.map() 的使用上可以類似加工的概念再 return 回去,而 .forEach() 卻不能,我們在針對 .map() 的使用進一步的寫下去:
var newFruits = fruits.map(function(item){
return 'new ' + item;
});
console.log(newFruits);
// ['new apple', 'nwe banana', 'new strawberry']
就會發現它可以新宣告的變數會變成陣列,並把加工 return 回來的值塞進去
如果用 .forEach() 是無法做到這樣的,
如果要在 .forEach() 做到一樣的效果,要再新宣告一個變數為空陣列:
let copy = [];
fruits.forEach(function(item){
copy.push('new ' + item);
});
console.log(copy);
// ['new apple', 'nwe banana', 'new strawberry']
上面就是 .map() 和 .forEach() 的差別