Ở kỳ này mình sẽ viết về JavaScript ES6, ngôn ngữ mình sử dụng nhiều thứ 2 chỉ sau ngôn ngữ yêu thích nhất của mình là C#. Mình dùng C# để code back-end và còn JavaScript thì code front-end. Cá nhân mình thấy JavaScript là một ngôn ngữ có khá là nhiều nhược điểm với nhiều khái niệm khó hiểu nếu đi sâu tìm hiểu, nhưng thời thế như thế nào mà giờ lại trở thành ngôn ngữ hot bậc nhất khi làm Web.
Mặc dù gần đây thì mọi người có nhắc đến nền tảng WebAssembly, Microsoft có sử dụng nền tảng này để code C# có thể chạy trên browser. Ngoài ra C/C++ hay Kotlin, Go cũng đều được WebAssembly hỗ trợ, do đó ta có nhiều lựa chọn ngôn ngữ cho client-side. Tuy nhiên với việc JavaScript đang thống lĩnh thị trường thì có lẽ WebAssembly còn rất lâu hoặc sẽ chẳng bao giờ thay thế được JavaScript.
JavaScript vốn có nhiều nhược điểm như phạm vi của biến thì global, không có scope rồi cũng không strong-typed, và có nhiều thứ khó hiểu và khó làm chủ như biến this, curry function. Nhưng do mức độ sử dụng của JavaScript là quá lớn, nên những năm gần đây, JavaScript liên tục được cải tiến để khắc phục các nhược điểm này. Và trong bài viết này, mình sẽ đề cập tới những tính năng mới trong JavaScript ES6 hay thường được gọi là Modern JavaScript.
Một số tính năng hay và nổi bật bao gồm
Block scope
Để hiểu về block scope trong JavaScript, ta xét đoạn code sau.
{ // Block scope var a = 10; } if (true) { // Block scope console.log(a); } for (var i = 0; i <= 10; i++) { // Block scope } console.log(i); function sum(a, b) { // Function scope var result = a + b; } sum(4, 3); console.log(result);
Chạy đoạn code trên và đây là kết quả.
10
11
Uncaught ReferenceError: result is not defined
Bạn có thể thấy biến a khi khai báo biến trong JavaScript thì dù biến có nằm trong một khối lệnh (block scope) thì giá trị của nó vẫn có thể truy cập từ một khối lệnh khác. Hoặc như biến i dù nằm trong khối lệnh for nhưng vẫn có thể được truy xuất từ global. Duy chỉ có biến result nằm trong hàm thì không truy xuất được.
Thực tế việc biến luôn có phạm vi hoạt động global như vậy gây rất phiền toái bởi khi có nhiều file khác nhau trong project thì rất dễ nhầm lẫn khi đặt tên biến trùng nhau. JavaScript thì không báo lỗi khi có nhiều biến trùng tên , và như vậy sẽ rất khó debug khi có lỗi xảy ra.
Trong ES6 ta có từ khóa let để hạn chế phạm vi hoạt động của biến, điều này giúp cho ta dễ dàng kiểm soát giống nhiều ngôn ngữ khác như C# hay Java.
let a = 10;
Trong trường hợp này, phạm vi hoạt động của biến a sẽ bị thu gọn lại bên trong dấu ngoặc { }.
Tiếp theo ta thay từ khóa var của biến i bằng từ khóa let
for (let i = 0; i <= 10; i++) {
Lúc này, JavaScript chạy đến dòng lệnh console.log(i) để gọi ra giá trị i nhưng do i lúc này không thể truy xuất nên sẽ bị lỗi và không chạy tiếp nữa. Kết quả ta thu được là
Uncaught ReferenceError: i is not defined
Trong ES6 còn có một từ khóa nữa là const, mọi người đều biết khai báo const để giá trị đó là hằng số và sẽ không được phép thay đổi. Thực tế thì không hoàn toàn như vậy. Với những giá trị là Scalar value (ví dụ như number hay string) thì không thể thay đổi nhưng với array hay object thì giá trị vẫn có thể thay đổi.
// Scalar values const score = 10; const say = "Hello world"; score = 20; // Báo lỗi tại run-time: Uncaught TypeError: Assignment to constant variable. say = "Bye world";
const numbers = [1, 2, 3]; const person = { name: "Peter", age: 20, }; numbers.push(4); // Biến numbers trở thành [1, 2, 3, 4] person.age = 30; // Biến Person sẽ trở thành {name: "Peter", age: 30}
Thực tế khi khai báo const với những giá trị như array hay object, bạn không thể thay đổi được reference của nó chứ không phải value của nó. Nếu bạn gán reference mới cho number như sau: numbers = [], thì sẽ không được vì như vậy bạn đang thay đổi reference của một const nhưng nếu thêm một giá trị số 4 như trên thì hoàn toàn có thể. Đây là một lưu ý khi sử dụng const.
Arrow function
Arrow functionlà một cải tiến rất hay của JavaScipt. Thay vì phải viết là
function sum(a, b) { return a + b; }
Ta có thể khai báo đơn giản như sau.
const sum = (a, b) => a + b;
Viết như vậy không chỉ là ngắn hơn mà còn dễ kiểm soát hơn khi làm việc với closure. Arrow function không quan tâm cái gì gọi nó trong khi function bình thường quan tâm tới cái gì gọi nó. Để dễ hiểu ta xét ví dụ sau.
this.id = "Global"; const test = { test1: function () { console.log("test1", this); }, test2: () => { console.log("test2", this); }, }; test.test1(); test.test2();
test1 { test1: [Function: test1], test2: [Function: test2] }
test2 { id: 'Global' }
Ở câu lệnh đầu tiên test.test1(), this trong hàm test1 này được xác định dựa vào cái gì gọi nó, và đối tượng test chính là cái đã gọi đến hàm test1, do vậy this hiển thị ra trong console log chính là đối tượng test. Còn câu lệnh test.test2(), this ở đây không phải là đối tượng test mà là module global của JavaScript. Từ khóa this là vấn đề khá khó trong JavaScript, nếu bạn chưa mượng tượng rõ cũng không sao, qua thời gian làm việc bạn sẽ quen dần, hiện tại bạn chỉ cần biết điều này giúp cho ta rất nhiều khi làm việc với event mà không lo bị loạn biến this.
Object literal
Trong JavaScript có một vài cách để khởi tạo một object. Tuy nhiên Object literal có lẽ hiện tại được dùng nhiều hơn cả do cú pháp đơn giản của nó.
const dynamic = "nickname"; const age = 20; const person = { name: "Peter", [dynamic]: "Spider man", age, // không cần viết là age: age }; console.log(person);
{ name: 'Peter', nickname: 'Spider man', age: 20 }
Đoạn code trên không có gì đặc biệt ngoài khai báo và in ra biến person. Duy có 2 điều lưu ý nhỏ, thứ nhất, object literal hỗ trợ dynamic property, điều đó có nghĩa cái tên property của person trong trường hợp này là nickname hoàn toàn có thể là một giá trị được user nhập vào rồi set cho nó, và ta có thể truy xuất bằng person[dynamic]. Còn với thuộc tính age, nếu viết đầy đủ để khởi tạo thì phải là age: age, nhưng do biến age trùng tên với thuộc tính age luôn, trong ES6 ta có thể viết rút ngắn gọn như trên, tính năng này được gọi là object shorthand syntax.
Destructuring và Rest/Spread
Destructuring là cú pháp cho phép gán các thuộc tính của một Object hoặc một Array. Điều này giúp bạn giảm thiểu đáng kể lượng code cần viết để thao tác với dữ liệu. Có hai loại Destructuring: Destructuring Objects và Destructuring Arrays.
const person = { name: "Peter", age: 20, gender: true, }; const { name, age } = person; const [first, second, ...rest] = [1, 2, 3, 4, 5]; console.log(`${name} - ${age} - ${first} - ${second} - ${rest}`);
Peter - 20 - 1 - 2 - 3,4,5
Với cú pháp destructuring như trên, ta vô cùng dễ dàng lấy được 2 thuộc tính name và age của person. Tương tự với array thì các giá trị lần lượt được gán vào first và second. Còn dấu 3 chấm trước rest thì sao, đó là rest param trong ES6, nếu như ta muốn lấy toàn bộ các giá trị còn lại vào biến rest. Rest param khá dễ bị nhầm lẫn với spread param vì đều sử dụng syntax ba chấm + tên biến. Tuy nhiên ứng dụng của chúng là khác nhau.
- Rest Parameter lấy tất cả các phần tử còn lại cho vào trong một mảng.
- Spread Operator lấy tất cả phần tử có trong một tập hợp ví dụ như mảng, gộp thành một phần tử duy nhất.
Dưới đây là ví dụ về Spread.
const array = ["Banana", "Watermelon", "Peachy"]; console.log(array); // [ 'Banana', 'Watermelon', 'Peachy' ] console.log(...array); // Banana Watermelon Peachy
Ta thấy ở dòng log thứ hai, mảng đã được gộp lại thành một chuỗi string duy nhất.
Ứng dụng của spread operator hay dùng có thể kể đến là update nhanh chóng một object như sau.
const person = { name: "Peter", age: 20, gender: true, }; const oldPerson = { ...person, age: 70, extraInfo: "Need treatment", }; console.log(oldPerson); // { name: 'Peter', age: 70, gender: true, extraInfo: 'Need treatment' }
Như vây, ta có thể nhanh chóng cập nhật giá trị cho thuộc tính hoặc thêm thuộc tính bằng cách sử dụng spread operator.
Template string
Bạn có thể viết string trong JavaScript bằng dấu quote kép hoặc đơn như sau.
const name = "John"; let question = "How are you, " + name + "?"; let answer = 'I am fine. Thank you.';
Với cách trên nếu muốn nối vào một biến thì ta phải cộng chuỗi. ES6 có một cách khác để ta thao tác dễ dàng hơn. Đó là dùng template string. Với cách này ta còn có thể sử dụng string trên nhiều dòng một cách dễ dàng.
let question = `How are you? ${name}? Long time no see.`;
Class
Lập trình hướng đối tượng hiện đang thống trị ngành lập trình, và sẽ là thiếu sót lớn nếu JavaScript không có nổi một từ khóa class để sử dụng. Do vậy trong phiên bản mới của JavaScript, từ khóa class đã ra đời.
class Person { constructor(name) { this.name = name; } say() { console.log(`Hello, I am ${this.name}.`); } } class Student extends Person { constructor(name, age) { super(name); this.age = age; } say() { console.log(`Hello, I am ${this.name}, and I am ${this.age} years old.`); } } const p = new Person("Peter"); const s = new Student("Jane", 20); p.say(); s.say();
Hello, I am Peter.
Hello, I am Jane, and I am 20 years old.
Bên trên là đoạn code tiêu biểu cho việc khai báo một class của JavaScript. Về cơ bản cú pháp khá giống với ngôn ngữ Java. Tuy nhiên có một vài điểm khác biệt. Thuộc tính cần được khai báo trong constructor. Từ khóa this là cần thiết để JavaScript biết được thuộc tính đó được gắn với đối tượng nào. Một lưu ý nữa là đối với class thì sẽ không có khái niệm hoisting như function. Tức là với function ta có thể viết dòng khai báo sau dòng sử dụng, nhưng với class ta bắt buộc phải định nghĩa class trước rồi sau đó mới có thể sử dụng.
Summary
Trong bài viết này mình đã đề cập đến một số điểm mới trong ES6 mà theo mình thấy là sẽ ứng dụng rất nhiều. Với những cải tiến này, JavaScript đang ngày một trở nên hoàn thiện, đồng thời việc viết code cũng trở nên đơn giản và hiệu quả hơn rất nhiều. Trước đây có ít trình duyệt chạy được, phải dùng Babel để chuyển đổi để tương thích thì nay ES6 đã được hầu hết các trình duyệt hỗ trợ nên hầu như các bạn sẽ không gặp phải vấn đề gì, bạn chỉ việc viết code và chạy thôi.