Modern JavaScript Cheatsheet (Part 1)
Trong JavaScript, có 3 keyword có thể dùng để khai báo biến và mỗi keyword lại mang ý nghĩa khác nhau. Đó là var, let và const. Giải thích ngắn gọn Các biến được khai báo bằng keyword const không thể được gán lại giá trị, trong khi các biến được khai báo bằng let và var thì có thể. Tôi gợi ý là ...
Trong JavaScript, có 3 keyword có thể dùng để khai báo biến và mỗi keyword lại mang ý nghĩa khác nhau. Đó là var, let và const.
Giải thích ngắn gọn
Các biến được khai báo bằng keyword const không thể được gán lại giá trị, trong khi các biến được khai báo bằng let và var thì có thể. Tôi gợi ý là mặc định thì chúng ta nên khai báo biến bằng const còn khai báo biến bằng let nếu chúng ta cần thay đổi giá trị của biến đó (mutate hoặc reassign) về sau.
Scope | Reassignable | Mutable | Temporal Dead Zone | |
---|---|---|---|---|
const | Block | No | Yes | Yes |
let | Block | Yes | Yes | Yes |
var | Function | Yes | Yes | No |
Ví dụ:
const person = "Nick"; person = "John" // Sẽ báo lỗi, biến person không thể được gán lại giá trị
let person = "Nick"; person = "John"; console.log(person) // "John", biến được khai báo bởi let có thể được gán lại giá trị
Giải thích chi tiết
Scope của 1 biến được định nghĩa là phạm vi mà biến đó có thể được sử dụng ở trong code.
var
Các biến được khai báo bằng var là function scoped, nghĩa là khi biến đó được khai báo trong hàm thì tất cả mọi thứ trong hàm đều có thể truy cập được giá trị của nó. Ngược lại, biến đó không thể được truy cập từ bên ngoài hàm. Ví dụ:
function myFunction() { var myVar = "Nick"; console.log(myVar); // "Nick" - myVar có thể được truy cập ở bên trong hàm } console.log(myVar); // Báo lỗi ReferenceError, myVar không thể được truy cập từ bên ngoài hàm
Dưới đây là 1 ví dụ khác:
function myFunction() { var myVar = "Nick"; if (true) { var myVar = "John"; console.log(myVar); // "John" // myVar có phạm vi sử dụng trong hàm, chúng ta vừa mới xoá giá trị cũ của nó ("Nick") và thay bằng giá trị mới ("John") } console.log(myVar); // "John" - đoạn xử lý trong block if đã thay đổi giá trị của biến } console.log(myVar); // Báo lỗi ReferenceError, myVar không thể được truy cập từ bên ngoài hàm
Ngoài ra, các biến được khai báo bằng var sẽ được chuyển lên trên cùng của scope khi chạy code. Điều này được gọi là var hoisting. Đoạn code này
console.log(myVar) // undefined -- không báo lỗi var myVar = 2;
sẽ được hiểu là
var myVar; console.log(myVar) // undefined -- không báo lỗi myVar = 2;
let
var và let khá giống nhau nhưng các biến được khai báo bằng let thì
- là block scoped
- không thể được truy cập trước khi được gán trá trị
- không thể được khai báo lại trong cùng 1 scope.
Ví dụ:
function myFunction() { let myVar = "Nick"; if (true) { let myVar = "John"; console.log(myVar); // "John" // myVar có phạm vi sử dụng trong block, chúng ta vừa mới tạo ra 1 biến myVar mới // biến myVar này không thể được truy cập từ bên ngoài block này và hoàn toàn độc lập với biến myVar mà chúng ta tạo lúc đầu } console.log(myVar); // "Nick - đoạn xử lý trong block if đã KHÔNG thay đổi giá trị của biến } console.log(myVar); // Báo lỗi ReferenceError, myVar không thể được truy cập từ bên ngoài hàm
Các biến được khai báo bằng let (hoặc const) không thể được truy cập trước khi được gán trá trị:
console.log(myVar) // Báo lỗi ReferenceError ! let myVar = 2;
Trái ngược với các biến khai báo bằng var, sẽ xảy ra lỗi nếu bạn cố đọc hay ghi các biến khai bảo bởi let (hoặc const) trước khi chúng được gán giá trị. Hiện tượng này được gọi là Temporal dead zone hoặc TDZ.
Một chú ý nữa là bạn không thể khai báo lại 1 biến đã được khai báo bằng let.
let myVar = 2; let myVar = 3; // Báo lỗi SyntaxError
const
Các biến được khai báo bằng const thì cũng giống các biến được khai báo bằng let, ngoài ra thì chúng không thể được gán lại giá trị. Điều đó có nghĩa là chúng có các đặc điểm sau:
- là block scoped
- không thể được truy cập trước khi được gán trá trị
- không thể được khai báo lại trong cùng 1 scope
- không thể được gán lại giá trị.
const myVar = "Nick"; myVar = "John" // Báo lỗi, không thể gán lại giá trị cho biến
const myVar = "Nick"; const myVar = "John" // Báo lỗi, không thể khai báo lại biến
Có 1 điểm cần chú ý ở đây: các biến được khai báo bằng const không phải là immutable. Cụ thể hơn thì object hoặc array khai báo bằng const có thể bị thay đổi giá trị.
Ví dụ với object:
const person = { name: 'Nick' }; person.name = 'John' // Không báo lỗi! Biến person không bị gán lại giá trị hoàn toàn mà chỉ bị thay đổi giá trị console.log(person.name) // "John" person = "Sandra" // Báo lỗi vì không thể gán lại giá trị cho biến khai báo bằng const
Ví dụ với array:
const person = []; person.push('John'); // Không báo lỗi! Biến person không bị gán lại giá trị hoàn toàn mà chỉ bị thay đổi giá trị console.log(person[0]) // "John" person = ["Nick"] // Báo lỗi vì không thể gán lại giá trị cho biến khai báo bằng const
Tham khảo
- http://wesbos.com/javascript-scoping/
- Temporal Dead Zone (TDZ) Demystified
Bản update ES6 JavaScript đã giới thiệu arrow function - 1 cách khác để khai báo và sử dụng hàm trong JavaScript. Arrow function mang đến những lợi ích như
- Ngắn gọn, súc tích hơn
- this có thể được lấy từ ngữ cảnh bao quanh
- return ngầm (implicit return)
Ví dụ
- Ngắn gọn và return ngầm
function double(x) { return x * 2; } // Cách truyền thống console.log(double(2)) // 4
const double = x => x * 2; // Cùng 1 hàm nhưng viết dưới dạng arrow function với implicit return console.log(double(2)) // 4
- Tham chiếu this
Ở trong arrow function, this bằng với this của ngữ cảnh bao quanh. Với arrow function, bạn không cần phải dùng tới trò that = this trước khi gọi 1 hàm trong 1 hàm khác nữa.
function myFunc() { this.myVar = 0; setTimeout(() => { this.myVar++; console.log(this.myVar) // 1 }, 0); }
Giải thích chi tiết
Ngắn gọn, súc tích
Arrow function ngắn gọn hơn function truyền thống theo nhiều cách khác nhau. Hãy cùng xem qua các trường hợp dưới đây.
Implicit và Explicit return
Explicit return là khi chúng ta sử dụng keyword return trong một hàm.
function double(x) { return x * 2; // Hàm này trả về x * 2 một cách rõ ràng thông qua việc sử dụng keyword return }
Trong cách viết hàm truyền thống, return luôn là explicit nhưng đối với arrow function chúng ta có thể implicit return - trả về giá trị mà không cần sử dụng keyword return.
const double = (x) => { return x * 2; // Explicit return }
Vì hàm này chỉ return giá trị (không xử lý gì trước keyword return) nên chúng ta có thể viết theo kiểu implicit return
const double = (x) => x * 2; // Trả về x*2
Chúng ta chỉ cần bỏ cặp dấu {} và keyword return. Đó cũng là lí do vì sao chúng ta gọi đó là return ngầm, tuy không có keyword return nhưng hàm này vẫn trả về x*2.
Chú ý là nếu bạn muốn return ngầm 1 object thì phải có dấu ngoặc () xung quanh nó.
const getPerson = () => ({ name: "Nick", age: 24 }) console.log(getPerson()) // { name: "Nick", age: 24 } -- object được return ngầm bởi arrow function
Khi hàm có 1 tham số
Khi hàm chỉ có 1 tham số chúng ta có thể bỏ cặp dấu () xung quanh tham số. Ví dụ với hàm double ở trên
const double = (x) => x * 2;
chúng ta có thể viết thành
const double = x => x * 2;
Khi hàm không có tham số
Khi hàm không có tham số thì chúng ta bắt buộc phải dùng cặp dấu () xung quanh tham số nếu không sẽ bị lỗi cú pháp.
() => { // Có dấu ngoặc, cú pháp hợp lệ const x = 2; return x; }
=> { // Không có dấu ngoặc, cú pháp không hợp lệ const x = 2; return x; }
Tham chiếu this
Ở trong 1 arrow function, this bằng với this của ngữ cảnh bao quanh. Điều đó có nghĩa là 1 arrow function không tạo ra this mới mà nó tự lấy từ xung quanh nó.
Nếu không có arrow function, khi muốn truy cập đến biến của this trong 1 hàm nằm trong hàm khác, chúng ta phải dùng đến trò that = this hoặc self = this.
Ví dụ sử dụng hàm setTimeout trong hàm myFunc:
function myFunc() { this.myVar = 0; var that = this; // that = this trick setTimeout( function() { // 1 this mới được tạo ra trong phạm vi hàm này that.myVar++; console.log(that.myVar) // 1 console.log(this.myVar) // undefined - xem lại khai báo hàm ở phía trên }, 0 ); }
Với arrow function, this sẽ được lấy từ ngữ cảnh bao quanh:
function myFunc() { this.myVar = 0; setTimeout( () => { // this lấy từ ngữ cảnh bao quanh (myFunc) this.myVar++; console.log(this.myVar) // 1 }, 0 ); }
Tham khảo
- Arrow functions introduction - WesBos
- JavaScript arrow function - MDN
- Arrow function and lexical this
Kể từ JavaScript ES2015, chúng ta có thể set giá trị mặc định cho tham số của hàm bằng cú pháp dưới đây
function myFunc(x = 10) { return x; } console.log(myFunc()) // 10 -- không có giá trị được truyền vào nên x được gán giá trị mặc định 10 console.log(myFunc(5)) // 5 -- có giá trị được truyền vào nên x được gán giá trị 5 console.log(myFunc(undefined)) // 10 -- giá trị undefined được truyền vào nên x được gán giá trị mặc định 10 console.log(myFunc(null)) // null -- giá trị null được truyền vào, xem chi tiết ở dưới
Giá trị mặc định của tham số được sử dụng trong 2 trường hợp:
- Không có giá trị được truyền vào hàm
- Giá trị undefined được truyền vào hàm
Nếu bạn truyền vào giá trị null thì giá trị mặc định của tham số cũng không được sử dụng đến.
Tham khảo
- Default parameter value - ES6 Features
- Default parameters - MDN
Destructuring là 1 cách thuận tiện để tạo ra các biến mới bằng cách tách giá trị được lưu trữ trong object hoặc array.
Object
Hãy cùng xem xét object sau
const person = { firstName: "Nick", lastName: "Anderson", age: 35, sex: "M" }
Khi không dùng destructuring:
const first = person.firstName; const age = person.age; const city = person.city || "Paris";
Khi dùng destructuring:
const { firstName: first, age, city = "Paris" } = person; console.log(age) // 35 -- Một biến age mới đã được tạo ra và có giá trị bằng với person.age console.log(first) // "Nick" -- Một biến first mới đã được tạo ra và có giá trị bằng với person.firstName console.log(firstName) // ReferenceError -- person.firstName tồn tại nhưng biến mới được tạo tên là first console.log(city) // "Paris" -- Một biến city mới đã được tạo ra. Vì person.city là undefined, city nhận giá trị mặc định "Paris"
Chú ý: Trong const { age } = person; thì cặp dấu {} không dùng để khai báo 1 object hay 1 block mà nó là cú pháp destructuring.
Tham số hàm
Destructuring thường được dùng để tách object truyền vào hàm thành các biến.
Khi không dùng destructuring:
function joinFirstLastName(person) { const firstName = person.firstName; const lastName = person.lastName; return firstName + '-' + lastName; } joinFirstLastName(person); // "Nick-Anderson"
Khi dùng destructuring chúng ta có 1 hàm ngắn gọn hơn:
function joinFirstLastName({ firstName, lastName }) { // Chúng ta tạo 2 biến mới firstName và lastName bằng cách tách tham số person return firstName + '-' + lastName; } joinFirstLastName(person); // "Nick-Anderson"
Destructuring cũng có thể được sử dụng cùng với arrow function:
const joinFirstLastName = ({ firstName, lastName }) => firstName + '-' + lastName; joinFirstLastName(person); // "Nick-Anderson"
Array
Ví dụ với array dưới
const myArray = ["a", "b", "c"];
Khi không dùng destructuring:
const x = myArray[0]; const y = myArray[1];
Khi dùng destructuring:
const [x, y] = myArray; console.log(x) // "a" console.log(y) // "b"
Tham khảo
- ES6 Features - Destructuring Assignment
- Destructuring Objects - WesBos
- ExploringJS - Destructuring
map, filter, reduce là những array method bắt nguồn từ 1 mẫu hình lập trình có tên gọi functional programming.
Giới thiệu khái quát thì
- Array.prototype.map() nhận vào 1 mảng, tiến hành xử lí gì đó với các phần tử của mảng và trả về 1 mảng với các phần tử đã được xử lí.
- Array.prototype.filter() nhận vào 1 mảng, kiểm tra và quyết định xem có giữ lại từng phần tử của mảng hay không, kết quả trả về là 1 mảng chỉ bao gồm những phần tử được giữ lại.
- Array.prototype.reduce() nhận vào 1 mảng và gộp các phần tử của mảng thành 1 giá trị duy nhất rồi trả về giá trị đó.
Với 3 method này chúng ta có thể tránh việc sử dụng vòng lặp for hoặc forEach trong hầu hết các trường hợp.
Ví dụ
const numbers = [0, 1, 2, 3, 4, 5, 6]; const doubledNumbers = numbers.map(n => n * 2); // [0, 2, 4, 6, 8, 10, 12] const evenNumbers = numbers.filter(n => n % 2 === 0); // [0, 2, 4, 6] const sum = numbers.reduce((prev, next) => prev + next, 0); // 21
Ví dụ khác: tính tổng điểm của những học sinh có điểm trên 10 bằng cách kết hợp map, filter và reduce
const students = [ { name: "Nick", grade: 10 }, { name: "John", grade: 15 }, { name: "Julia", grade: 19 }, { name: "Nathalie", grade: 9 }, ]; const aboveTenSum = students .map(student => student.grade) // map mảng students thành 1 mảng chứa điểm của những học sinh đó .filter(grade => grade >= 10) // filter mảng chứa điểm và giữ lại những điểm trên 10 .reduce((prev, next) => prev + next, 0); // tính tổng của những điểm trên 10 console.log(aboveTenSum) // 44 -- 10 (Nick) + 15 (John) + 19 (Julia), Nathalie có điểm dưới 10 nên không tính
Giải thích
Chúng ta sẽ sử dụng mảng sau trong các các ví dụ dưới đây
const numbers = [0, 1, 2, 3, 4, 5, 6];
Array.prototype.map()
const doubledNumbers = numbers.map(function(n) { return n * 2; }); console.log(doubledNumbers); // [0, 2, 4, 6, 8, 10, 12]
Ở đây chúng ta sử dụng map đối với mảng numbers, nó duyệt qua từng phần tử của mảng và truyền phần tử đó vào hàm. Mục đích của hàm là nhân phần tử được truyền vào với 2 và trả về giá trị mới.
Chúng ta có thể viết lại ví dụ này cho rõ ràng hơn như sau:
const doubleN = function(n) { return n * 2; }; const doubledNumbers = numbers.map(doubleN); console.log(doubledNumbers); // [0, 2, 4, 6, 8, 10, 12]
numbers.map(doubleN) trả về [doubleN(0), doubleN(1), doubleN(2), doubleN(3), doubleN(4), doubleN(5), doubleN(6)] tức là bằng với [0, 2, 4, 6, 8, 10, 12].
Ngoài ra, chúng ta có thể sẽ thường thấy method này dùng chung với arrow function
const doubledNumbers = numbers.map(n => n * 2); console.log(doubledNumbers); // [0, 2, 4, 6, 8, 10, 12]
Array.prototype.filter()
const evenNumbers = numbers.filter(function(n) { return n % 2 === 0; }); console.log(evenNumbers); // [0, 2, 4, 6]
Ở đây chúng ta sử dụng filter đối với mảng numbers, nó duyệt qua từng phần tử của mảng và truyền phần tử đó vào hàm. Mục đích của hàm là quyết định xem phần tử được truyền vào có được giữ lại trong mảng hay không (nếu là số chẵn thì giữ lại). Sau đó filter trả về 1 mảng mới chỉ gồm các phần tử đã được giữ lại.
Tương tự như map, filter cũng thường được dùng chung với arrow function
const evenNumbers = numbers.filter(n => n % 2 === 0); console.log(evenNumbers); // [0, 2, 4, 6]
Array.prototype.reduce()
Mục đích của method reduce là gộp các phần tử của mảng mà nó duyệt qua thành 1 giá trị duy nhất. Gộp như thế nào thì phụ thuộc vào cách chúng ta quy định.
const sum = numbers.reduce( function(acc, n) { return acc + n; }, 0 // giá trị của biến tích lũy acc ở lần lặp đầu tiên ); console.log(sum) //21
Giống như method map và filter, reduce được sử dụng với 1 mảng và nhận vào tham số thứ nhất là 1 hàm. Tuy nhiên có 1 điểm khác biệt đó là reduce nhận vào 2 tham số
- Tham số đầu tiên là 1 hàm sẽ được gọi ở mỗi lần lặp,
- Tham số thứ 2 là giá trị của biến tích lũy (acc trong ví dụ trên) ở lần lặp đầu tiên.
Hàm được truyền vào reduce cũng nhận vào 2 tham số. Tham số đầu tiên (acc) là biến tích lũy, tham số thứ 2 là phần tử hiện tại (n). Giá trị của biến tích lũy bằng với giá trị trả về của hàm ở lần lặp trước đó. Ở lần lặp đầu tiên acc bằng với giá trị của tham số thứ 2 được truyền vào method reduce.
Tham khảo
- Understanding map / filter / reduce in JS
Spread operator và rest operator có cùng cú pháp ... và được giới thiệu từ ES2015. Spread operator được sử dụng để mở rộng 1 iterable (ví dụ như 1 mảng) thành nhiều phần tử còn rest operator thì ngược lại, nó được sử dụng để gom nhiều phần tử thành 1 iterable.
Ví dụ
const arr1 = ["a", "b", "c"]; const arr2 = [...arr1, "d", "e", "f"]; // ["a", "b", "c", "d", "e", "f"]
function myFunc(x, y, ...params) { console.log(x); console.log(y); console.log(params) } myFunc("a", "b", "c", "d", "e", "f") // "a" // "b" // ["c", "d", "e", "f"]
const { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }; console.log(x); // 1 console.log(y); // 2 console.log(z); // { a: 3, b: 4 } const n = { x, y, ...z }; console.log(n); // { x: 1, y: 2, a: 3, b: 4 }
Giải thích
Iterable
Nếu chúng ta có 2 mảng như dưới
const arr1 = ["a", "b", "c"]; const arr2 = [arr1, "d", "e", "f"]; // [["a", "b", "c"], "d", "e", "f"]
Phần tử đầu tiên của arr2 là 1 mảng bởi arr1 được đưa nguyên vào arr2. Nếu chúng ta muốn arr2 là mảng các kí tự thì phải làm thế nào? Với spread operator chúng ta có thể làm như sau
const arr1 = ["a", "b", "c"]; const arr2 = [...arr1, "d", "e", "f"]; // ["a", "b", "c", "d", "e", "f"]
Function rest parameter
Ở tham số hàm, chúng ta có thể dùng rest operator để gom nhiều phần tử thành 1 mảng. Hãy cùng xem ví dụ sau:
function myFunc() { for (var i = 0; i < arguments.length; i++) { console.log(arguments[i]); } } myFunc("Nick", "Anderson", 10