Modern JavaScript Cheatsheet (Part 1)
Bài đăng này đã không được cập nhật trong 6 năm
Khai báo biến: var
, const
, let
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
Arrow function
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
Giá trị mặc định của tham số hàm
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
Destructuring object và array
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
Array methods: map
, filter
, reduce
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
Spread operator, rest operator
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, 12, 6);
// "Nick"
// "Anderson"
// 10
// 12
// 6
Javascript cung cấp sẵn 1 object arguments
cho mỗi hàm và nó chứa tất cả các tham số được truyền vào hàm.
Giả sử bây giờ chúng ta muốn hàm này trả về 1 học sinh với thông tin về nhiều điểm số và điểm trung bình. Khi đó sẽ tiện hơn nếu chúng ta tách 2 tham số đầu tiên thành 2 biến riêng biệt và tách các tham số còn lại thành 1 mảng mà chúng ta có thể duyệt qua. Đó chính xác là những gì rest operator
cho phép chúng ta làm.
function createStudent(firstName, lastName, ...grades) {
// firstName = "Nick"
// lastName = "Anderson"
// [10, 12, 6] -- "..." nhận tất cả các tham số còn lại và tạo ra 1 mảng "grades" chứa chúng
const avgGrade = grades.reduce((acc, curr) => acc + curr, 0) / grades.length; // computes average grade from grades
return {
firstName: firstName,
lastName: lastName,
grades: grades,
avgGrade: avgGrade
}
}
const student = createStudent("Nick", "Anderson", 10, 12, 6);
console.log(student);
// {
// firstName: "Nick",
// lastName: "Anderson",
// grades: [10, 12, 6],
// avgGrade: 9,33
// }
Object properties spreading
const myObj = { x: 1, y: 2, a: 3, b: 4 };
const { x, y, ...z } = myObj; // object destructuring
console.log(x); // 1
console.log(y); // 2
console.log(z); // { a: 3, b: 4 }
// z là phần còn lại của object đã được destructure
const n = { x, y, ...z };
console.log(n); // { x: 1, y: 2, a: 3, b: 4 }
// Ở đây các thuộc tính của z được mở rộng vào n
Tham khảo
- TC39 - Object rest/spread
- Spread operator introduction - WesBos
- JavaScript & the spread operator
- 6 Great uses of the spread operator
Object property shorthand
Khi gán 1 biến vào 1 thuộc tính của 1 object, nếu tên biến trùng với tên của thuộc tính thì chúng ta có thể viết ngắn gọn như sau
const x = 10;
const myObj = { x };
console.log(myObj.x) // 10
Giải thích
Trước ES2015 thì khi chúng ta muốn khai báo 1 object và gán các biến vào các thuộc tính chúng ta thường phải viết
const x = 10;
const y = 20;
const myObj = {
x: x, // gán giá trị của biến x cho myObj.x
y: y // gán giá trị của biến y cho myObj.y
};
console.log(myObj.x) // 10
console.log(myObj.y) // 20
Cách viết này hơi trùng lặp vì ở đây tên thuộc tính trùng với tên biến. Với ES2015 chúng ta có thể viết ngắn gọn lại:
const x = 10;
const y = 20;
const myObj = {
x,
y
};
console.log(myObj.x) // 10
console.log(myObj.y) // 20
Tham khảo
Template literals
Template literal là 1 cú pháp mới cho phép nội suy biểu thức JavaScript trong string.
Ví dụ
const name = "Nick";
`Hello ${name}, the following expression is equal to four : ${2+2}`;
// Hello Nick, the following expression is equal to four: 4
Tham khảo
Tagged template literals
Template tag là 1 function có thể được gắn trước 1 template literal. Khi function đó được gọi thì tham số đầu tiên là 1 mảng các string đứng xen kẽ các biến nội suy trong templage còn các tham số tiếp theo là các giá trị của các biến nội suy.
Ví dụ
function highlight(strings, ...values) {
const interpolation = strings.reduce((prev, current) => {
return prev + current + (values.length ? "<mark>" + values.shift() + "</mark>" : "");
}, "");
return interpolation;
}
const condiment = "jam";
const meal = "toast";
highlight`I like ${condiment} on ${meal}.`;
// "I like <mark>jam</mark> on <mark>toast</mark>."
// giá trị của tham số strings trong hàm highlight là ["I like ", " on ", "."]
// giá trị của tham số values trong hàm highlight là ["jam", "toast"]
1 ví dụ khác hay ho hơn:
function comma(strings, ...values) {
return strings.reduce((prev, next) => {
let value = values.shift() || [];
value = value.join(", ");
return prev + next + value;
}, "");
}
const snacks = ['apples', 'bananas', 'cherries'];
comma`I like ${snacks} to snack on.`;
// "I like apples, bananas, cherries to snack on."
Tham khảo
Nguồn bài viết
All rights reserved