Bài viết được dịch từ: webapplog.com

Bài viết này sẽ giới thiệu nhanh cho bạn về ES6. Nếu bạn chưa biết ES6 là gì, thì nó là một phiên bản mới của Javascript. Nếu bạn là một kỹ sư phần mềm bận rộn (có ai lại không nhỉ?), hãy dành thời gian để học 10 tính năng tốt nhất thế hệ tiếp theo của ngôn ngữ lập trình phổ biến nhất - JavaScript.

Đây là danh sách 10 tính năng tốt nhất của ES6 cho những kỹ sư phần mềm bận rộn (sắp xếp ngẫu nhiên):

  1. Default Parameters in ES6
  2. Template Literals in ES6
  3. Multi-line String in ES6
  4. Destructuring Assignment in ES6
  5. Enhanced Object Literals in ES6
  6. Arrow Function in ES6
  7. Promises in ES6
  8. Block-Scoped Constructs Let and Cont
  9. Classes in ES6
  10. Modules in ES6

Chú ý: Danh sách này mang tính chủ quan. Nó không nhằm mục đích phủ nhận sự hữu dụng của các tính năng khác trong ES6, cái không xuất hiện trong danh sách lý do đơn giản là tôi giới hạn trong 10 tính năng.

Đầu tiên, là một chút về lịch sử của JavaScript dành cho những ai chưa biết:

  1. 1995: JavaScript được ra đời với tên gọi LiveScript
  2. 1997: Chuẩn ECMAScript được thành lập
  3. 1999: ES3 ra đời và IE5 là tất cả cơn thịnh nộ
  4. 2000 - 2005: XMLHttpRequest cũng được biết đến như AJAX, làm tăng sự phổ biến của các ứng dụng như Outlook Web Access (2000) và Oddpost (2002), Gmail (2004), và Google Maps (2005).
  5. 2009: ES5 ra đời (đây là cái được sử dụng phổ biến nhất hiện nay) với forEach, Object.keys, Object.create (Douglas Crockford), và chuẩn JSON.
  6. 2015: ES6/ECMAScript2015 ra đời, nó có hầu hết syntatic sugar.

Vậy là đủ về lịch sử, tiếp theo hãy xem xét code

1. Default Parameters in ES6

 Chúng ta có các câu lệnh để định nghĩa các tham số mặc định:

var link = function (height, color, url) {
    var height = height || 50
    var color = color || 'red'
    var url = url || 'http://azat.co'
    ...
}

Mọi thứ vẫn ổn cho đến khi giá trị là 0 và bởi vì trong JavaScript 0 là falsy nên nó sẽ mặc định là giá trị mà chúng ta hard-coded (50, 'red', 'http://azat.co'), vì thế chúng ta cần sử dụng cách khác. Trong ES6, chúng ta có thể đặt các giá trị mặc định khi khai báo tham số như thế này:

var link = function(height = 50, color = 'red', url = 'http://azat.co') {
  ...
}

Cú pháp này tương tự Ruby!

2. Template Literals in ES6

Template Literals hay interpolation trong các ngôn ngữ khác là một cách hiển thị các biến trong chuỗi. Trong ES5 chúng ta làm như thế này:

var name = 'Your name is ' + first + ' ' + last + '.'
var url = 'http://localhost:3000/api/messages/' + id

May mắn, trong ES6 chúng ta có thể sử dụng cú pháp mới ${NAME}  bên trong chuỗi:

var name = `Your name is ${first} ${last}.`
var url = `http://localhost:3000/api/messages/${id}`

3. Multi-line String in ES6

Một syntactic sugar khác là chuỗi có nhiều dòng (multi-line string). Trong ES5, chúng ta làm như thế này:

var roadPoem = 'Then took the other, as just as fair,\n\t'
    + 'And having perhaps the better claim\n\t'
    + 'Because it was grassy and wanted wear,\n\t'
    + 'Though as for that the passing there\n\t'
    + 'Had worn them really about the same,\n\t'

var fourAgreements = 'You have the right to be you.\n\
    You can only be you when you do your best.'

Trong ES6, chỉ cần sử dụng dấu backticks:

var roadPoem = `Then took the other, as just as fair,
    And having perhaps the better claim
    Because it was grassy and wanted wear,
    Though as for that the passing there
    Had worn them really about the same,`

var fourAgreements = `You have the right to be you.
    You can only be you when you do your best.`

4. Destructuring Assignment in ES6

Destructuring có thể là một khái niệm khó hiểu, hãy xem phép gán đơn giản này nơi các khóa house và mouse là các biến house và mouse:

var data = $('body').data(), // data has properties house and mouse
  house = data.house,
  mouse = data.mouse

Một ví dụ khác của phép gán destructuring (từ Node.js):

var jsonMiddleware = require('body-parser').json

var body = req.body, // body has username and password
  username = body.username,
  password = body.password  

Trong ES6, chúng ta có thể thay thế code ES5 với các câu lệnh:

var {house, mouse} = $('body').data() // we'll get house and mouse variables

var {jsonMiddleware} = require('body-parser')

var {username, password} = req.body

Điều này cũng làm việc với mảng:

var [col1, col2]  = $('.column'),
  [line1, line2, line3, , line5] = file.split('\n')

Có lẽ phải mất một khoảng thời gian để sử dụng cú pháp gán destructuring, nhưng nó rất hữu ích.

5. Enhanced Object Literals in ES6

Cái bạn có thể làm với object literals giờ đang thay đổi! Chúng ta đi từ phiên bản của JSON trong ES5 tới một thứ gần giống class trong ES6.

Đây là một đối tượng điển hình trong ES5 với một vài phương thức và thuộc tính:

var serviceBase = {port: 3000, url: 'azat.co'},
    getAccounts = function(){return [1,2,3]}

var accountServiceES5 = {
  port: serviceBase.port,
  url: serviceBase.url,
  getAccounts: getAccounts,
  toString: function() {
    return JSON.stringify(this.valueOf())
  },
  getUrl: function() {return "http://" + this.url + ':' + this.port},
  valueOf_1_2_3: getAccounts()
}

Nếu muốn đẹp, chúng ta có thể kế thừa từ đối tượng seviceBase bằng cách tạo protoype với phương thức Object.create:

var accountServiceES5ObjectCreate = Object.create(serviceBase)
var accountServiceES5ObjectCreate = {
  getAccounts: getAccounts,
  toString: function() {
    return JSON.stringify(this.valueOf())
  },
  getUrl: function() {return "http://" + this.url + ':' + this.port},
  valueOf_1_2_3: getAccounts()
}

Tôi biết, accountServiceES5ObjectCreateaccountServiceES5 không hoàn toàn giống nhau, bởi vì một đối tượng (accountServiceES5) sẽ có các thuộc tính trong đối tượng __proto__ như dưới đây:

Nhưng vì lợi ích của ví dụ, chúng ta sẽ coi chúng tương tự. Trong ES6 object literal, phép gán getAccounts: getAccounts, trở thành chỉ getAccounts. Chúng ta thiết lập protoype ngay trong thuộc tính __proto__:

var serviceBase = {port: 3000, url: 'azat.co'},
    getAccounts = function(){return [1,2,3]}
var accountService = {
    __proto__: serviceBase,
    getAccounts,

Vì thế chúng ta có thể gọi super và các khóa động (valueOf_1_2_3): 

    toString() {
     return JSON.stringify((super.valueOf()))
    },
    getUrl() {return "http://" + this.url + ':' + this.port},
    [ 'valueOf_' + getAccounts().join('_') ]: getAccounts()
};
console.log(accountService)

Đây là sự cải tiến tuyệt vời cho object literals cũ!

6. Arrow Functions in ES6

Đây là tính năng mà tôi mong chờ nhất. Tôi thích CoffeeScript vì fat arrows. Hiện nay chúng có trong ES6. Fat arrow tuyệt vời bởi vì chúng tạo ra hành vi của thuộc tính this. Ví dụ this sẽ có cùng giá trị với bối cảnh (context) của hàm - nó không bị thay đổi. Thay đổi thường xảy ra mỗi lần bạn tạo một closure.

Sử dụng arrow function trong ES6 cho phép chúng ta không cần sử dụng that = this hoặc self = this hoặc _this = this hoặc .bind(this). Ví dụ đây là một đoạn code trong ES5:

var _this = this
$('.btn').click(function(event){
  _this.sendData()
})

Đây là code trong ES6 không có _this = this:

$('.btn').click((event) =>{
  this.sendData()
})

Đáng buồn, ES6 quyết định rằng có skinny arrow là quá nhiều thứ tốt cho chúng ta và họ cho chúng ta một hàm (function) cũ rườm rà thay thế (Skinny arrow in CoffeeScript làm việc giống như các hàm (function) thông thường trong ES5 và ES6).

Đây là một ví dụ khác về cách chúng ta sử dụng call  để truyền bối cảnh (context) tới hàm logUpperCase() trong ES5:

var logUpperCase = function() {
  var _this = this

  this.string = this.string.toUpperCase()
  return function () {
    return console.log(_this.string)
  }
}

logUpperCase.call({ string: 'es6 rocks' })()

Trong ES6, chúng ta không cần sử dụng _this

var logUpperCase = function() {
  this.string = this.string.toUpperCase()
  return () => console.log(this.string)
}

logUpperCase.call({ string: 'es6 rocks' })()

Chú ý rằng bạn có thể trộn và kết hợp hàm cũ với => trong ES6 như trên. Và khi một arrow function được sử dụng với một dòng lệnh, nó trở thành một biểu thức, ví dụ nó sẽ ám chỉ rằng return (return () => console.log(_this.string)) kết quả của một câu lệnh. Nếu bạn có nhiều hơn một dòng, bạn sẽ cần sử dụng return một cách rõ ràng.

Đây là đoạn code tạo một mảng messages từ mảng ids:

var ids = ['5632953c4e345e145fdf2df8','563295464e345e145fdf2df9']
var messages = ids.map(function (value) {
  return "ID is " + value // explicit return
});

Trong ES6 sẽ trở thành:

var ids = ['5632953c4e345e145fdf2df8','563295464e345e145fdf2df9']
var messages = ids.map(value => `ID is ${value}`) // implicit return

Bạn có thấy rằng tôi sử dụng string template? Một tính năng khác từ CoffeeScript... tôi thích chúng!

Cặp ngoặc đơn () không bắt buộc cho một tham số trong arrow function. Bạn cần chúng khi sử dụng nhiều hơn một tham số.

Trong ES5 hàm có một câu lệnh return rõ ràng:

var ids = ['5632953c4e345e145fdf2df8', '563295464e345e145fdf2df9'];
var messages = ids.map(function (value, index, list) {
  return 'ID of ' + index + ' element is ' + value + ' ' // explicit return
});

Còn đây là phiên bản ES6 với cặp ngoặc đơn bao quanh các tham số và câu lệnh return không rõ ràng:

var ids = ['5632953c4e345e145fdf2df8','563295464e345e145fdf2df9']
var messages = ids.map((value, index, list) => `ID of ${index} element is ${value} `) // implicit return

7. Promises trong ES6

Promises là một chủ đề gây tranh cãi. Có nhiều phiên bản promise với một chút khác biệt về cú pháp như: Q, Bluebird, deferred.js, avow, jquery deferred là một vài ví dụ. Một số người nói không cần promise và chỉ cần sử dụng async, generators, callbacks, ...May mắn là hiện tại đã có sẵn một chuẩn Promise trong ES6.

Hãy xem xét một ví dụ bất đồng bộ với setTimeout():

setTimeout(function(){
  console.log('Yay!')
}, 1000)

Chúng ta có thể viết lại code trong ES6 với Promise:

var wait1000 =  new Promise(function(resolve, reject) {
  setTimeout(resolve, 1000)
}).then(function() {
  console.log('Yay!')
})

Hoặc với arrow function:

var wait1000 =  new Promise((resolve, reject)=> {
  setTimeout(resolve, 1000)
}).then(()=> {
  console.log('Yay!')
})

Bạn có thể thấy số dòng code tăng từ 3 lên 5 mà không rõ lợi ích của Promise. Đúng vậy. Lợi ích sẽ rõ ràng nếu có nhiều khối lệnh logic lồng trong hàm setTimeout():

setTimeout(function(){
  console.log('Yay!')
  setTimeout(function(){
    console.log('Wheeyee!')
  }, 1000)
}, 1000)

Đoạn code trên có thể viết lại trong ES6 với promise:

var wait1000 =  ()=> new Promise((resolve, reject)=> {setTimeout(resolve, 1000)})

wait1000()
    .then(function() {
        console.log('Yay!')
        return wait1000()
    })
    .then(function() {
        console.log('Wheeyee!')
    });

Bạn vẫn chưa bị thuyết phục rằng Promises tốt hơn các hàm callback thông thường? Tôi cũng vậy. Tôi nghĩ rằng một khi bạn có ý tưởng sử dụng callback và suy nghĩ cẩn thận về chúng thì không cần thêm sự phức tạp của promise.

Tuy nhiên, ES6 có Promises là dành cho những ai yêu thích chúng. Promise có tính năng hữu ích là catch lỗi cho tất cả các hàm callback. Bạn có thể đọc thêm bài viết này để biết nhiều hơn về promise: Giới thiệu Promise trong ES6.

Techmaster cũng có một loạt bài giới thiệu về Promise, qua các ví dụ thực tế. Bạn có thể đọc thêm ở đây: Lập trình Promise với BlueBird qua ví dụ. 

8. Block-Scoped Constructs Let and Const

Let cho phép chúng ta khai báo biến trong phạm vi các khối lệnh. Các khối lệnh được định nghĩa bởi cặp ngoặc nhọn {}. Trong ES5, các khối không giới hạn phạm vi của các biến:

function calculateTotalAmount (vip) {
  var amount = 0
  if (vip) {
    var amount = 1
  }
  { // more crazy blocks!
    var amount = 100
    {
      var amount = 1000
      }
  }  
  return amount
}

Kết quả của đoạn code trên là 1000. Wow! Đó thực sự là bug. Trong ES6, chúng sử dụng let để giới hạn phạm của biến trong các khối lệnh.

function calculateTotalAmount (vip) {
  var amount = 0 // probably should also be let, but you can mix var and let
  if (vip) {
    let amount = 1 // first amount is still 0
  } 
  { // more crazy blocks!
    let amount = 100 // first amount is still 0
    {
      let amount = 1000 // first amount is still 0
      }
  }  
  return amount
}

console.log(calculateTotalAmount(true))

Kết quả của đoạn code trên là 0, bởi vì khối lệnh if sử dụng let. Nếu nó không có từ khóa let (amount = 1) kết quả sẽ là 1.

Khi ES6 giới thiệu từ khóa const, mọi thứ trở lên dễ dàng hơn, nó chỉ là một biến không thay đổi, và cũng giới hạn phạm vi trong các khối lệnh giống như let. Ví dụ, đoạn code bên dưới gồm nhiều hằng số trong các khối lệnh khác nhau:

function calculateTotalAmount (vip) {
  const amount = 0  
  if (vip) {
    const amount = 1 
  } 
  { // more crazy blocks!
    const amount = 100 
    {
      const amount = 1000
      }
  }  
  return amount
}

console.log(calculateTotalAmount(true))

Theo ý kiến của cá nhân tôi, letconst làm cho ngôn ngữ trở lên phức tạp hơn. Không có chúng, chúng ta chỉ có một hành vi, giờ chúng ta phải xem xét nhiều kịch bản hơn.

9. Classes trong ES6

Nếu bạn yêu lập trình hướng đối tượng (OOP), bạn sẽ thích tính năng này. Nó làm cho việc viết class và kế thừa từ chúng dễ dàng như viết một comment trên Facebook.

Tạo và sử dụng class trong ES5 khá đau khổ, bởi vì không có từ khóa class. Ngoài ra, nhiều mẫu (pattern) kế thừa giống như pseudo classical, classical, functional chỉ làm tăng sự nhầm lẫn, giống như thêm dầu vào lửa trong cuộc chiến giữa các quan điểm lập trình JavaScript khác nhau.

Tôi sẽ không cho bạn thấy cách viết một class trong ES5, bởi vì có quá nhiều thứ. Hãy xem một ví dụ về class trong ES6 ngay. Tôi có thể nói với bạn rằng class của ES6 sử dụng prototype, không phải hướng tiếp cận funtion factory. Chúng ta có một class baseModel cái định nghĩa một hàm khởi tạo (constructor) và một phương thức getName():

class baseModel {
    constructor(options = {}, data = []) { // class constructor
        this.name = 'Base'
        this.url = 'http://azat.co/api'
        this.data = data
        this.options = options
    }

    getName() { // class method
        console.log(`Class name: ${this.name}`)
    }
}

Chú ý tôi sử dụng các giá trị tham số mặc định cho optionsdata. Vì thế tên phương thức không cần có từ khóa function hoặc dấu (:). Một điểm khác biệt lớn nữa là bạn không thể gán thuộc tính this.name theo cách giống với các phương thức. Để thiết lập giá trị của một thuộc tính, đơn giản gán một giá trị trong hàm khởi tạo (constructor).

AccountModel kế thừa từ baseModel với class NAME extends PARENT_NAME:

class AccountModel extends baseModel {
    constructor(options, data) {

Để gọi hàm khởi tạo cha, sử dụng super() với các tham số:

        super({private: true}, ['32113123123', '524214691']) //call the parent method with super
        this.name = 'Account Model'
        this.url +='/accounts/'
    }

Nếu bạn muốn đẹp, bạn có thể thiết lập getter giống như thế này và accountsData sẽ có một thuộc tính:

    get accountsData() { //calculated attribute getter
    // ... make XHR
        return this.data
    }
}

Vậy làm thế nào để sử dụng câu thần chú này? Nó dễ như lừa một đứa trẻ 3 tuổi nghĩ rằng ông già Noel là có thật:

let accounts = new AccountModel(5)
accounts.getName()
console.log('Data is %s', accounts.accountsData)

Kết quả sẽ là:

Class name: Account Model
Data is %s 32113123123,524214691

10. Modules in ES6

Trước ES6 JavaScript không hỗ trợ module. Mọi người thường sử dụng AMD, RequireJS, CommonJS và nhiều cái khác. Bây giờ trong ES6 bạn có thể sử dụng các import export.

Trong ES5 bạn sẽ sử dụng các thẻ <script> với IIFE, hoặc một thư viện giống như AMD, trong ES6 bạn có thể sử dụng class của bạn với export. Tôi là một lập trình viên Node.js, vì thế tôi sử dụng CommonJS cũng là cú pháp của Node.js. Khá đơn giản để sử dụng CommonJS trên trình duyệt với Browserify. Hãy xem ví dụ chúng ta có một biến port và phương thức getAccounts trong file module ES5:

module.exports = {
  port: 3000,
  getAccounts: function() {
    ...
  }
}

Trong file main.js ES5 chúng ta require('module.js) là dependency:

var service = require('module.js')
console.log(service.port) // 3000

Trong ES6, chúng ta sử dụng export import. Ví dụ, đây là thư viện của chúng ta trong file module.js ES6:

export var port = 3000
export function getAccounts(url) {
  ...
}

Trong file main.js ES6, chúng ta sử dụng import {name} từ cú pháp 'my-module'. Ví dụ:

import {port, getAccounts} from 'module'
console.log(port) // 3000

Chúng ta có thể import mọi thứ vào một biến service trong main.js:
 

import * as service from 'module'
console.log(service.port) // 3000

Cá nhân tôi thấy module trong ES6 lộn xộn. Vâng chúng trông hoành tráng hơn, nhưng Node.js module không thay đổi nhanh chóng. Nó tốt hơn vì chỉ có duy nhất một style JavaScript cho cả trình duyệt và server, vì thế tôi vẫn sử dụng CommonJS/Node.js.

Hỗ trợ cho module ES6 trong các trình duyệt chưa thể có ngay (tại thời điểm đang viết bài này - cuối năm 2015), vì thế bạn sẽ cần sử dụng một vài thứ chẳng hạn jspm để sử dụng module ES6.

Để biết thêm thông tin và ví dụ về module ES6, bạn có thể đọc ở đây.

Làm thế nào để sử dụng ES6 ngay hôm nay (Babel)

ES6 đã hoàn thành, nhưng chưa được hỗ trợ đầy đủ bởi các trình duyệt. Để sử dụng ES6, bạn cần một trình biên dịch như Babel. Bạn có thể chạy nó độc lập hoặc sử dụng như một plugin trong hệ thống của mình. Babel plugin có  sẵn cho Grunt, Gulp và Webpack.

Đây là một ví dụ Gulp, đầu tiên là cài đặt plugin:

$ npm install --save-dev gulp-babel

Trong gulpfile.js, định nghĩa một task build đọc file src/app.js và biên dịch tới thư mục build:

var gulp = require('gulp'),
  babel = require('gulp-babel')

gulp.task('build', function () {
  return gulp.src('src/app.js')
    .pipe(babel())
    .pipe(gulp.dest('build'))
})

Node.js và ES6

Với Node.js, bạn có thể biên dịch các file Node.js của mình với công cụ hoặc sử dụng module Babel độc lập là babel-core. Để cài đặt:

$ npm install --save-dev babel-core

Sau đó, trong Node.js bạn gọi hàm này:

require("babel-core").transform(es5Code, options)

Tóm tắt những tính năng của ES6

Còn nhiều tính năng đáng chú ý của ES6, cái mà bạn có thể không sử dụng (ít nhất là lúc này). Đây là danh sách các tính năng này:

  1. New Math, Number, String, Array and Object methods
  2. Binary and octal number types
  3. Default rest spread
  4. For of comprehensions
  5. Symblols
  6. Tail calls
  7. Generators
  8. New data structures like Map and Set

Dành cho những người muốn học thêm về ES6, đây là danh sách để đọc thêm:

  1. https://github.com/azat-co/cheatsheets/tree/master/es6
  2. https://leanpub.com/understandinges6
  3. https://leanpub.com/exploring-es6

Tham khảo thêm:

https://en.wikipedia.org/wiki/Syntactic_sugar

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

http://javascriptissexy.com/understand-javascript-closures-with-ease/