Chặng đường phát triển của Javascript từ ES6 đến ES12

Bài viết được sự cho phép của tác giả Duy Phan

Javascript là một thành phần không thể thiếu đối với frontend developers. Tuy nhiên, ngay từ lúc ra đời, nó đã tồn tại khá nhiều vấn đề cần khắc phục. Đó là lý do tại sao từ 2015 (ES6) tới 2021 (ES12) ra đời nhằm giúp Javascript trở nên tốt hơn.

Chặng đường phát triển của Javascript từ ES6 đến ES12

ECMAScript

Là một bản đặc tả kỹ thuật (specification) cung cấp các quy tắc, chi tiết, hướng dẫn mà một ngôn ngữ kịch bản (scripting language) phải tuân theo cho một vài mục đích chung.

Nói thì hơi khó hiểu, nhưng bạn có thể hiểu nôm na là: một nhóm các ông lớn trong lĩnh vực phần mềm (bao gồm nhưng không giới hạn các nhà cung cấp trình duyệt web) ngồi lại với nhau, sau đó đưa ra một concept rồi mọi người làm theo.

Cụ thể thì bản đặc tả này giúp chúng ta có một cách viết chung cho Javascript, quên đi cái thời mỗi trình duyệt chạy Javascript mỗi kiểu.

ES6 (hay ES2015)

Bản ES6 này đem đến cho chúng ta rất nhiều tính năng hữu ích, một trong số quan trọng nhất có thể kể đến như: classes, arrow functions, template string, destructuring, promises…

Đây cũng là phiên bản mang đến rất nhiều sự thay đổi, ảnh hưởng tới sự phát triển rực rỡ của Javascript tới sau này.

1. Classes

Javascript vốn dĩ là một ngôn ngữ xây dựng trên concept Prototype. Tất nhiên, nó support OOP tạm bợ kèm theo cách viết phức tạp.

Bản ES6 này cuối cùng đã đưa ra định nghĩa Class (khá quen thuộc trong các ngôn ngữ lập trình khác như PHP, .NET, Java…).

class Animal {
   constructor(name, color) {
     this.name = name;
     this.color = color;
   }

   // Đây là một property trong prototypy
   toString() {
     console.log(`name ${this.name}, color ${this.color}`);

   }
 }

var animal = new Animal('myDog', 'yellow'); // khởi tạo class
animal.toString(); // name myDog, color yellow

console.log(animal.hasOwnProperty('name')); //true
console.log(animal.hasOwnProperty('toString')); // false
console.log(animal.__proto__.hasOwnProperty('toString')); // true

class Cat extends Animal {
 constructor(action) {
   // Class này kế thừa Animal, do đó cần gọi super function trong constructor.
   super('cat','white');

   this.action = action;
 }

 toString() {
   console.log(`${super.toString()}, action ${this.action}`);
 }
}

var cat = new Cat('catch')
cat.toString(); // name cat, color white, action catch

console.log(cat instanceof Cat); // true
console.log(cat instanceof Animal); // true

2. Arrow functions

Là một cách viết function nhanh chóng mà không phải khai báo từ khóa function.

Khác với cách khai báo function thông thường, arrow function chia sẻ this context (cũng như arguments) giống phần code bên ngoài của chúng.

const add = (a, b) => {
  return a + b
}

// Nếu nội dung trong function body đơn giản, bạn có thể rút gọn luôn 2 dấu ngoặc {}
const add = (a, b) => a + b


// Sử dụng chung this context với object bên ngoài.
const someone = {
  _name: "John Doe",
  _friends: ["Jane Doe"],
  printFriends() {
    this._friends.forEach(f =>
      console.log(`${this._name} knows ${f}`))
  }
}

// Sử dụng chung arguments với function bên ngoài
function square() {
  let doSquare = () => {
    let numbers = []

    for (let number of arguments) {
      numbers.push(number * number)
    }

    return numbers
  }

  return doSquare()
}

Quên đi các kiểu viết như vầy đi nha 😀

const _self = this;

const that = this;

3. Template string

Template string cung cấp giải pháp tốt hơn thay thế việc cộng từng chuỗi nhỏ theo cách truyền thống.

Người ta nói nó giúp hạn chế các kiểu tấn công như injection attacks, nhưng cá nhân mình thì thấy nó giúp code dễ đọc hơn thôi :D.

// Template string cơ bản, không có tham biến
`This is a pretty little template string.`

// Chuỗi với nhiều dòng
`In ES5 this is
 not legal.`

// Thêm vào các tham biến
const name = "Bob", time = "today"
`Hello ${name}, how are you ${time}?`

Ngoài ra, có một tính năng nữa khá hay với template string, đó là Tagged templates.

function tagged(strings, person, age) {
  const str0 = strings[0]; // "That "
  const str1 = strings[1]; // " is a "
  const str2 = strings[2]; // "."

  const ageStr = age > 99 ? "centenarian" : "youngster"

  return `${str0}${person}${str1}${ageStr}${str2}`
}

const output = tagged`That ${'Mike'} is a ${age}.`
// That Mike is a youngster.

React có một package styled-components khá nổi tiếng cũng sử dụng tagged templates.

4. Destructuring

Destructuring giúp chúng ta xử lý nội dung của Array và Object dễ dàng hơn.

// Gán biến theo thứ tự thành phần trong Array
const [a, b, c] = [1, 2, 3]
a === 1 // true
b === 2 // true
c === 3 // true

// Bạn cũng có thể bỏ qua vài tham số nếu không cần thiết
const [a, , c] = [1, 2, 3]
c === 3 // true

// Thực hiện điều tương tự với Object
const student = { 
    name: 'John Doe', 
    age: 31,
    country: 'Vietnam'
}
const { name, age, country } = student
name === 'John Doe' // true
age === 31 // true
country === 'Vietnam' // true

5. Default value cho tham số

Nếu bạn không nhập tham số, nó sẽ tự động lấy giá trị mặc định.

function f(x, y = 12) {
  return x + y
}

f(3) === 15 // true

6. Rest syntax

function restSample(x, ...rest) {
  // rest là một Array
  return x * rest.length
}
restSample(3, "hello", "John Doe", "Vietnam") === 9 // true


const student = { 
    name: 'John Doe', 
    age: 31,
    country: 'Vietnam'
}
const { name, ...rest } = student
name === 'John Doe' // true
rest.age === 31 // true
rest.country === 'Vietnam' // true
rest.name === undefined // true

  Mẹo tạo form thu thập dữ liệu bằng JavaScript kết hợp Google Forms và Google Sheet

  Cần cải thiện kỹ năng JavaScript nào để làm React?

7. Spread syntax

# Cú pháp spead
function spreadSample(x, y, z) {
  return x + y + z;
}
// Do function spreadSample chỉ nhận 3 tham số
// Khi bạn pass 6 tham số nó cũng chỉ thao tác trên 3 tham số đầu 1,2,3
spreadSample(...[1, 2, 3, 4, 5, 6]) === 6 // true

# Sử dụng với Array
const students = ['Brian', 'Ronnie']
const people = ['Ken', ...students, 'Tom', 'Justin']
// ['Ken', 'Brian', 'Ronnie', 'Tom', 'Justin']

# Sử dụng với Object
const student = { 
  name: 'John Doe', 
  age: 31,
  country: 'Vietnam'
}
const studentB = {
  ...student,
  name: 'Jane Doe'
}
studentB.age === 31 // true
studentB.country === 'Vietnam' // true
studentB.name === 'Jane Doe' // true

8. Khai báo object nâng cao

Hỗ trợ các kiểu viết nhanh (shorthand) khi khai báo property, method cũng như thực hiện gọi hàm super.

const ageField = 'age'
const address = '290 An Duong Vuong, HCMC, Vietnam'

const student = {
  // Khai báo field mặc định, giống ES5
  name: 'John Doe',
  // Khai báo field dynamic
  [ageField]: 18,
  // Viết tắt, thay vì phải viết `address: address`
  address,
  // Khai báo nhanh phương thức trong object
  // Thay vì phải viết `toString: () {...}`
  toString() {
    // Super call
    return 'Student ' + super.toString()
  }
}

9. Sử dụng let/const thay thế cho var

let: biến chung trong một scope cụ thể, có thể bị ghi đè.

const: không thể ghi đè. Tuy nhiên, đối với các đối tượng indicators sau:

  • Array: không thể ghi đè, nhưng có thể thêm vào hoặc xóa bớt các elements của nó.
  • Object: cũng không thể ghi đè, nhưng có thể thay đổi nội dung của các properties.

Cả let/const đều chỉ hoạt động trong một scope cụ thể. Tham khảo ví dụ sau đây:

function scope() {
  {
    let x

    {
      // Đoạn code này ok vì lúc này chúng ta đã ở trong một block scoped mới
      const x = 'John Doe'

      // Lỗi rồi nha, x đã được khai báo trong cùng block với từ khóa `const` ở trên
      x = 'Jane Doe'

        {
          // Đoạn code này ok vì lúc này chúng ta đã ở trong một block scoped mới
          let x = 'Justin'
        }
    }

    // x đã được khai báo trước đo với let, nên bạn có thể assign nó tùy ý
    x = 'Ronnie'

    // Lỗi rồi nha, x đã được khai báo trong cùng block với từ khóa `let` ở trên
    let x = "inner"
  }
}

10. Iterators và for … of …

Câu lệnh for … of … thực hiện vòng lặp trên một danh sách các giá trị theo thứ tự từ một interable object.

Mặc định, Javascript tích hợp sẵn các iterable objects như ArrayStringTypedArrayMapSetNodeList, các DOM collections, đối tượng arguments.

Ngoài ra, chúng ta cũng có thể tự define các iterable objects giống như ví dụ dưới đây.

interface IteratorResult {
  done: boolean
  value: any
}

interface Iterator {
  next(): IteratorResult
}

interface Iterable {
  [Symbol.iterator](): Iterator
}

let fibonacci: Iterable = {
  [Symbol.iterator]() {
    let pre = 0,
      cur = 1
    return {
      next() {
        [pre, cur] = [cur, pre + cur]
        return { done: false, value: cur }
      },
    }
  },
}

for (const n of fibonacci) {
  // Break loop khi value lớn hơn 1000000, không thì toang
  if (n > 1_000_000)
    break;
  console.log(n);
}

11. Generators

Generators đơn giản hóa quá trình tạo ra các iterators thông qua từ khóa function* và yield. Một function được khai báo với function* sẽ trả về một Generator instance.

Generator là một kiểu con (subtype) của iterators, bao gồm next và throw.

Từ khóa yield là một expression trả về giá trị (giống như return vậy đó). Bạn cũng có thể throw exception nếu muốn.

const fibonacci: { [Symbol.iterator]: () => Generator<number> } = {
  [Symbol.iterator]: function* () {
    let pre = 0, cur = 1

    for (;;) {
      [pre, cur] = [cur, pre + cur]
      yield cur
    }
  },
}

for (const n of fibonacci) {
  // Break loop khi value lớn hơn 1000000
  if (n > 1_000_000) break
  console.log(n)
}

12. Modules

Bạn có thể viết code ở nhiều file khác nhau để dễ quản lý, mỗi file sẽ được xem như một module. Bạn chỉ cần export chúng, sau đó import vào nơi bạn cần sử dụng.

// lib/math.js
export function sum(x, y) {
  return x + y
}
export const pi = 3.141593

// app.js
import * as math from "lib/math"
console.log("2π = " + math.sum(math.pi, math.pi))

// otherApp.js
import {sum, pi} from "lib/math"
console.log("2π = " + sum(pi, pi))

Ngoài ra, ES6 còn cung cấp thêm 2 biểu thức đặc biệt là export default và export *.

// lib/mathplusplus.js
export * from "lib/math"
export var e = 2.71828182846
export default function(x) {
    return Math.exp(x)
}

// app.js
import exp, {pi, e} from "lib/mathplusplus"
console.log("e^π = " + exp(pi))

Tham khảo việc làm JavaScript hấp dẫn trên TopDev

13. Map – WeakMap – Set – WeakSet

Map – WeakMap – Set – WeakSet là các kiểu dữ liệu mới được ra đời để xử lý Collection – hay dữ liệu có cấu trúc, giúp giải quyết những thiếu sót mà các kiểu dữ liệu thông thường (như Object, Array) gặp phải, đồng thời mang lại tốc độ xử lý tốt hơn.

Set

Set là một collection các phần tử khác nhau và không có key. Dữ liệu trong Set luôn luôn là duy nhất (unique).

// Khởi tạo Set
const blankSet = new Set()

// Khởi tạo Set có khai báo giá trị
const set: Set<string | number> = new Set(['a', 'b', 'c', 'd', 1, 2, 3])

// Thêm phần tử
const s: Set<string> = new Set()
s.add('hello').add('goodbye').add('hello')

// Đếm số phần tử
s.size === 2 // true

// Kiểm tra một phần tử có tồn tại hay không
s.has('hello') // true

// Xóa phần tử
const char = new Set(['a', 'b', 'c'])
char.delete('c') // char = Set(2) {"a", "b"}

// Loop qua các phần tử
// Do Set là kiểu dữ liệu iterators, do đó chúng ta cần dùng for...of
const chars = new Set(['a', 'b', 'c'])
for (const char of chars){
  console.log(char)
}

// Convert một Set sang Array
const char = new Set(['a', 'b', 'c'])
const arrChar = [...char] // ['a', 'b', 'c']

WeakSet

Tương tự như Set, tuy nhiên có một vài khác biệt lớn:

  • dữ liệu truyền vào WeakSet phải là objectclass hoặc function.
  • không có thuộc tính size, nên chỉ có loop từ từ.
  • nó cũng không có các methods như clearkeysvaluesentriesforEach như Set.
  • do đó, nó cũng không thuộc kiểu iterators.
// Khởi tạo WeakSet
const ws = new WeakSet()
const value = { data: 42 }
ws.add(value)

Một điểm quan trọng nữa là bạn cần tạo biến cho từng element trước khi add vào WeakSet, nếu không Javascript sẽ coi như đó là dữ liệu rác và thực hiện garbage collected.

// Khởi tạo WeakSet
const ws = new WeakSet()
const value = { data: 42 }
ws.add(value) // [{ data: 42 }]
// Do element truyền vào không có references ở đâu cả
// Nên hệ thống tự động thực hiện garbage collected.
ws.add({ data: 43 }) // [{ data: 42 }]

Thực ra mình cũng chưa có dịp xài thằng này bao giờ.

Map

Thằng này thì nó cũng tương tự như Set, nhưng Map sẽ có cấu trúc dạng key => value, trong khi Set chỉ có value.

key của Map có thể là bất cứ thứ gì (stringnumbersymbolNaNclassfunction,…)

Đây cũng là thứ mà NestJS lưu trữ các Provider để thực hiện dependency injection.

const m = new Map()
m.set('hello', 42)
m.get('hello') === 42 // true

// Key có thể là bất cứ cái gì bạn muốn
const set = new Set()
m.set(set, 34)
m.get(set) === 34 // true

WeakMap

Thằng này cũng tương tự WeakSet, tuy nhiên WeakSet thì dựa trên value, còn WeakMap dựa trên key nha mọi người.

Thằng này mình cũng chưa xài bao giờ luôn 😀

14. Proxy

Đây là một tính năng cực kỳ cực kỳ hay ho mà ES6 cung cấp. Nó giúp chúng ta thực hiện những thứ tương tự như Magic Methods trong PHP.

Bạn có thể hiểu nó wrap lại một variable, khi bạn thao tác một hành động nào đó, nó sẽ chặn lại và thực hiện một vài xử lý cụ thể mà bạn muốn.

const proxy = new Proxy(target, handler)
  • target: đối tượng bạn cần wrap
  • handler: nó được gọi là proxy configuration – hoặc trap.
// Thực hiện proxy một object thường
const target = {}
const handler = {
  get: function (receiver, name) {
    return `Hello, ${name}!`
  }
}
const p = new Proxy(target, handler)
p.world === "Hello, world!" // true

// Thực hiện proxy một function
const target = function () {
  return 'I am the target'
}
const handler = {
  apply: function (receiver, ...args) {
    return 'I am the proxy'
  },
}
const p = new Proxy(target, handler)
p() === 'I am the proxy' // true

Sau đây là danh sách các trap mà proxy hiện tại đang hỗ trợ:

const handler = {
  // target.prop
  get: ...,
  // target.prop = value
  set: ...,
  // 'prop' in target
  has: ...,
  // delete target.prop
  deleteProperty: ...,
  // target(...args)
  apply: ...,
  // new target(...args)
  construct: ...,
  // Object.getOwnPropertyDescriptor(target, 'prop')
  getOwnPropertyDescriptor: ...,
  // Object.defineProperty(target, 'prop', descriptor)
  defineProperty: ...,
  // Object.getPrototypeOf(target), Reflect.getPrototypeOf(target),
  // target.__proto__, object.isPrototypeOf(target), object instanceof target
  getPrototypeOf: ...,
  // Object.setPrototypeOf(target), Reflect.setPrototypeOf(target)
  setPrototypeOf: ...,
  // for (let i in target) {}
  enumerate: ...,
  // Object.keys(target)
  ownKeys: ...,
  // Object.preventExtensions(target)
  preventExtensions: ...,
  // Object.isExtensible(target)
  isExtensible :...
}

15. Symbol

Symbol là một kiểu dữ liệu nguyên thủy (primitive data types). Khi bạn tạo một symbol, giá trị của nó được giữ kín và chỉ sử dụng nội bộ.

Mỗi một symbol đều là duy nhất, có nghĩa là chúng không bao giờ có giá trị giống nhau, mặc dù nhìn trông giống nhau.

Ơ nghe ngáo nhỉ 😀

const ABC = Symbol('ABC')
const ABC2 = Symbol('ABC')
ABC === ABC2 // false

Bạn cũng có thể khởi tạo symbol bằng expression Symbol.for. Lúc này chúng sẽ hoạt động như một singleton – nếu đã tồn tại thì trả về symbol đã tạo, nếu chưa có thì tạo mới.

const ABC3 = Symbol.for('ABC')
const ABC4 = Symbol.for('ABC')
ABC3 === ABC4 // true

Lưu ý: sử dụng Symbol.for khi bạn muốn tạo một symbol ở chỗ này nhưng lại sử dụng ở chỗ khác, tuy nhiên nó sẽ khiến symbol mất đi tính duy nhất. Cho nên, tốt nhất bạn nên export ra rồi sử dụng thay vì khai báo kiểu singleton này.

16. Math – Number – String – Object APIs

Có nhiều thư viện mới được thêm vào bản ES6 này.

Number.EPSILON
Number.isInteger(Infinity) // false
Number.isNaN('NaN') // false

Math.acosh(3) // 1.762747174039086
Math.hypot(3, 4) // 5
Math.imul(Math.pow(2, 32) - 1, Math.pow(2, 32) - 2) // 2

'abcde'.includes('cd') // true
'abc'.repeat(3) // "abcabcabc"

Array.from(document.querySelectorAll('*'))
Array.of(1, 2, 3)

[(0, 0, 0)].fill(7, 1) // [0,7,7]

[(1, 2, 3)].findIndex((x) => x == 2) // 1

[('a', 'b', 'c')].entries() // iterator [0, "a"], [1,"b"], [2,"c"]
[('a', 'b', 'c')].keys() // iterator 0, 1, 2
[('a', 'b', 'c')].values() // iterator "a", "b", "c"

Object.assign(Point, { origin: new Point(0, 0) })

17. Promises

Promise là một giải pháp xử lý bất đồng bộ, với cách viết trang nhã, dễ đọc hơn so với callback truyền thống.

Promises

Trước đó, Promise đã được implement trong một vài thư viện của JS như Q, When, Bluebird… Từ bản ES6, nó đã được hỗ trợ trong JS core.

Một Promise đại diện cho một value có thể chưa tồn tại ở thời điểm hiện tại, nhưng sẽ được xử lý và có value cụ thể vào một thời gian nào đó trong tương lai.

function timeout(duration = 0) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, duration)
  })
}

const p = timeout(1000)
  .then(() => {
    return timeout(2000)
  })
  .then(() => {
    throw new Error('hmm')
  })
  .catch((err) => {
    return Promise.all([timeout(100), timeout(200)])
  })

18. Reflect API

Ở ES5, nó tồn tại dưới dạng static method của Object. Lên ES6 thì nó được tách ra thành Reflect class.

Reflect không có constructor, do đó bạn không thể khởi tạo một new instance. Mọi phương thức của nó đều là static.

Các phương thức của Reflect tương ứng với các traps của proxy và kết quả trả về của nó cũng chính là kết quả trap cần trả về.

const O = { a: 1 }
Object.defineProperty(O, 'b', { value: 2 })
O[Symbol('c')] = 3

Reflect.ownKeys(O) // ['a', 'b', Symbol(c)]

function C(a, b) {
  this.c = a + b
}

const instance = Reflect.construct(C, [20, 22])
instance.c // 42

ES7 (hay ES2016)

TC39 giới thiệu rất nhiều features, nhưng release có vài cái thôi 😀

1. Array.includes

Kiểm tra một phần tử có tồn tại hay không.

interface Array<T> {
  includes(searchElement: T, fromIndex?: number): boolean
}

[1,2,3].includes(1) // true

2. Toán tử lũy thừa

Bạn có thể sử dụng toán tử ** thay cho cách viết thông qua library Math.pow.

console.log(2**10) // 1024
// Tương tự với
console.log(Math.pow(2, 10))

ES8 (hay ES2017)

Release nhiều hơn bản ES7 một xíu 😀

1. async – await

Promise trong ES6 cũng tuyệt vời đấy, nhưng lắm lúc nó lại đẻ ra cái promise hell như thế này

connectToDatabase().then((database) =>
  getUser(database).then((user) =>
    getUserSettings(database)
      .then((settings) => enableAccess(user, settings))
  )
)

Nhức nách quá phải không 😀

Rất may async – await đã được sinh ra để giải quyết vấn đề này

const database = await connectToDatabase()
const user = await getUser(database)
const settings = await getUserSettings(database)
await enableAccess(user, settings)

Lúc này code trông clean hẳn rồi phải không 😀

2. Object.values()

Trả về toàn bộ values của một object dưới dạng array.

const countries: Record<string, string> = {
    BR: 'Brazil',
    DE: 'Germany',
    RO: 'Romania',
    US: 'United States of America'
}

const values: string[] = Object.values(countries) // ['Brazil', 'Germany', 'Romania', 'United States of America']

3. Object.entries()

Trả về một array các pairs (mỗi pair là một array có 2 phần tử, phần tử đầu tiên là key, phần tử thứ hai là value).

const countries: Record<string, string> = {
  BR: 'Brazil',
  DE: 'Germany',
  RO: 'Romania',
}

const entries: Array<[string, string]> = Object.entries(countries)
// [['BR', 'Brazil'], ['DE', 'Germany'], ['RO', 'Romania']]

4. Get tất cả descriptors của Object

Object.getOwnPropertyDescriptors() trả về tất cả descriptor của một object (ngoại trừ các thuộc tính được extends từ object / class cha).

const obj = {
  name: 'Pablo',
  get foo() {
    return 42
  },
}

Object.getOwnPropertyDescriptors(obj)
//
// {
//  "name": {
//     "value": "Pablo",
//     "writable":true,
//     "enumerable":true,
//     "configurable":true
//  },
//  "foo":{
//     "enumerable":true,
//     "configurable":true,
//     "get": function foo()
//     "set": undefined
//  }
// }

5. String padding (padStart, padEnd)

interface String {
  padStart(maxLength: number, fillString?: string): string
  padEnd(maxLength: number, fillString?: string): string
}

// Trả về một string mới có độ dài bằng 10
// Có thêm 6 space phía trước, do độ dài ban đầu là 4 ký tự
'0.10'.padStart(10) // '      0.10'

'0.10'.padStart(12) // '       0.10'
'23.10'.padStart(12) // '      23.10'
'12,330.10'.padStart(12) // '  12,330.10'

'hi'.padStart(1) // 'hi'
'hi'.padStart(5) // '   hi'
'hi'.padStart(5, 'abcd') // 'abchi'
'hi'.padStart(10, 'abcd') // 'abcdabcdhi'

'loading'.padEnd(10, '.') // 'loading...'

6. Trailing commas

Cho phép tồn tại dấu phẩy ở sau tham số cuối cùng của một khi khai báo hoặc gọi một function.

getDescription(name, age,) { ... }

ES8 có một vài thay đổi tuy nhỏ nhưng tác động rất lớn, nhưng đối với mình quan trọng nhất vẫn là các toán tử bất đồng bộ async – await.

ES9 (hay ES2018)

Thay đổi bự nhất chắc là cho phép thực hiện await trong vòng loop.

1. await khi loop

Đôi khi chúng ta có nhu cầu thực hiện gọi một function async trong vòng lặp for.

async function doSomething(item) {
  return Promise.resolve(item)
}

async function process(array) {
  for (const i of array) {
    await doSomething(i)
  }
}

async function process(array) {
  array.forEach(async i => {
    await doSomething(i)
  })
}

Code như vầy là tèo cmnr nha 😀

Do bản thân vòng lặp for là đồng bộ nên nó sẽ thực hiện xong toàn bộ vòng lặp trước khi các biểu thức bất đồng bộ bên trong được xử lý xong.

ES9 giới thiệu một cách giúp thực hiện điều này.

async function process(array) {
  for await (const i of array) {
    doSomething(i)
  }
}

2. Promise.finally()

finally() sẽ đuợc gọi sau khi một Promise hoàn thành, bất kể là fails (.catch()) hay success (.then()).

function process() {
  process1()
  .then(process2)
  .then(process3)
  .catch(console.log)
  .finally(() => {
    console.log(`it must execut no matter success or fail`)
  })
}

3. Cải thiện rest – spead syntax

Thực ra mình lỡ giới thiệu hết trong phần ES6 rồi 😀 do quên mất là một số tính năng chỉ xuất hiện sau bản ES9.

Thôi kéo lên coi lại giúp mình nha 😀

4. RegExp groups

RegExp lúc này có thể trả về các packets thỏa điều kiện.

const regExpDate = /([0-9]{4})-([0-9]{2})-([0-9]{2})/
const match = regExpDate.exec('2020-06-25')
const year = match[1] // 2020
const month = match[2] // 06
const day = match[3] // 25

5. Regexp dotAll

. đại diện cho bất kỳ ký hiệu ngoại trừ \n – hay xuống dòng – enter.

Thêm vào cờ s giúp cho phép \n.

/hello.world/.test('hello\nworld')  // false
/hello.world/s.test('hello\nworld') // true

ES10 (hay ES2019)

Cũng không có quá nhiều sự thay đổi, tuy nhiên cũng khá thú vị.

1. JSON.stringify() hoạt động thân thiện hơn

Trước đó, JSON.stringify() sẽ trả về một chuỗi string không đúng định dạng Unicode nếu đầu vào là một Unicode nhưng nằm ngoài vùng hỗ trợ.

Bây giờ thì nó sẽ trả ra một chuỗi Unicode hợp lệ dưới dạng UTF-8.

2. Kiểu dữ liệu bigint

BigInt – bigint là một kiểu dữ liệu nguyên thủy mới được sử dụng để biểu diễn các số nguyên có giá trị lớn hơn 2^53. Đây cũng là định dạng số lớn nhất mà Javascript có thể biểu diễn.

const x = Number.MAX_SAFE_INTEGER;
// 9007199254740991 - nó chính là (2^53 - 1)

const y = x + 1;
// 9007199254740992 - nó chính là (2^53)

const z = x + 2
// 9007199254740992 - vẫn là 2^53

Một số BigInt được tạo bằng cách thêm n vào cuối chữ số hoặc có thể gọi hàm tạo như ví dụ bên dưới

const theBiggestInt = 9007199254740991n

const alsoHuge = BigInt(9007199254740991)
// 007199254740991n

const hugeButString = BigInt('9007199254740991')
// 9007199254740991n

typeof 123 // 'number'
typeof 123n // 'bigint'

42n === BigInt(42) // true
42n == 42 // true

Tuy nhiên, khi bạn thực hiện các phép toán (+ – * / …) thì bắt buộc phải cùng kiểu dữ liệu nha

20000000000000n / 20n
// 1000000000000n

20000000000000n / 20
// Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions

3. String.matchAll()

String.matchAll() sẽ trả về một iterator chứa toàn bộ các kết quả trùng khớp với đối số RegExp được truyền vào.

const regexp = /t(e)(st(\d?))/g
const str = 'test1test2'
const array = [...str.matchAll(regexp)]

console.log(array[0])
// Array ["test1", "e", "st1", "1"]

console.log(array[1])
// Array ["test2", "e", "st2", "2"]

4. Array.flat() – Array.flatMap()

Trước đó để flatten một Array, chúng ta hay dùng một library ngoài như Lodash. Giờ thì Javascript đã support sẵn luôn.

const arr1 = [1, 2, [3, 4]]
arr1.flat() // [1, 2, 3, 4]

const arr2 = [1, 2, [3, 4, [5, 6]]]
arr2.flat() // [1, 2, 3, 4, [5, 6]]

// Chỉ định depth thay cho mặc định
arr2.flat(2) // [1, 2, 3, 4, 5, 6]

Khi không có đối số thì depth mặc định bằng 1 nha.

Chúng ta cũng có thể flatten toàn bộ Array mà không cần quan tâm tới depth bằng cách truyền vào Infinity.

const animals = [['', ''], ['', '', ['',[''], '']]]

const flatAnimals = animals.flat(Infinity)

console.log(flatAnimals)
// ['', '', '', '', '', '', '']

Còn Array.flatMap() là sự kết hợp của 2 phương thức map() và flat() với depth=1. Hay nói cách khác là mỗi giá trị của mảng sẽ được map sang một mảng mới và flat với depth=1.

const animals = ['', '', '', '']
const noises = ['woof', 'meow', 'baa', 'mooo']

const mappedOnly = animals.map((animal, index) => [animal, noises[index]])
const mappedAndFlatten = animals.flatMap((animal, index) => [
  animal,
  noises[index],
])

console.log(mappedOnly)
// [['', 'woof'], ['', 'meow'], ['', 'baa'], ['', 'mooo']

console.log(mappedAndFlatten)
// ['', 'woof', '', 'meow', '', 'baa', '', 'mooo']

5. String.trimStart() và String.trimEnd()

String.trimStart() loại bỏ space ở đầu chuỗi trong khi String.trimEnd() thì xóa space ở cuối chuỗi.

const greeting = ' Hello world! '
console.log(greeting.trimStart()) // 'Hello world! '

console.log(greeting.trimEnd()) // ' Hello world!'

6. Object.fromEntries()

Phương thức này cho phép nhận một cặp pair key – value thành một object. Nó hoạt động ngược lại so với Object.entries().

const entries = new Map([
  ['foo', 'bar'],
  ['baz', 42],
])
const obj = Object.fromEntries(entries)
console.log(obj) // Object { foo: "bar", baz: 42 }

ES11 (hay ES2020)

1. Promise.allSettled()

Promise hỗ trợ hai loại combinators, đó là hai phương thức tĩnh Promise.all() và Promise.race(). Nhưng với ES11 thì bạn có thêm phương thức Promise.allSettled().

Nếu Promise.all() và Promise.race() sẽ dừng lại khi có bất kì một Promise nào bị rejected thì Promise.allSettled() sẽ tiếp tục chạy hết các Promise còn lại.

const promise1 = Promise.resolve(3)
const promise2 = new Promise((resolve, reject) =>
  setTimeout(reject, 100, 'foo')
)
const promises = [promise1, promise2]
Promise.allSettled(promises).then((results) =>
  results.forEach((result) => console.log(result.status))
)
// "fulfilled"
// "rejected"

2. Optional chaining ?

Trong quá trình làm việc, chắc hẳn bạn cũng đã quá quen thuộc với các câu lệnh kiểm tra giá trị có tồn tại hay không trước khi xử lý tiếp:

interface Car {
  id: string
  info: {
    name: string
  }
}

const car: Car | undefined = await getCar()

const isCarExist = car && car.info

if (isCarExist) { 
  carName = car.info.name
}

// Đoạn code sau sẽ báo lỗi khi car không tồn tại
const carInfo = car.info

Với ES11, chúng ta có thể kiểm tra một property của Object hoặc element của Array có tồn tại hay chưa bằng toán tử ?

carName = car?.info?.name
const carInfo = car?.info

// Hoặc kết hợp giá trị default nếu chưa tồn tại
carName = car?.info?.name || 'Default Name'

// Tương tự với Array
const arr: { name: string }[] = [{ name: 'John' }, { name: 'Jane' }, { name: 'James' }]
const name4 = arr[4]?.name || 'Default Name'

Code clean hơn nhiều rồi phải không 😀

3. Toán tử check null ??

Trong Javascript, 3 giá trị 0null và undefined đều đại diện giá trị false.

!!0 // false
!!undefined // false
!!null // false

Nhưng đôi lúc, 0 là một giá trị bình thường, chúng ta không muốn nó là false.

const user = {
  level: 0,
}

const level = user.level || 'no level' 
// 0 đại diện cho false, nên ở đây JS sẽ hiển thị 'no level'


// Nếu bạn muốn hiển thị 0
const level =
  user.level !== undefined && user.level !== null ? user.level : 'no level'

ES11 cung cấp cách thức khác clean hơn

const username = user.level ?? 'no level' // 0

4. Dynamic import

Như tên gọi, chúng ta có thể import một thư viện ở bất kỳ đâu trong đoạn code.

el.onclick = () => {
  import(`./greetingsModule.js`)
    .then((module) => {
      module.doSomthing()
    })
    .catch(console.error)
}

Hãy nhớ là dynamic import sẽ trả về một Promise nhé. Bạn hoàn toàn có thể sử dụng nó kết hợp với async/await.

ES12 (hay ES2021)

1. Promise.any()

Promise.any() sẽ trả về data của Promise đầu tiên trong mảng được resolved. Nếu tất cả Promise đều bị rejected, nó sẽ ném lỗi AggregateError.

Phương thức này ngược lại với Promise.race().

const promise1 = Promise.reject(0)
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'quick'))
const promise3 = new Promise((resolve) => setTimeout(resolve, 500, 'slow'))

Promise.any([promise1, promise2, promise3]).then((value) => console.log(value))
// quick

const promise4 = Promise.reject(0)
const promise5 = Promise.reject(0)
const promise6 = Promise.reject(0)

Promise.any([promise4, promise5, promise6]).then((value) => console.log(value))
// AggregateError: All promises were rejected

2. Numeric separators

Để cải thiện khả năng đọc, bổ sung mới này cho phép sử dụng _ làm dấu phân cách trong các ký tự số.

// A decimal integer literal with its digits grouped per thousand:
1_000_000_000_000
// A decimal literal with its digits grouped per thousand:
1_000_000.220_720
// A binary integer literal with its bits grouped per octet:
0b01010110_00111000
// A binary integer literal with its bits grouped per nibble:
0b0101_0110_0011_1000
// A hexadecimal integer literal with its digits grouped by byte:
0x40_76_38_6A_73
// A BigInt literal with its digits grouped per thousand:
4_642_473_943_484_686_707n

Lưu ý là nó không ảnh hưởng tới giá trị, chỉ là giúp chúng ta đọc code dễ hơn mà thôi.

3. String.replaceAll()

Một cách viết khác cho phương thức String.replace() giúp replace toàn bộ string mà không cần sử dụng RegExp.

const str = 'macOS is way better than windows. I love macOS.'
const newStr = str.replace('macOS', 'Linux')
console.log(newStr)
// Linux is way better than windows. I love macOS.

const newStr2 = str.replace(/macOS/g, 'Linux')
console.log(newStr2)
// Linux is way better than windows. I love Linux.

const str = 'macOS is way better than windows. I love macOS.'
const newStr = str.replaceAll('macOS', 'Linux')
console.log(newStr)
// Linux is way better than windows. I love Linux.

4. Toán tử gán có điều kiện &&=||=??=

Các toán tử gán logic mới được đề xuất &&=||=??= giúp chúng ta có thể gán một giá trị cho một biến dựa trên một phép toán logic. Nó kết hợp phép toán logic với biểu thức gán.

let x = 10
let y = 15

x &&= y
// Tương đương if(x) x = y

x ||= y
// Tương đương if(!x) { x = y }

x ??= y
// Tương đương if(x === null || x === undefined) { x = y }

Kết

Bài dài quá rồi. Mình sẽ tổng hợp về ES2022 vào phần tới nhé.

Bài viết gốc được đăng tải tại duypt.dev

Xem thêm:

Xem ngay tin đăng tuyển lập trình viên đãi ngộ tốt trên TopDev