[JS] 看懂 React 開發中常用的 ES6+ 語法

筆者近期因專案需求而開始使用 React 作為前端開發框架,說真的剛接觸 ES6+ 語法時,面對完全無法腦補出的程式語意,猛然發現 JavaScript 世界真的已經變了 (其實是自己腳步慢了),因此本篇稍微整理了一些比較常用的功能,希望能幫助讀者對這些語法有一定的熟悉度,這樣在學習任何前端框架時才不會陌生感太重。

前言


雖然目前 ES6+ 語法在各個瀏覽器的支援程度不盡相同 ( IE 只有慘而已 ),但仍可透過 babel 轉換 ES6+ 語法到支援度較完整的 ES5 語法,因此不需擔心瀏覽器支援度的問題,放膽去學習及使用新語法新功能吧!以下將針對筆者常接觸到的功能進行說明,期望讀者都可以看得懂且瞭解其作用與目的,如需要更深入的探討,就請自行 google 延伸學習囉!

目前各瀏覽器對 ES6 語法支援度的比例

 

 

簡化屬性描述 (Shorthand Property Names)


當我們在定義屬性時會用 屬性名稱屬性值 語法來描述,當屬性值為變數且與屬性名稱相同時,以往在 ES5 中會使用 { age: age } 來描述,但現在透過 ES6 語法可直接使用 { age } 達到相同目的;另外在 function 的宣告方式也有簡化,可以使用 functionName () { ... } 來表示 functionName: funnction () { ... } 宣告方式,這樣是不是精簡多了呢。

function getStudent(chemistry, biology, mathematics) {

  return {   
    // [屬性名稱] 與 [屬性值變數名稱] 相同的時候
    chemistry: chemistry, // ES5 只能這樣做
    biology, // ES6 可以此表示 biology: biology
    math: mathematics , // 不同名稱當然就只能這樣囉

    // 以更精簡方式宣告 funcion 
    getScore() {
     return this.chemistry + this.biology + this.math
    }
  }

}

 

 

解構賦值 (Destructuring Assignment)


可以將物件直接拆解出我們所需要的屬性值,只要對應到「相同屬性名稱」就可以 assign 值到特定的變數中,方便開發人員直接獲得所關注的屬性資料。

  1. 需要解構屬性值至與屬性名稱相同的變數時,這樣簡化使用即可
  2. student 的 name 屬性值被 assign 到新定義的 name 變數中
  3. student 的 age 屬性值被 assign 到新定義的 hisAge 變數中 
  4. 可以直接使用 name 及 hisAge 進行接續行為 (省去使用 student.name ...)

 

測試代碼如下,可以玩玩看印象會更深刻喔!

// [範例1] 在 function 中只取出需要的傳入值
var student = { name: 'chris', age: 15 }
showStudentInfoES6(student)

function showStudentInfoES6 ({ name, age: hisAge }) {
  console.log('name: ', name)     // -> name: chris
  console.log('hisAge:', hisAge)  // -> hisAge: 15
}


// [範例2] 容易混淆的複雜資料情境
var qoo = { aaa: 111, bbb: 222, ccc: { ddd: 333, eee: 444 } }

var { aaa, bbb } = qoo
console.log(aaa) // -> 111
console.log(bbb) // -> 222

var { ccc: { eee } } = qoo
console.log(eee) // -> 444

var { ccc: xxx } = qoo
console.log(xxx) // -> {ddd:333, eee:444}

var { ccc } = qoo
console.log(ccc) // -> {ddd:333, eee:444}

 

 

模組 (Module)


以往需透過 CommonJS 及 AMD 對 JavaScript 模組化,如今在 ES6 中實現了 Module 的功能,開發人員可以透過 ES6 標準語法 exportimport 操作 Module ,以下將針對基本用法進行說明。

 

basic export & import

以下是基本的輸出及載入範例,雖然 ES6 允許在各個「變數」及「函式」宣告前直接加上 export 語法來進行輸出,但還是建議統一在檔案下方進行匯出,這樣可以清楚地呈現此模組所有匯出項目;在載入端只需要使用 import 將需要載入的項目置放在「中括號」內 (名稱要與輸出名稱相符),並在 from 後面設定來源檔案位置。

注意 import 並非使用「解構賦值」語法 (雖然乍看很像),所以那套是行不通的喔!
/* utility.js */
const timeout = 200

function addNumber(...nums){
  var result = 0
  nums.forEach(function (number) {
    result += number
  })
  return result
}

export {timeout, addNumber}  


/* other.js */
import { addNumber } from './utility.js'  

console.log(addNumber(1, 2)) // -> 3

 

default export

有時候一個 module 只做一件事情,例如在 React 每個組件的 js 檔就只會輸出 React.Component 物件,因此為方便起見可直接使用 export default 將該物件作為預設輸出項目 (僅允許一個 default 設定存在);在載入預設項目時,可已使用任意名稱表示該組件。

載入 default  變數或方法時,無需要加上「中括號」喔!
/* myComponent.js */
export default class MyComponent extends React.Component {
  // ...
}

/* header.js */
import MyOwnNameComponent from './myComponent'

const header = props => {
  return <div>
    <MyOwnNameComponent />
  </div>
}

export default header

 

import into a namespace

有時候為了使用方便,會利用 * 號將所有項目載入至特定 namespace 中,等到使用時再決定叫用那個「變數」或「函式」名稱;另外當載入項目「名稱過長」或「有相同名稱變數存在」時,我們也可以在 import 時將載入項目名稱取個別名。 

載入所有項目至 namespace 時,無需要加上「中括號」喔!
/* utility.js */
function doSomething1 () { return 'do1' }
function doSomething2 () { return 'do2' }

export { doSomething1, doSomething2 }


/* other.js */
import * as util from './utility'  // into a namespace
import { doSomething2 as do2 } from './utility'  // rename

console.log(util.doSomething1()) // -> 'do1'
console.log(util.doSomething2()) // -> 'do2'
console.log(do2()) // -> 'do2'

 

execute module only

執行 import 語句都會執行所要載入的模組,因此若只是想要執行特定模組時,就不會載入任何項目;以下範例表示在 app 建立時,載入(執行) setupToastre 模組去初始 toastr 的全域設定 。

/* setupToastr.js */
import toastr from 'toastr'

toastr.options.closeButton = true
toastr.options.timeOut = 3000
toastr.options.progressBar = true


/* app.js */
import './setupToastr'

 

 

其餘屬性 (Rest Properties)


顧名思義就是可以存放「剩餘屬性資料」的變數,當我們從 Object 中解構特定屬性值後,其餘屬性可以透過「三個點」語法傳入 Rest Properties 變數中存放。

// 測試物件
var person = { name: 'chris', age: 19, isMarried: true }

// 簡單的將剩餘屬性存放在 otherInfo 中
var { name, ...otherInfo } = person

console.log(name)  // -> chris
console.log(otherInfo) // -> { age: 19, isMarried: true}

 

 

展開屬性 (Spread Properties)


用來展開物件裏頭所有屬性結構資料,語法是以「三個點」作為 operator 動作;在 Redux 透過 Reducer 修改 state 屬性資料時,常會使用此語法特性複製 state 回傳新物件,程式範例如下。

// 測試物件
var person = { name: 'chris', age: 19, isMarried: true }
console.log( {...person} ) // -> { name: 'chris', age: 19, isMarried: true }
console.log( person === {...person} ) // -> false, 是新物件唷

// 展開 person 屬性並與新 age 屬性值合併輸出新物件
function changeAge (person, newAge) {
  return { ...person, age: newAge }
}

console.log(changeAge(person, 10)) 
// -> { name: "chris", age: 10, isMarried: true }

 

 

字串樣板 (String Template)


字串樣板的首要條件就是需要用「反引號」圍住,接著可以使用 ${variableName} 在字串樣板中崁入變數值,以此讓字串銜接變數的語意更加清晰。

var first = 'chris', last = 'chen'
var name = `your name is ${first} ${last}.`
console.log(name) // -> 'your name is chris chen.'

 

 

箭頭函數 (Arrow Functions)


使用更短的語法來定義函數表示式,首先只要習慣轉換用法即可,請參考以下代碼。

// 傳統的方法宣告方式
function square(value){
  return value*value
}

// 使用 arrow function 改寫
// 參數: 使用 () 包住
// 內容: 寫在 {} 裡面
var square1 = (value) => {return value*value} 

// 特定情境下使用 arrow function 可以更加精簡
// 參數: 只有在單個參數時才可以省略 ()
// 內容: 只有在單個 statement 且是回傳值時才可以省略 {} 及 return 文字
var square2 = value => value*value

// 若要直接回傳物件時,使用 () 包住物件即可
// 順便複習使用簡寫特性表示 {name:name, phoneNo:phoneNo}
var buildContact = (name, phoneNo) => ({name, phoneNo})

// 測試一下囉
console.log(square1(2)) // -> 4
console.log(square2(2)) // -> 4
console.log(buildContact('chris','0911123123'))  
// -> {name:"chris", phoneNo: "0911123123"}

 

它還有一個重要特性

箭頭函數體內的 this 會指向定義時所在的物件

在函式中「this」指的是目前的物件,而這個「this」真正的意義是經由「呼叫函數的方式」來指定,這點常常會造成開發人員的困擾,而 arrow function 對於 this 的定義方式,恰巧為此困擾提供了簡易的解決方案;以下將透過一個簡單的實例,理解「this」帶來的困擾及各種解決方案。

透過以下代碼可以發現 printThis 函數中的 this 值,會依據不同的呼叫方式而改變指向的物件。

function Person() {
  this.age = 0
  this.printThis = function() {
    console.log(this)
  }
}

var p = new Person()
p.printThis() // -> {age: 0, printThis: ƒ}

var printThis = p.printThis
printThis()  // -> Window {stop: ƒ, open: ƒ, alert: ƒ, ...}

 

常見處理方式會用變數來保存固定 this 指向的物件,如此就不會受到呼叫函數的方式而改變。

function Person() {
  const self = this
  self.age = 0
  self.printThis = function() {
    console.log(self)
  }
}

var p = new Person()
p.printThis() // -> {age: 0, printThis: ƒ}

var printThis = p.printThis
printThis()  // -> {age: 0, printThis: ƒ}

 

我們也可以調整 printThis 為箭頭函數,讓 printThis 中 this 依照 arrow function 特性指向定義時所在的 Person 物件;測試結果如下,確實可以達到相同效果。

function Person() {
  this.age = 0

  // 使用 arrow function 特性
  // 讓 this 為定義當下 scope 的 Person 物件
  this.printThis = () => {
    console.log(this)
  }  
}

var p = new Person()
p.printThis() // -> {age: 0, printThis: ƒ}

var printThis = p.printThis
printThis()  // -> {age: 0, printThis: ƒ}

 

當然也可以透過 Function.prototype.bind() 方法直接傳入綁定的物件。

function Person() {
  this.age = 0
  this.printThis = function () {
    console.log(this)
  }.bind(this) // 直接使用 bind() 手動綁定 function 中的 this 物件 
}

var p = new Person()
p.printThis() // -> {age: 0, printThis: ƒ}

var printThis = p.printThis
printThis()  // -> {age: 0, printThis: ƒ}

 

最後舉個 React 中實際會發生的情境,筆者希望在點選 LogoutBtn 時可以執行透過 props 從外部傳入的 handleLeaveClick 函數,而在函數中會呼叫「Header」組件內 setState 方法來變更組件狀態;但由於實際執行  handleLeaveClick 函數是在「LogoutBtn」組件內發生的,所以此時  handleLeaveClick 函數中的 this 是指向 LogoutBtn 組件,所以會變更到 LogoutBtn 組件中的狀態(如果有相同狀態),造成悲劇的開始。

class Header extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      showComfirmBox: false
    }
  }

  handleLeaveClick () {
    // 此叫用情境下 this 表示 LogoutBtn 時,就無法操作 Header 的 setState 方法啦
    this.setState(state => ({ ...this.state, showComfirmBox: true }))
  }

  render () {
    return <div>
      {/* 這樣寫會讓 handleLeaveClick 中 this 表示觸發事件的 LogoutBtn 物件喔 */}
      <LogoutBtn onClick={this.handleLeaveClick}>離開系統</LogoutBtn>
    </div>
  }
}

 

修正方式如下,三種方式都可以確保 handleLeaveClick 函數中 this 指向 「Header 」組件。

class Header extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      showComfirmBox: false
    }

    /* 方法一 */
    this.handleLeaveClick = this.handleLeaveClick.bind(this)
  }

  handleLeaveClick () {
    this.setState(state => ({ ...this.state, showComfirmBox: true }))
  }

  render () {
    return <div>

      {/* 方法一 */}
      <LogoutBtn onClick={this.handleLeaveClick()}>離開系統</LogoutBtn> 


      {/* 方法二 */}
      <LogoutBtn onClick={() => this.handleLeaveClick()}>離開系統</LogoutBtn>   
  

      {/* 方法三 */}
      <LogoutBtn onClick={this.handleLeaveClick.bind(this)}>離開系統</LogoutBtn>
    </div>
  }
}

 

 

後記


筆者目前接觸到的 ES6+ 語法及眉角實在是多到爆炸,當然無法每個項目都鉅細靡遺的研究,只能多看看多學習多使用,紀錄下每次看不懂的疑惑點,找時間好好地深入淺出搞搞,最後會發現這些新東西用久了好像也挺自然地。共勉之。

 

 

參考資訊


ECMAScript 6 入門

The Complete Rules to 'this'

一看就懂的 React ES5、ES6+ 常見用法對照表

 


希望此篇文章可以幫助到需要的人

若內容有誤或有其他建議請不吝留言給筆者喔 !