0%

【Vue.js 學習筆記】07. JavaScript ES6

前言

本節內容包含下述子章節:

  1. 使用 let 與 const 宣告變數
  2. 展開與其餘參數
  3. 解構
  4. 縮寫
  5. 箭頭函式與傳統函式
  6. 字串模板 Template String
  7. 常用陣列方法
  8. 測驗 2: ES6 小測驗


使用 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合併陣列

  • 淺複製 (Shallow Copy) V.S. 深複製 (Deep Copy)

JavaScript 的物件或是陣列的儲存方式是記錄記憶體位置,因此當將 groupA 指給 groupBgroupB 進行賦值時,

是使用淺複製的方式將 groupA 所記錄的記憶體位置傳給 groupB,最終會導致當對 groupB 進行操作時,

groupA 也會後受到影響。

let groupA = ['小明', '杰倫', '阿姨'];
let groupB = groupA
groupB.push('阿明');
console.log(groupA);  //["小明", "杰倫", "阿姨", "阿明"]

為了避免這種影響到互相影響發生,可以改採深複製的方式,借助上一節所提到的展開語法,將 groupA 值一一取出後,

放入一個新的陣列中,處理物件也是相同宣告方法。

let groupB = [...groupA]

  • 類陣列觀念說明

在使用 Node.childNodesdocument.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)

我們可以把解構賦值想像成是鏡子的概念,將右方資料往左邊送,然後依照「順序」或「屬性」對上特定值,

如同鏡射的概念,但不會左右顛倒。而實務上的幫助在於透過更簡短的程式碼來達成相同效果,除了使用便利外,

更能有效提升程式碼的閱讀性。


解構賦值常用於陣列和物件當中,兩者最大的差異為:

陣列的解構賦值強調的是「順序」,而物件的解構賦值強調的則是「屬性名稱」,屬性名稱必須相互對應才能夠取得到值。

  • 陣列解構賦值(array 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); //阿, 爾, 史, 卡, 特, 斯


  • 物件解構賦值(object destructuring)

如同之前所提,陣列是使用「順序」作為索引值的對應。但物件則是其「屬性名稱」來做對應,因此在物件解構賦值中

沒有「順序」性的存在。


基礎用法:

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;

它會根據前面的屬性名稱來對應要給的值,但值其實是賦與給冒號 後面的變數,如:playerjerseyNumber

換個寫法,相信你會更清楚,此時 py === '基紐' ,而 jN === 12

let { player: py, jerseyNumber: jN } = { player: '基紐', jerseyNumber: 12 };

因此,相對的,當冒號前的屬性名稱對應不到物件中的屬性名稱時,則會出現 undefined 的情況,如程式碼:

let { player } = { py: '基紐' };
console.log(player); // undefined

在物件解構賦值中,冒號前是用來對應物件的屬性名稱,冒號後才是真正建立的變數名稱和被賦值的對象。



透過以下範例來加深印象:

  1. 重新賦予變數名稱

如以下 wang 在取得值後,將變數名稱改為 chen,因此 chen === '大王' ,而原先的 wang 變數就會消失,

因此回傳 not defined 的訊息。

let Team = {
  wang: "大王",
  jay: "周董",
  chia: "恰恰"
};
let { wang: chen } = Team;
console.log(wang); // 錯誤, not defined
console.log(chen); // 大王

  1. 混合使用練習

讓我們混合解構賦值的「陣列」和「物件」實際來練習一次吧。

let {
  player: chia,
  team: [ , jay ]
} = {
  player: "恰恰",
  team: ["大王", "周董", "大師兄"]
};
console.log(chia, jay);

請問此時答案是什麼,說明如下:

  • player 取得右方的 "恰恰" 後,並將變數名稱改為 chia

  • team 取得球隊陣列後,第一個值 "大王" 被放入一個空變數,接著自然會將第二個值套用在 jay 上囉!


  1. 預設值

為了避免值沒有賦予造成 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
}

這段也很常在 webpackNode.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() 的差別