筆者近期因專案需求而開始使用 React 作為前端開發框架,說真的剛接觸 ES6+ 語法時,面對完全無法腦補出的程式語意,猛然發現 JavaScript 世界真的已經變了 (其實是自己腳步慢了),因此本篇稍微整理了一些比較常用的功能,希望能幫助讀者對這些語法有一定的熟悉度,這樣在學習任何前端框架時才不會陌生感太重。
前言
雖然目前 ES6+ 語法在各個瀏覽器的支援程度不盡相同 ( IE 只有慘而已 ),但仍可透過 babel 轉換 ES6+ 語法到支援度較完整的 ES5 語法,因此不需擔心瀏覽器支援度的問題,放膽去學習及使用新語法新功能吧!以下將針對筆者常接觸到的功能進行說明,期望讀者都可以看得懂且瞭解其作用與目的,如需要更深入的探討,就請自行 google 延伸學習囉!
簡化屬性描述 (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 值到特定的變數中,方便開發人員直接獲得所關注的屬性資料。
- 需要解構屬性值至與屬性名稱相同的變數時,這樣簡化使用即可
- student 的 name 屬性值被 assign 到新定義的 name 變數中
- student 的 age 屬性值被 assign 到新定義的 hisAge 變數中
- 可以直接使用 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 標準語法 export 及 import 操作 Module ,以下將針對基本用法進行說明。
basic export & import
以下是基本的輸出及載入範例,雖然 ES6 允許在各個「變數」及「函式」宣告前直接加上 export
語法來進行輸出,但還是建議統一在檔案下方進行匯出,這樣可以清楚地呈現此模組所有匯出項目;在載入端只需要使用 import
將需要載入的項目置放在「中括號」內 (名稱要與輸出名稱相符),並在 from 後面設定來源檔案位置。
/* 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 設定存在);在載入預設項目時,可已使用任意名稱表示該組件。
/* 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 時將載入項目名稱取個別名。
/* 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」真正的意義是經由「呼叫函數的方式」來指定,這點常常會造成開發人員的困擾,而 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
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+ 語法及眉角實在是多到爆炸,當然無法每個項目都鉅細靡遺的研究,只能多看看多學習多使用,紀錄下每次看不懂的疑惑點,找時間好好地深入淺出搞搞,最後會發現這些新東西用久了好像也挺自然地。共勉之。
參考資訊
希望此篇文章可以幫助到需要的人
若內容有誤或有其他建議請不吝留言給筆者喔 !