ES5 là đặc tả ngôn ngữ JavaScript được sử dụng phổ biến nhất hiện nay. Cơ bản về JavaScript từ ES5 trở về trước có thể tham khảo tại https://ngocminhtran.com/javascript-co-ban/. Tuy nhiên, nhiều đặc trưng mới được giới thiệu trong các đặc tả ES6, ES7, v.v. Hiểu về các đặc trưng mới trong JavaScript sẽ giúp chúng ta sử dụng các framework như React, Angular, v.v. một cách hiệu quả.

let const

Trước ES6 (ES2015), khai báo biến trong JS sử dụng từ khóa var và trong ES6 bổ sung thêm hai từ khóa là letconst. Một biến được khai báo với const sẽ nhận giá trị không đổi hay không thể được gán lại như ví dụ:


const count = 2;

count = count + 1; // không hợp lệ

Từ ES5 trở về trước trong JS chỉ có hai phạm vi khai báo biến là phạm vi toàn cục (biến toàn cục – global variables) (bên ngoài các hàm) và có thể được truy cập từ bất kỳ vị trí nào trong chương trình như ví dụ:


var carName = "Volvo";// biến toàn cục

// có thể dùng carName ở đây

function myFunction() {

  // có thể dùng carName ở đây

}

Và phạm vi hàm (biến cục bộ – local variables) là các biến chỉ có thể được truy cập trong các hàm đã khai báo chúng như ví dụ:


// carName không thể dùng tại đây

function myFunction() {

   var carName = "Volvo";

  // carName dùng tại đây

}

// carName không thể dùng tại đây

Với từ khóa let trong ES6, biến trong JS có thêm một phạm vi mới là phạm vi khối (block variables) tức là các biến được chỉ được truy cập trong phạm vi giới hạn bởi cặp ngoặc {}. Xem ví dụ về cách dùng letvar:


var x = 10;// phạm vi toàn cục

// Nếu x dùng ở đây, giá trị x = 10

{

   let x = 2;// phạm vi khối

   // Nếu x dùng ở đây, giá trị x = 2

}

// Nếu x dùng ở đây, giá trị x = 10

Thực thi với CodePen. 

Hàm mũi tên (Arrow functions)

Cú pháp

Định hàm trong JavaScript từ ES5 trở về trước sẽ như sau:


var myF = function(x,y){

  return x+y;

}

ES6 giới thiệu cách viết ngắn gọn với hàm bằng cách dùng hàm mũi tên theo cú pháp cho trường hợp hàm không có tham số


Biến_hàm = ( ) => {

  //lệnh thực thi của hàm

}

Hay có tham số:


Biến_hàm = ( tham số 1, tham số 2,… ) => {

  //lệnh thực thi của hàm

}

myF (ví dụ trên) có thể được viết lại dùng hàm mũi tên có tham số:


var myF = (x,y)=>{

   return x+y;

}

Hàm một lệnh

Trong trường hợp hàm chỉ có một lệnh duy nhất, ví dụ myF chỉ có một lệnh return, chúng ta có thể dùng cú pháp:

Biến_hàm = ( tham số 1, tham số 2,… ) => lệnh thực thi hàm

myF chỉ có một lệnh nên có thể được viết lại như sau:


var myF = (x,y)=> x+y;

Dùng myF:


document.getElementById('kq').innerHTML = myF(2,3);// 5

Trả về ngầm định

Hàm mũi tên chỉ có một lệnh cho phép trả về giá trị ngầm định, tức là không cần dùng từ khóa return. Xem xét ví dụ sau:


var x = () => 8;

document.getElementById('kq').innerHTML = x(); // 8

Từ khóa this

Từ khóa this được dùng tùy thuộc vào ngữ cảnh và cả chế độ strict hay không của JavaScript. Trong nhiều ngữ cảnh, hàm mũi tên sẽ ứng xử khác so với hàm bình thường. Xét đối tương car với phương thức fullName như sau:


const car = {

   model: 'Fiesta',

   manufacturer: 'Ford',

   fullName: function() {

      return `${this.manufacturer} ${this.model}`;

   }

}

Hàm (hay phương thức) fullName trả về một chuỗi kết hợp hai biến modelmanufacturer. Ở đây chúng ta dùng kí hiệu ${} còn được gọi là string interpolation (tương tự trong C#). Gọi fullName từ đối tượng car như sau: car.fullName() thì kết quả là Ford Fiesta.

Hàm fullName có thể được viết lại dùng hàm mũi tên như sau:


const car = {

  model: 'Fiesta',

  manufacturer: 'Ford',

  fullName: () => {

    return `${this.manufacturer} ${this.model}`;

  }

}

Nếu gọi car.fullName() kết quả sẽ là undefined undefined. Nguyên nhân là do phạm vi của this với hàm mũi tên chỉ được thừa kế từ ngữ cảnh thực thi, cụ thể là thực thi hàm fullName và hàm mũi tên sẽ không kết nối this với các biến modelmanufacturer.

Hiểu được cách dùng this trong hàm bình thường và hàm mũi tên là rất quan trọng để có thể sử dụng hiệu quả nhất từ khóa này.

Thực thi với CodePen. 

Hàm khởi tạo (constructor)

Hàm khởi tạo là một hàm đặc biệt của đối tượng và sẽ được nhắc lại trong bài viết về lập trình hướng đối tượng trong JS. Hàm mũi tên sẽ không được dùng để định nghĩa các hàm khởi tạo trong đối tượng. Nếu sử dụng, lỗi TypeError sẽ xuất hiện.

Các đối tượng (objects) và mảng (arrays)

Chúng ta có thể mở rộng một mảng, một đối tượng hay một chuỗi bằng toán tử (ba chấm) trong JS (gọi là the spread operator). Xét ví dụ một mảng


const a = [1,2,3];

document.getElementById('kq').innerHTML = a;// 1,2,3

chúng ta có thể mở rộng mảng a dùng …:


const b = [...a,4,5,6];

document.getElementById('kq').innerHTML = b;//1,2,3,4,5,6

có thể tạo ra một bản sao mảng a:


const c = [...a];

Tương tự mảng, chúng ta cũng có thể tạo ra một bản sao của một đối tượng:


const obj = {name:'Minh', age:36};

const newobj = {...obj};

Toán tử …sẽ chuyển một chuỗi thành một mảng các ký tự của chuỗi đó:


const str = 'hello';// chuỗi str

const strarr = [...str];//mảng strarr

document.getElementById('kq').innerHTML = strarr;//h,e,l,l,o

Một trong những ứng dụng quan trọng nhất của toán tử ba chấm (the spread operator) là hỗ trợ sử dụng một mảng giống như một đối số hàm (function argument). Xét hàm f sau:


const f = (x,y,z) => {return x+y+z;}

Trước đây, chúng ta có thể dùng hàm apply để biến mảng a thành đối số hàm như sau:


document.getElementById('kq').innerHTML = f.apply(null,a);// 6

Hiện tại, chúng ta có thể dùng toán tử ba chấm một cách đơn giản:


document.getElementById('kq').innerHTML = f(...a);// 6

Phân rã mảng và đối tượng

Trong JS chúng ta có thể dễ dàng phân rã các phần tử tử một mảng và gán cho các biến hay cho mảng khác. Ví dụ cho mảng numbers:


const numbers = [1, 2, 3, 4, 5];

Có thể gán các phần tử thứ nhất và thứ hai trong mảng đến 2 biến firstsecond như sau:


const numbers = [1, 2, 3, 4, 5];

const [first,second] = numbers;

có thể gán phần tử thứ nhất, thứ hai và cuối cùng:


[first,second,,,fifth] = numbers;

Toán tử ba chấm cũng hữu ích khi chúng ta cần phân rã các phần tử từ một mảng (array destructuring), ví dụ:


[first, second, ...others] = numbers;

document.getElementById('kq').innerHTML = first;// 1

document.getElementById('kq').innerHTML = second;// 2

document.getElementById('kq').innerHTML = others;// 3,4,5

Ở đây chúng ta gán phần tử thứ nhất của mảng numbers cho biến first, phần tử thứ hai cho biến second và phần còn lại cho mảng others.

ES2018 cho phép chúng ta thực hiện tương tự với các đối tượng, ví dụ:


const { first, second, ...others } = {

  first: 1,

  second: 2,

  third: 3,

  fourth: 4,

  fifth: 5

}

first // 1

second // 2

others // { third: 3, fourth: 4, fifth: 5 }

Có thể áp dụng ngược lại:


const items = { first, second, ...others }

items //{ first: 1, second: 2, third: 3, fourth: 4, fifth: 5 }

Thực thi với CodePen. 

Khai báo chuỗi với Template Literals

ES2015/ES6 giới thiệu cách khai báo chuỗi mới dùng dấu bên cạnh các khai báo chuỗi truyền thống với dấu nháy đơn ‘ ‘ hay nháy kép “”. Cân nhắc ví dụ:


const string = "My name is Minh";

Nếu chúng ta muốn viết chuỗi thành hai hàng thì phải dùng dấu \ như sau:


const string = "My name\

is Minh";

Để hiển thị chuỗi thành hai hàng giống như lệnh trên chúng ta thêm dấu \n:


const string = "My name \n\

is Minh";

Kể từ ES6, chúng ta có thể khai báo lại biến string như sau:


const string = `My name

is Minh`;

Lưu ý chúng ta dùng cặp “ và khi viết thành hai dòng chỉ việc nhấn Enter mà không cần dùng thêm dấu \n và \ như khi khai báo chuỗi dùng nháy đơn hay nháy kép. Cách thức khai báo chuỗi như trên gọi là Template Literals.

Một trong những vai trò quan trọng nhất của Template Literals chính là hỗ trợ String Inperpolation (xem lại phần từ khóa this ở trên). Cân nhắc ví dụ sau:


const name = "Minh";

const string = `My name is ${name}`; // Kết quả: My name is Minh

Ngoài ra, Template Literals còn rất nhiều hữu ích khác trong JS.

Thực thi với CodePen.

Lập trình hướng đối tượng

Trước ES6, lập trình hướng đối tượng trong JS gây rất nhiều khó khăn cho những người đã quên thuộc với các ngôn ngữ như Java, C# hay Python. ES6 đã giới thiệu tiêu chuẩn lập trình hướng đối tượng mới giúp chúng ta tiếp cận một cách tự nhiên hơn mà vẫn giữ được những điểu mạnh của JS trước đó.

Khai báo lớp

Lớp (class) được khai báo với từ khóa class như ví dụ lớp Person sau:


class Person {

}

Hàm constructor

Với các ngôn ngữ như C# hay Java, hàm constructor có tên hàm trùng với tên lớp và không có kiểu trả về. Trong JS, hàm này dùng từ khóa constructor như sau:


class Person {

  constructor(name) {

    this.name = name;

  }

}

Ở đây chúng ta cần phân biệt hai biến name, một biến name là biến cục bộ của hàm khởi tạo và một biến name là biến của lớp Person. Dùng từ khóa this để phân biệt hai biến trùng tên.

Lớp được sử dụng thông qua thể hiện (instance) như sau:


const minh = new Person('Ngoc Minh Tran');

Chúng ta có thể truy cập trực tiếp biến name của lớp Person thông qua thể hiện như sau:


alert(minh.name); // Ngoc Minh Tran

Trong các ngôn ngữ như C# hay Java có hỗ trợ chức năng quá tải constructor cho phép chúng ta định nghĩa nhiều hàm constructor trong một lớp, tuy nhiên, JS không hỗ trợ điều này. Trong JS, một lớp chỉ chứa một constructor.

Phương thức của lớp

Một hàm (function) được khai báo trong một lớp gọi là một phương thức (method). Khi khai báo phương thức chúng ta không dùng từ khóa function như ví dụ phương thức hello của lớp Person:


class Person {

  constructor(name) {

    this.name = name;

  }

  hello() {

    return 'Hello, I am ' + this.name + '.';

  }

}

Phương thức hello() phải được truy cập qua thể hiện của lớp Person:


alert(minh.hello());

Phương thức có thể được truy cập trực tiếp thông qua tên lớp nếu chúng ta khai báo phương thức này là phương thức tĩnh bằng cách dùng từ khóa static trước tên phương thức như sau:


static sHello() {

  return 'Hello, I am a static method.';

}

Truy cập đến phương thức này:


alert(Person.sHello());

Trong JS chưa hỗ trợ các phương thức private hay protected.

Getter và Setter

Như đã đề cập ở phần trên, chúng ta có thể truy cập trực tiếp đến biến name thông qua các thể hiện của lớp Person. Tuy nhiên, trong mô hình hướng đối tượng, cách truy cập hiệu quả nhất đến các biến của lớp là sử dụng các setter hay getter. Định nghĩa lại lớp Person dùng setter (setName) và getter (getName) như sau và chú ý cách dùng:


class Person {

  constructor(name) {

    this.name = name;

  }

  set setName(value){

    this.name = value;

  }

  get getName() {

    return this.name;

  }

  hello() {

    return 'Hello, I am ' + this.name + '.';

  }

  static sHello(){

    return "Hi everyone, I am a static method.";

  }

}

const minh = new Person();

minh.setName = 'Ngoc Minh';

alert(minh.getName);

Giống Java hay C#, setter bắt đầu bằng từ khóa set và getter bắt đầu bằng get. Một lớp có thể chỉ có setter hay getter hoặc cả hai.

Thừa kế

Một trong những ưu điểm của mô hình hướng đối tượng là khả năng kế thừa. Chúng ta có thể định nghĩa một lớp kế thừa từ một lớp khác dùng từ khóa extends như ví dụ sau:


class Programmer extends Person{

  //….

}

Lúc này, lớp Programmer sẽ thừa kế các thành phần từ lớp Person:


const minh = new Programmer('Minh');

alert(minh.hello());

Chúng ta có thể định nghĩa lại phương thức hello() trong lớp Programmer:


class Programmer extends Person{

  hello() {

    return super.hello() + " I am a programmer."

  }

}

Ở đây chúng ta đã dùng từ khóa super để tham chiếu đến lớp cha Person.

Thực thi với CodePen. 

Lời kết cho phần 1

Trong phần 1 này chúng ta đã cùng tìm hiểu các nét mới trong ngôn ngữ JavaScript hiện đại kể từ ES6 như cách khai báo biến với let và const, hàm mũi tên, xử lý chuỗi với template literals và interpolation, và lập trình hướng đối tượng. Tất cả đều nhằm mang lại sự đơn giản và hiệu quả cho người lập trình. Trong phần tiếp theo, chúng ta sẽ tìm hiểu về lập trình không đồng bộ trong JS với promise và async/await – một trong những nội dung khó hiểu và lý thú nhất trong JS.