Cẩm nang dành cho người mới bắt đầu JavaScript
JavaScript là một trong những ngôn ngữ lập trình phổ biến nhất trên thế giới. Do đó, mình tin rằng đây là một sự lựa chọn tuyệt vời cho ngôn ngữ lập trình đầu tiên của bạn. Ngoài ra, JavaScript còn được sử dụng trong việc tạo ra các ứng dụng web (từ giao diện cho đến hệ thống dữ liệu), lập trình game, trí tuệ nhân tạo, ứng dụng điện thoại và các chương trình điều khiển các thiết bị ngoại vi. Nói tóm gọn lại là JavaScript có thể làm được mọi thứ. Cẩm nang này được dịch ra từ một quyển sách có tên là "The JavaScript Beginner's Handbook" được thực hiện do lập trình viên Flavio Copes.
JavaScript là gì?
JavaScript là một ngôn ngữ lập trình mà có những yếu tố như:- Bậc cao (high-level): nó cung cấp cho chúng ta những hàm trừu tượng (abstractions) (nói nôm na là những hàm đã được các lập trình viên viết sẵn) nên bạn sẽ không cần phải quan tâm đến việc máy tính sẽ đọc code của bạn như thế nào. Nó sẽ quản lý bộ nhớ một cách tự động với bộ thu gom rác, vì vậy bạn có thể tập trung vào code hơn thay vì quản lý bộ nhớ như các ngôn ngữ khác như C và cung cấp nhiều cấu trúc (constructs) cho phép bạn xử lý các biến (variable) và đối tượng (object) mạnh mẽ.
- Động (dynamic): trái ngược với các ngôn ngữ lập trình tĩnh (static), một ngôn ngữ động thực thi nhiều việc mà một ngôn ngữ tĩnh thực hiện tại thời điểm biên dịch (compile time). Điều này có ưu và nhược điểm, nó cung cấp cho bạn các tính năng mạnh mẽ như gõ động (dynamic typing), ràng buộc muộn (late binding), khả năng phục hồi (reflection), lập trình hàm (functional programming), thay đổi thời gian chạy đối tượng (object runtime alteration), closures và hơn thế nữa. Đừng lo lắng nếu những điều đó bạn chưa biết - bạn sẽ biết tất cả chúng ở cuối cẩm nang này.
- Gõ động (dynamic typing): một biến không thực thi một kiểu (type). Bạn có thể gán lại bất kỳ kiểu nào cho một biến, ví dụ: gán một số nguyên (integer) cho một biến chứa một chuỗi (string).
- Gõ yếu (weak typing): trái ngược với gõ mạnh (strong typing), các ngôn ngữ thuộc gõ yếu không thực thi kiểu đối tượng, cho phép linh hoạt hơn nhưng lại từ chối kiểm tra kiểu (thứ mà TypeScript - xây dựng dựa trên JavaScript - cung cấp)
- Thông dịch (interpreted): nó thường được biết đến như một ngôn ngữ thông dịch, có nghĩa là nó không cần giai đoạn biên dịch (compilation stage) trước khi một chương trình có thể chạy, trái ngược với C, Java hoặc Go chẳng hạn. Trên thực tế, các trình duyệt biên dịch JavaScript trước khi thực thi nó vì lý do hiệu suất.
- Đa hình (multi-paradigm): ngôn ngữ không thực thi bất kỳ mô hình lập trình cụ thể nào, không giống như Java. Ví dụ, Java buộc sử dụng lập trình hướng đối tượng (object-oriented programming) hoặc C buộc lập trình mệnh lệnh (imperative programming). Bạn có thể viết JavaScript bằng cách sử dụng mô hình hướng đối tượng (object-oriented paradigm), sử dụng các nguyên mẫu (prototypes) và cú pháp các lớp (class) mới (kể từ ES6). Bạn có thể viết JavaScript theo kiểu lập trình hàm, với các hàm hạng nhất (first-class function) của nó, hoặc thậm chí theo kiểu mệnh lệnh (giống C).
Lịch sử hình thành
Được tạo ra vào năm 1995, JavaScript đã trải qua một chặng đường rất dài kể từ khởi đầu có phần khiêm tốn của nó.
Đây là ngôn ngữ kịch bản (scripting language) đầu tiên được hỗ trợ bởi các trình duyệt web và nhờ đó nó đã đạt được lợi thế cạnh tranh so với bất kỳ ngôn ngữ nào khác và ngày nay nó vẫn là ngôn ngữ kịch bản duy nhất mà chúng ta có thể sử dụng để xây dựng các ứng dụng web.
Mặc dù các ngôn ngữ khác vẫn đang tồn tại nhưng tất cả đều phải biên dịch sang JavaScript - hoặc gần đây là WebAssembly, nhưng đây là một câu chuyện khác.
Ban đầu, JavaScript gần như không mạnh mẽ như ngày nay và nó chủ yếu được sử dụng cho các hiệu ứng lạ mắt và điều kỳ diệu được biết đến vào thời điểm đó là HTML động (Dynamic HTML).
Với nhu cầu ngày càng tăng mà nền tảng web đòi hỏi (và tiếp tục đòi hỏi), JavaScript cũng có trách nhiệm phát triển để đáp ứng nhu cầu của một trong những hệ sinh thái được sử dụng rộng rãi nhất trên thế giới.
JavaScript hiện cũng được sử dụng rộng rãi bên ngoài trình duyệt. Sự nổi lên của Node.js trong vài năm qua đã mở khóa ra thêm nhiều sự lựa chọn thay vì trước đây chủ yếu là sự thống trị của Java, Ruby, PHP và các ngôn ngữ phía máy chủ truyền thống hơn.
JavaScript hiện cũng là ngôn ngữ cung cấp sức mạnh cho cơ sở dữ liệu và nhiều ứng dụng khác, và thậm chí có thể phát triển các ứng dụng nhúng, ứng dụng dành cho thiết bị di động, ứng dụng TV, v.v. Mới đó thôi chỉ là một ngôn ngữ nhỏ bên trong trình duyệt nhưng hiện giờ lại là ngôn ngữ phổ biến nhất trên thế giới.
Chỉ có thể là JavaScript
Đôi khi thật khó để tách JavaScript ra khỏi các tính năng của môi trường mà nó được sử dụng.
Ví dụ: dòng console.log()
bạn có thể tìm thấy trong nhiều đoạn code ví dụ không phải là JavaScript. Thay vào đó, nó là một phần của thư viện API khổng lồ được cung cấp cho chúng ta trong trình duyệt.
Theo cách tương tự, trên máy chủ, đôi khi có thể khó tách các tính năng của ngôn ngữ JavaScript khỏi các API được cung cấp bởi Node.js.
Một tính năng cụ thể được cung cấp bởi React hay Vue? Hay nó chỉ đơn thuần là "JavaScript thuần" (hay còn gọi là Vanilla JavaScript) như nó thường được gọi?
Trong cẩm nang này, chúng ta sẽ chỉ bàn luận về ngôn ngữ JavaScript.
Mình sẽ không làm phức tạp quá trình học tập của bạn với những thứ bên ngoài nó và được cung cấp bởi các hệ sinh thái khác ở bên ngoài.
Cú pháp
Trong phần giới thiệu nhỏ này, mình muốn nói với bạn về 5 khái niệm:
- Khoảng trắng (white space)
- Phân biệt chữ hoa chữ thường (case sensitivity)
- Giá trị (literals)
- Định danh (identifiers)
- Chú thích (comments)
Khoảng trắng
JavaScript không coi khoảng trắng là có ý nghĩa. Dấu cách và dấu ngắt dòng có thể được thêm vào bất kỳ kiểu nào bạn thích, ít nhất là trên lý thuyết.
Trong thực tế, bạn rất có thể sẽ giữ một phong cách được xác định rõ ràng và tuân theo những gì mọi người thường sử dụng và thực thi điều này bằng cách sử dụng linter hoặc một công cụ tạo kiểu như Prettier.
Ví dụ: mình luôn sử dụng 2 ký tự khoảng trắng cho mỗi thụt đầu dòng.
Phân biệt chữ hoa chữ thường
JavaScript phân biệt chữ hoa chữ thường. Một biến được đặt tên là example
sẽ khác với biến Example
.
Tương tự đối với bất kỳ định danh nào.
Giá trị
Được hiểu theo nghĩa đen là một giá trị được viết trong code, ví dụ: một số, một chuỗi, một boolean hoặc các cấu trúc nâng cao hơn, như Object Literals hoặc Array Literals:
5 'Test' true ['a', 'b'] {color: 'red', shape: 'Rectangle'}
Định danh
Định danh là một chuỗi các ký tự có thể được sử dụng để xác định một biến, một hàm hoặc một đối tượng. Nó có thể bắt đầu bằng một chữ cái, ký hiệu đô la $
hoặc một dấu gạch dưới _
và nó có thể chứa các chữ số. Sử dụng mã Unicode, một chữ cái có thể là bất kỳ ký tự nào được phép, ví dụ: biểu tượng cảm xúc 😀.
Test test TEST _test Test1 $test
Ký hiệu $
thường được sử dụng để tham chiếu đến các phần tử DOM.
Một số tên chỉ được dành riêng cho việc sử dụng trong nội bộ JavaScript và chúng ta không thể sử dụng chúng làm định danh.
Chú thích
Chú thích là một trong những phần quan trọng nhất của bất kỳ chương trình nào, trong bất kỳ ngôn ngữ lập trình nào. Chúng quan trọng vì chúng cho phép chúng ta chú thích code và thêm thông tin quan trọng mà nếu không thì người khác (hoặc chính chúng ta) sẽ không có được khi đọc code.
Trong JavaScript, chúng ta có thể viết chú thích trên một dòng bằng cách sử dụng //
. Mọi thứ sau //
không được trình thông dịch JavaScript coi là code.
Giống như bên dưới:
// a comment true //another comment
Một loại chú thích khác là chú thích nhiều dòng. Nó bắt đầu bằng /*
và kết thúc bằng */
.
Mọi thứ ở giữa sẽ không được coi là code:
/* some kind of comment */
Dấu chấm phẩy
Mọi dòng trong chương trình JavaScript được kết thúc bằng cách sử dụng dấu chấm phẩy.
Bạn có thể sử dụng hoặc không bởi vì trình thông dịch JavaScript đủ thông minh để hiểu được bạn có đang kết thúc dòng lệnh đó hay không.
Trong hầu hết các trường hợp, bạn có thể loại bỏ hoàn toàn dấu chấm phẩy khỏi chương trình của mình mà không cần nghĩ đến nó.
Mặc dù thực tế này đang gây tranh cãi rất nhiều. Một số lập trình viên sẽ luôn sử dụng dấu chấm phẩy, một số khác sẽ không bao giờ sử dụng dấu chấm phẩy và bạn sẽ luôn tìm thấy code có hoặc không sử dụng dấu chấm phẩy.
Sở thích cá nhân của mình là tránh dấu chấm phẩy, vì vậy các ví dụ của mình trong hướng dẫn sẽ không bao gồm nó.
Giá trị
Chuỗi hello
là một giá trị.
Số 12
là một giá trị.
Như vậy chuỗi hello
và số 12
đều là những giá trị. String và number là kiểu lần lượt tương ứng với những giá trị đó.
Kiểu (type) là loại của giá trị. Chúng ta có nhiều kiểu giá trị trong JavaScript và chúng ta sẽ nói chi tiết về chúng ở phần sau. Mỗi kiểu đều có những đặc điểm riêng.
Khi chúng ta cần tham chiếu đến một giá trị, chúng ta sẽ gán nó cho một biến (variable). Biến có thể có tên và giá trị là những gì được lưu trữ trong một biến. Vì vậy, sau này chúng ta có thể truy cập giá trị đó thông qua tên biến.
Biến
Biến là một giá trị được gán cho một mã định danh, vì vậy bạn có thể tham chiếu và sử dụng nó sau này trong chương trình.
Điều này là do JavaScript là kiểu gõ yếu, một khái niệm bạn sẽ thường xuyên nghe đến.
Một biến phải được khai báo trước khi bạn có thể sử dụng nó.
Chúng ta có 2 cách để khai báo biến. Cách đầu tiên là sử dụng từ khoá const
:
const a = 0
Cách thứ hai là sử dụng từ khoá let
:
let a = 0
Có gì khác biệt giữa const
và let
?
const
xác định một tham chiếu không đổi đến một giá trị. Điều này có nghĩa là không thể thay đổi tham chiếu. Bạn không thể gán lại giá trị mới cho nó.
Còn sử dụng let
, bạn có thể gán một giá trị mới cho nó.
Ví dụ, bạn không thể làm điều này:
const a = 0 a = 1
Bởi vì nếu bạn làm điều đó, bạn sẽ nhận được thông báo lỗi như sau: TypeError: Assignment to constant variable.
.
Mặc khác, bạn có thể sử dụng bằng từ khoá let
:
let a = 0 a = 1
const
ở đây không mang nghĩa là "constant" (hằng số) trong những ngôn ngữ khác như C. Đặc biệt, nó không được hiểu là giá trị không thể thay đổi - mà nó được hiểu là giá trị không thể được gán lại. Nếu biến trỏ đến một đối tượng hoặc một mảng (chúng ta sẽ xem thêm về đối tượng và mảng sau) thì nội dung của đối tượng hoặc mảng có thể tự do thay đổi.
Biến được khai báo với từ khoá const
phải có giá trị mặc định.
const a = 0
Nhưng đối với từ khoá let
thì chúng ta có thể đặt giá sau:
let a a = 0
Ngoài ra, bạn có thể khai báo nhiều biến trong 1 câu lệnh như sau:
const a = 1, b = 2 let c = 1, d = 2
Nhưng bạn không thể khai báo cùng một biến quá 2 lần:
let a = 1 let a = 2
Hoặc bạn sẽ nhận được thông báo lỗi cho việc khai báo biến "trùng lặp".
Lời khuyên của mình là luôn luôn sử dụng const
và chỉ sử dụng let
khi bạn biết giá trị nào cần được gán lại với biến đó. Tại sao? Bởi vì code của chúng ta càng tối ưu càng tốt. Nếu chúng ta đã biết giá trị không thể gán lại, code của chúng ta sẽ ít sinh ra lỗi (bug) hơn.
Bây giờ chúng ta đã biết được cách làm việc với const
và let
, mình muốn đề cập thêm từ khoá var
.
Cho đến 2015, var
là cách duy nhất để khai báo biến trong JavaScript. Bây giờ, với nền tảng code hiện đại hơn, chúng ta sẽ được khuyến khích sử dụng const
và let
nhiều hơn.
Kiểu dữ liệu
Các biến trong JavaScript không có bất kỳ loại nào được liên kết chặt chẽ.
Các biến này thường không được định dạng (untyped).
Khi bạn gán giá trị với một số kiểu nhất định cho một biến, sau đó bạn có thể gán lại biến đó để lưu trữ giá trị thuộc bất kỳ kiểu nào khác mà không gặp bất kỳ vấn đề nào.
Trong JavaScript, chúng ta có 2 loại kiểu dữ liệu chính: kiểu nguyên thủy (primitive types) và kiểu đối tượng (object types).
Kiểu nguyên thuỷ
Các kiểu nguyên thủy bao gồm:
- Kiểu number
- Kiểu string
- Kiểu boolean
- Kiểu symbol
Và 2 kiểu đặc biệt là: null
và undefined
Kiểu đối tượng
Bất kỳ giá trị nào không thuộc kiểu nguyên thủy (string, number, boolean, null hoặc undefined) đều là một đối tượng.
Các kiểu đối tượng có các thuộc tính (properties) và cũng có các phương thức (methods) có thể hoạt động trên các thuộc tính đó.
Chúng ta sẽ nói thêm về các đối tượng ở phần sau.
Biểu thức
Biểu thức là một đơn vị code của JavaScript mà nó có thể đánh giá và trả về một giá trị.
Các biểu thức có thể khác nhau về mức độ phức tạp.
Chúng ta sẽ bắt đầu từ những cái rất đơn giản, được gọi là biểu thức chính (primary expressions):
2 0.02 'something' true false this //the current scope undefined i //where i is a variable or a constant
Biểu thức số học (Arithmetic expressions) là biểu thức nhận một biến và một toán tử (operator) và kết quả là một số:
1 / 2 i++ i -= 2 i * 2
Biểu thức chuỗi (String expressions) là biểu thức dẫn đến một chuỗi:
'A ' + 'string'
Biểu thức logic (Logical expressions) sử dụng các toán tử logic và giải quyết thành giá trị boolean:
a && b a || b !a
Các biểu thức nâng cao hơn liên quan đến đối tượng, hàm và mảng sẽ được mình giới thiệu chúng sau.
Toán tử
Các toán tử cho phép bạn lấy hai biểu thức đơn giản và kết hợp chúng để tạo thành một biểu thức phức tạp hơn.
Chúng ta có thể phân loại các toán tử dựa trên các toán hạng mà chúng làm việc. Một số toán tử làm việc với 1 toán hạng. Hầu hết đều hoạt động với 2 toán hạng. Chỉ có một toán tử làm việc với 3 toán hạng.
Trong phần giới thiệu đầu tiên về các toán tử, mình sẽ giới thiệu các toán tử mà bạn có thể quen thuộc nhất: các toán tử có 2 toán hạng.
Mình đã giới thiệu một lần khi nói về phần biến: toán tử gán =
. Bạn sử dụng =
để gán giá trị cho một biến:
let b = 2
Bây giờ chúng ta sẽ đến với một tập hợp các toán tử nhị phân khác mà bạn đã quen thuộc từ toán học cơ bản.
Toán tử cộng (+)
const three = 1 + 2 const four = three + 1
Toán tử +
cũng thực hiện nối chuỗi nếu bạn sử dụng chuỗi, vì vậy hãy chú ý:
const three = 1 + 2 three + 1 // 4 'three' + 1 // three1
Toán tử trừ (-)
const two = 4 - 2
Toán tử chia (/)
Trả về thương số của toán tử đầu tiên và toán tử thứ hai:
const result = 20 / 5 //result === 4 const result = 20 / 7 //result === 2.857142857142857
Nếu bạn chia cho 0, JavaScript sẽ không phát sinh bất kỳ lỗi nào nhưng sẽ trả về giá trị Infinity
(hoặc -Infinity
nếu giá trị là âm).
1 / 0 //Infinity -1 / 0 //-Infinity
Toán tử lấy dư (%)
Lấy dư là một phép tính rất hữu ích trong nhiều trường hợp sử dụng:
const result = 20 % 5 //result === 0 const result = 20 % 7 //result === 6
Phần dư bằng 0 luôn là NaN
, một giá trị đặc biệt có nghĩa là "Not a Number":
1 % 0 //NaN -1 % 0 //NaN
Toán tử nhân (*)
1 * 2 //2 -1 * 2 //-2
Toán tử lũy thừa (**)
Nâng toán hạng đầu tiên lên thành lũy thừa của toán hạng thứ hai
1 ** 2 //1 2 ** 1 //2 2 ** 2 //4 2 ** 8 //256 8 ** 2 //64
Quy tắc ưu tiên
Mọi câu lệnh phức tạp với nhiều toán tử trong cùng một dòng sẽ đưa ra các vấn đề về ưu tiên.
Lấy ví dụ sau:
let a = 1 * 2 + 5 / 2 % 2 //2.5
Kết quả là 2.5, nhưng tại sao?
Thao tác nào được thực hiện trước và thao tác nào cần đợi?
Một số thao tác được ưu tiên hơn những thao tác khác. Các quy tắc ưu tiên được liệt kê trong bảng này:
Toán tử | Chi tiết |
---|---|
* / % |
nhân/chia |
+ - |
cộng/trừ |
= |
gán |
Các phép toán cùng cấp (ví dụ như + và -) được thực hiện theo thứ tự từ trái sang phải.
Tuân theo các quy tắc này, hoạt động trên có thể được giải quyết theo cách này:
let a = 1 * 2 + 5 / 2 % 2 let a = 2 + 5 / 2 % 2 let a = 2 + 2.5 % 2 let a = 2 + 0.5 let a = 2.5
Toán tử so sánh
Nhóm toán tử thứ ba mà mình muốn giới thiệu là toán tử điều kiện.
Bạn có thể sử dụng các toán tử sau để so sánh hai số hoặc hai chuỗi.
Các toán tử so sánh luôn trả về giá trị boolean (true
hoặc false
).
Đó là các toán tử so sánh bất bình đẳng (disequality comparison operators):
<
nghĩa là "bé hơn"<=
nghĩa là "bé hơn bằng">
nghĩa là "lớn hơn">=
nghĩa là "lớn hơn bằng"
Ví dụ:
let a = 2 a >= 1 //true
Ngoài những toán tử đó, chúng ta có 4 toán tử so sánh bình đẳng. Chúng chấp nhận hai giá trị và trả về một boolean:
===
kiểm tra sự bình đẳng!==
kiểm tra sự bất bình đẳng
Lưu ý rằng chúng ta cũng có toán tử ==
và !=
. Trong JavaScript, mình thực sự khuyên bạn chỉ nên sử dụng ===
và !==
vì chúng có thể ngăn chặn một số vấn đề nhỏ.
Câu điều kiện
Một câu lệnh if
được sử dụng để làm cho chương trình có hướng đi rõ ràng, tùy thuộc vào kết quả đúng hay sai của một biểu thức.
Đây là ví dụ đơn giản nhất, đoạn code này luôn luôn thực thi:
if (true) { //do something }
Và ngược lại, đoạn code này sẽ không thực thi:
if (false) { //do something (? never ?) }
Điều kiện để kiểm tra biểu thức là khi bạn truyền cho nó giá trị đúng hay sai. Nếu bạn truyền vào là một số, giá trị đó luôn cho giá trị true
trừ khi nó là 0
. Nếu bạn truyền vào là một chuỗi, nó luôn đánh giá là true
trừ khi đó là một chuỗi trống. Đó là quy tắc chung khi ép kiểu sang boolean.
Bạn có để ý thấy dấu ngoặc nhọn không? Nó được gọi là một khối (block) và nó được dùng để gom nhóm thành một danh sách các câu lệnh khác nhau.
Một khối có thể được đặt ở bất cứ nơi nào bạn có thể có một câu lệnh. Và nếu bạn có một câu lệnh duy nhất để thực thi sau các điều kiện, bạn có thể bỏ qua khối và chỉ cần viết câu lệnh:
if (true) doSomething()
Nhưng mình luôn thích sử dụng dấu ngoặc nhọn để cho đoạn code rõ ràng hơn.
Bạn có thể cung cấp phần thứ hai cho câu lệnh if
: else
.
Bạn sẽ thêm một câu lệnh sẽ được thực thi nếu điều kiện if
là false
:
if (true) { //do something } else { //do something else }
Vì else
chấp nhận một câu lệnh, bạn có thể lồng một câu lệnh if
/ else
khác vào bên trong nó:
if (a === true) { //do something } else if (b === true) { //do something else } else { //fallback }
Mảng
Mảng (array) là tập hợp các phần tử.
Mảng trong ngôn ngữ JavaScript không có kiểu là "mảng".
Mà mảng có kiểu là đối tượng.
Chúng ta có thể khai báo mảng rỗng bằng 2 cách dưới đây:
const a = [] const a = Array()
Đầu tiên là sử dụng cú pháp ký tự của mảng (array literal syntax). Thứ hai sử dụng hàm tích hợp trong Array.
Bạn có thể điền trước mảng bằng cú pháp sau:
const a = [1, 2, 3] const a = Array.of(1, 2, 3)
Một mảng có thể chứa bất kỳ giá trị nào, ngay cả các giá trị thuộc các kiểu khác nhau:
const a = [1, 'Flavio', ['a', 'b']]
Vì chúng ta có thể thêm một mảng vào một mảng, chúng ta có thể tạo mảng đa chiều, có các ứng dụng rất hữu ích (ví dụ: ma trận):
const matrix = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ] matrix[0][0] //1 matrix[2][0] //7
Bạn có thể truy cập bất kỳ phần tử nào của mảng bằng cách tham chiếu đến chỉ mục (index) của nó, bắt đầu từ 0:
a[0] //1 a[1] //2 a[2] //3
Bạn có thể khởi tạo một mảng mới với một tập hợp các giá trị bằng cú pháp này. Dòng code này trước tiên sẽ khởi tạo một mảng 12 phần tử và điền vào mỗi phần tử bằng số 0:
Array(12).fill(0)
Bạn có thể lấy số phần tử trong mảng bằng cách kiểm tra thuộc tính length
của nó:
const a = [1, 2, 3] a.length //3
Lưu ý rằng bạn có thể đặt độ dài của mảng. Nếu bạn chỉ định một số lớn hơn độ dài hiện tại của mảng, thì không có gì xảy ra. Nếu bạn chỉ định một số nhỏ hơn, mảng sẽ bị cắt ở vị trí đó:
const a = [1, 2, 3] a //[ 1, 2, 3 ] a.length = 2 a //[ 1, 2 ]
Cách để thêm một phần tử vào một mảng
Chúng ta có thể thêm một phần tử vào vị trí cuối cùng của một mảng bằng cách sử dụng phương thức push()
:
a.push(4)
Chúng ta có thể thêm một phần tử vào vị trí đầu tiên của một mảng bằng cách sử dụng phương thức unshift()
:
a.unshift(0) a.unshift(-2, -1)
Cách để xoá một phần tử từ một mảng
Chúng ta có thể xoá một phần tử từ vị trí cuối cùng của một mảng bằng cách sử dụng phương thức pop()
:
a.pop()
Chúng ta có thể xoá một phần tử từ vị trí đầu tiên của một mảng bằng cách sử dụng phương thức shift()
:
a.shift()
Cách để trộn nhiều mảng lại với nhau
Bạn có thể trộn nhiều mảng với nhau bằng cách sử dụng phương thức concat()
:
const a = [1, 2] const b = [3, 4] const c = a.concat(b) //[1,2,3,4] a //[1,2] b //[3,4]
Bạn cũng có thể sử dụng toán tử spread (...
) bằng cách này:
const a = [1, 2] const b = [3, 4] const c = [...a, ...b] c //[1,2,3,4]
Cách để tìm một phần tử nhất định trong một mảng
Bạn có thể sử dụng phương thức find()
để tìm phần tử nhất định trong một mảng với cấu trúc như sau:
a.find((element, index, array) => { //return true or false })
Phương thức này sẽ trả về phần tử đầu tiên nếu như nó trả về true
và trả về undefined
nếu như nó không tìm thấy phần tử nào.
Cách sử dụng thường thấy của phương thức này là:
a.find(x => x.id === my_id)
Dòng lệnh trên nó sẽ trả về phần tử đầu tiên trong mảng nếu nó thoả điều kiện id === my_id
.
Phương thức findIndex()
hoạt động tương tự với phương thức find()
, nhưng nó sẽ trả về vị trí của phần tử đầu tiên thay vì là phần tử đầu tiên trong phương thức find()
.
a.findIndex((element, index, array) => { //return true or false })
Một phương thức khác cũng hoạt động tương tự là includes()
:
a.includes(value)
Nó sẽ trả về true
nếu nó chứa values
.
a.includes(value, i)
Nó sẽ trả về true
nếu nó chứa values
sau vị trí i
.
String
Một chuỗi (string) bao gồm nhiều ký tự (character).
Nó cũng có thể được định nghĩa là một chuỗi ký tự (string literal), được đặt trong dấu ''
hoặc dấu ""
:
'A string' "Another string"
Cá nhân mình luôn thích các dấu ''
và chỉ sử dụng dấu ""
trong HTML để xác định các thuộc tính.
Bạn sẽ gán một chuỗi cho một biến như thế này:
const name = 'Flavio'
Bạn có thể xác định độ dài của một chuỗi bằng cách sử dụng thuộc tính length
của nó:
'Flavio'.length //6 const name = 'Flavio' name.length //6
''
hoặc ""
được xem là chuỗi trống. Độ dài của chúng sẽ bằng 0
.
''.length //0
Nhiều chuỗi có thể trộn chung với nhau bằng toán tử +
:
"A " + "string"
Bạn có thể sử dụng toán tử +
để nội suy (interpolate) các biến
const name = 'Flavio' "My name is " + name //My name is Flavio
Một cách khác để xác định chuỗi là sử dụng ký tự mẫu (template literal), được xác định bên trong các dấu ``
. Chúng đặc biệt hữu ích để làm cho các chuỗi nhiều dòng trở nên đơn giản hơn nhiều. Với các dấu ngoặc kép hoặc đơn, bạn không thể dễ dàng xác định một chuỗi nhiều dòng, do đó bạn cần sử dụng các ký tự thoát.
Khi một ký tự mẫu được mở bằng ``
, bạn chỉ cần nhấn **enter** để tạo một dòng mới, không có ký tự đặc biệt và nó được hiển thị như cũ:
const string = `Hey this string is awesome!`
Ký tự mẫu cũng tuyệt vời vì chúng cung cấp một cách dễ dàng để nội suy các biến và biểu thức thành chuỗi.
Bạn có thể sử dụng cú pháp ${...}
này:
const var = 'test' const string = `something ${var}` //something test
Bên trong ${}
, bạn có thể bất cứ thứ gì, kể cả biểu thức:
const string = `something ${1 + 2 + 3}` const string2 = `something ${foo() ? 'x' : 'y'}`
Loop
Vòng lặp là một trong những cấu trúc điều khiển chính của JavaScript.
Với một vòng lặp, chúng ta có thể tự động hóa và lặp lại một khối lệnh nhiều lần, thậm chí là vô thời hạn.
JavaScript cung cấp nhiều cách để lặp qua các vòng lặp.
Mình sẽ tập trung vào 3 cách dưới đây:
- vòng lặp
while
- vòng lặp
for
- vòng lặp
for...of
While
Vòng lặp while là cấu trúc lặp đơn giản nhất mà JavaScript cung cấp cho chúng ta.
Chúng ta thêm một điều kiện sau từ khóa ```while``` và chúng ta cung cấp một khối lệnh được chạy cho đến khi điều kiện được đánh giá là ```true```.
Ví dụ:
const list = ['a', 'b', 'c'] let i = 0 while (i < list.length) { console.log(list[i]) //value console.log(i) //index i = i + 1 }
Bạn có thể ngắt vòng lặp while bằng cách sử dụng từ khoá break
, giống như này:
while (true) { if (somethingIsTrue) break }
và nếu bạn xác định rằng ở giữa vòng lặp bạn muốn bỏ qua lần lặp hiện tại, bạn có thể chuyển sang lần lặp tiếp theo bằng cách sử dụng từ khoá continue
:
while (true) { if (somethingIsTrue) continue //do something else }
Và tương tự với vòng lặp while, chúng ta cũng có vòng lặp do...while. Cách hoạt động cũng tương tự với vòng lặp while, ngoại trừ điều kiện được đánh giá sau khi khối lệnh được thực thi.
Điều này có nghĩa là khối lệnh luôn được thực thi ít nhất một lần.
Ví dụ:
const list = ['a', 'b', 'c'] let i = 0 do { console.log(list[i]) //value console.log(i) //index i = i + 1 } while (i < list.length)
For
Cấu trúc lặp rất quan trọng thứ hai trong JavaScript là vòng lặp for.
Chúng ta sử dụng từ khóa for
và chúng ta sẽ truyền một bộ 3 đầu vào: phần khởi tạo, phần điều kiện và phần gia tăng.
Ví dụ:
const list = ['a', 'b', 'c'] for (let i = 0; i < list.length; i++) { console.log(list[i]) //value console.log(i) //index }
Cũng giống như với vòng lặp while, bạn có thể ngắt vòng lặp for bằng cách sử dụng từ khoá break
và bạn có thể chuyển tiếp nhanh đến lần lặp tiếp theo của vòng lặp for bằng cách sử dụng từ khoá continue
.
For...of
Vòng lặp này được xuất hiện gần đây (được giới thiệu vào năm 2015) và nó là một phiên bản đơn giản của vòng lặp for:
const list = ['a', 'b', 'c'] for (const value of list) { console.log(value) //value }
Function
Trong bất kỳ chương trình JavaScript đơn giản hay phức tạp, mọi thứ đều xảy ra bên trong các hàm.
Hàm là một phần cốt lõi, thiết yếu của JavaScript.
Hàm là gì?
Hàm là một khối lệnh, được bao bọc.
Đây là một khai báo hàm:
function getData() { // do something }
Một hàm có thể được chạy bất cứ lúc nào bạn muốn bằng cách gọi nó, như sau:
getData()
Một hàm có thể có một hoặc nhiều đối số (argument):
function getData() { //do something } function getData(color) { //do something } function getData(color, age) { //do something }
Khi chúng ta có thể truyền một đối số, chúng ta gọi hàm và truyền tham số:
function getData(color, age) { //do something } getData('green', 24) getData('black')
Lưu ý rằng trong lời gọi hàm thứ hai, mình đã truyền tham số chuỗi black
làm đối số color
, nhưng không có truyền tham số age
. Trong trường hợp này, age
bên trong hàm là undefined
.
Chúng ta có thể kiểm tra xem một giá trị không phải là undefined
bằng cách sử dụng điều kiện này:
function getData(color, age) { //do something if (typeof age !== 'undefined') { //... } }
typeof
là toán tử một ngôi cho phép chúng ta kiểm tra kiểu của một biến.
Bạn cũng có thể kiểm tra bằng cách này:
function getData(color, age) { //do something if (age) { //... } }
Mặc dù điều kiện sẽ là true
nếu age
là null
, 0
hoặc là một chuỗi trống.
Bạn cũng có thể sử dụng giá trị mặc định cho các tham số đối với trường hợp chúng không được truyền vào:
function getData(color = 'black', age = 25) { //do something }
Bạn có thể truyền vào bất kỳ giá trị nào dưới dạng tham số: số, chuỗi, boolean, mảng, đối tượng và cả hàm.
Một hàm có một giá trị trả về. Theo mặc định, một hàm trả về undefined
, trừ khi bạn thêm một từ khóa return
có giá trị:
function getData() { // do something return 'hi!' }
Chúng ta có thể gán giá trị trả về này cho một biến khi chúng ta gọi hàm:
function getData() { // do something return 'hi!' } let result = getData()
Bây giờ biến result
sẽ giữ một chuỗi với giá trị hi!
.
Bạn chỉ có thể trả về một giá trị.
Để trả về nhiều giá trị, bạn có thể trả về một đối tượng hoặc một mảng, như sau:
function getData() { return ['Flavio', 37] } let [name, age] = getData()
Các hàm có thể được tạo bên trong các hàm khác:
const getData = () => { const dosomething = () => {} dosomething() return 'test' }
Hàm lồng nhau (nested function) không thể được gọi từ bên ngoài của hàm bao (enclosing function).
Bạn cũng có thể trả về một hàm từ một hàm.
Arrow Function
Hàm mũi tên (arrow function) được giới thiệu gần đây về JavaScript.
Chúng rất thường được sử dụng thay vì các hàm "thông thường", những hàm mà mình đã mô tả trong phần hàm. Bạn sẽ tìm thấy cả hai loại này được sử dụng ở khắp mọi nơi.
Về mặt trực quan, chúng cho phép bạn viết các hàm với cú pháp ngắn hơn, ví dụ:
function getData() { //... }thành:
() => { //... }
Lưu ý rằng chúng ta không có tên hàm ở đây.
Các hàm mũi tên là ẩn danh. Chúng ta phải gán chúng cho một biến.
Chúng ta có thể gán một hàm thông thường cho một biến, như sau:
let getData = function getData() { //... }
Khi chúng ta làm như vậy, chúng ta có thể xóa tên khỏi hàm:
let getData = function() { //... }
và gọi hàm bằng tên biến:
let getData = function() { //... } getData()
Chúng ta cũng làm điều tương tự đối với hàm mũi tên:
let getData = () => { //... } getData()
Nếu thân hàm chỉ chứa một câu lệnh duy nhất, bạn có thể bỏ qua dấu {}
và viết mọi thứ trên một dòng:
const getData = () => console.log('hi!')
Các tham số được truyền vào trong dấu ngoặc đơn:
const getData = (param1, param2) => console.log(param1, param2)
Nếu bạn có một (và chỉ một) tham số, bạn có thể bỏ qua hoàn toàn các dấu ngoặc đơn:
const getData = param => console.log(param)
Hàm mũi tên cho phép bạn có thể trả về một cách ngầm định - nghĩa là các giá trị được trả về mà không cần phải sử dụng từ khóa return
.
Nó hoạt động khi có một câu lệnh một dòng trong thân hàm:
const getData = () => 'test' getData() //'test'
Giống như với các hàm thông thường, chúng ta có thể có các giá trị mặc định cho các tham số trong trường hợp chúng không được truyền vào:
const getData = (color = 'black', age = 2) => { //do something }
Và giống như các hàm thông thường, chúng ta chỉ có thể trả về một giá trị.
Các hàm mũi tên cũng có thể chứa các hàm mũi tên khác, hoặc thậm chí là các hàm thông thường.
Hai loại hàm này rất giống nhau, vì vậy bạn có thể hỏi tại sao hàm mũi tên lại được giới thiệu. Sự khác biệt lớn với các hàm thông thường là khi chúng được sử dụng như các phương thức đối tượng.
Object
Bất kỳ giá trị nào không thuộc kiểu nguyên thủy (chuỗi, số, boolean, ký hiệu, null hoặc undefined) đều là một đối tượng.
Đây là cách chúng ta xác định một đối tượng:
const car = { }
Đây là cú pháp của đối tượng theo nghĩa đen, là một trong những thứ tốt nhất trong JavaScript.
Bạn cũng có thể sử dụng cú pháp new Object
:
const car = new Object()
Cú pháp khác để khởi tạo đối tượng là sử dụng Object.create()
:
const car = Object.create()
Bạn cũng có thể khởi tạo một đối tượng bằng cách sử dụng từ khóa new
trước một hàm có chữ hoa. Hàm này đóng vai trò là một phương thức khởi tạo cho đối tượng đó. Trong đó, chúng ta có thể khởi tạo các đối số mà chúng ta nhận được dưới dạng tham số, để thiết lập trạng thái ban đầu của đối tượng:
function Car(brand, model) { this.brand = brand this.model = model }
Chúng ta khởi tạo một đối tượng mới bằng cách sử dụng:
const myCar = new Car('Ford', 'Fiesta') myCar.brand //'Ford' myCar.model //'Fiesta'
Các đối tượng luôn được truyền vào thông qua tham chiếu.
Nếu bạn chỉ định một biến có cùng giá trị của một biến khác, nếu đó là kiểu nguyên thủy như một số hoặc một chuỗi, chúng được chuyển bằng giá trị.
Lấy ví dụ dưới đây:
let age = 36 let myAge = age myAge = 37 age //36
const car = { color: 'blue' } const anotherCar = car anotherCar.color = 'yellow' car.color //'yellow'
Ngay cả mảng hoặc hàm cũng được xem là đối tượng, vì vậy điều rất quan trọng là phải hiểu cách chúng hoạt động như thế nào.
Object Properties
Các đối tượng có các thuộc tính (properties), được cấu tạo bởi một cái tên được liên kết với một giá trị.
Giá trị của một thuộc tính có thể thuộc bất kỳ kiểu nào, có nghĩa là nó có thể là một mảng, một hàm và thậm chí nó có thể là một đối tượng, vì các đối tượng có thể lồng các đối tượng khác.
Đây là cú pháp theo nghĩa đen của đối tượng mà chúng ta đã thấy trong phần object:
const car = { }
Chúng ta có thể định nghĩa thuộc tính color
bằng cách này:
const car = { color: 'blue' }
Ở đây chúng ta có đối tượng car
với thuộc tính được đặt tên là color
, với giá trị là blue
.
Tên có thể là bất kỳ chuỗi nào, nhưng hãy cẩn thận với các ký tự đặc biệt - nếu chúng ta muốn bao gồm một ký tự không hợp lệ làm tên biến trong tên thuộc tính, chúng ta sẽ phải sử dụng dấu ''
xung quanh nó:
const car = { color: 'blue', 'the color': 'blue' }
Các ký tự tên biến không hợp lệ bao gồm dấu cách, dấu gạch nối và các ký tự đặc biệt khác.
Như bạn có thể thấy, khi chúng ta có nhiều thuộc tính, chúng ta phân tách từng thuộc tính bằng dấu ```,```.
Chúng ta có thể lấy giá trị của một thuộc tính bằng 2 cú pháp khác nhau.
Cú pháp đầu tiên là dấu .
:
car.color //'blue'
Cú pháp thứ hai (là cách duy nhất chúng ta có thể sử dụng cho các thuộc tính có tên không hợp lệ), là sử dụng dấu ```[]````:
car['the color'] //'blue'
Nếu bạn truy cập một thuộc tính không tồn tại, bạn sẽ nhận được giá trị undefined
:
car.brand //undefined
Như đã đề cập trước đó, các đối tượng có thể có các đối tượng khác lồng nhau làm thuộc tính:
const car = { brand: { name: 'Ford' }, color: 'blue' }
Trong ví dụ này, bạn có thể truy cập vào tên thương hiệu bằng cách sử dụng:
car.brand.name
hoặc
car['brand']['name']
Bạn có thể đặt giá trị của một thuộc tính khi bạn định nghĩa một đối tượng.
Nhưng bạn luôn có thể cập nhật nó sau đó:
const car = { color: 'blue' } car.color = 'yellow' car['color'] = 'red'
Và bạn cũng có thể thêm các thuộc tính mới vào một đối tượng:
car.model = 'Fiesta' car.model //'Fiesta'
Ngoài ra, bạn cũng có thể xoá một thuộc tính từ một đối tượng, ví dụ như mình muốn xoá thuộc tính brand
của đối tượng trên:
delete car.brand
Object Method
Chúng ta đã nói về hàm trong phần trước.
Hàm có thể được gán cho một thuộc tính và trong trường hợp này chúng được gọi là các phương thức.
Trong ví dụ này, thuộc tính start
có một hàm được gán và chúng ta có thể gọi nó bằng cách sử dụng cú pháp dấu .
mà chúng ta đã sử dụng cho các thuộc tính, với dấu ()
ở cuối:
const car = { brand: 'Ford', model: 'Fiesta', start: function() { console.log('Started') } } car.start()
Bên trong một phương thức được xác định bằng cú pháp function() {}
, chúng ta có quyền truy cập vào cá thể đối tượng bằng cách tham chiếu đến từ khoá this
.
Trong ví dụ sau, chúng ta có quyền truy cập vào các giá trị thuộc tính brand
và model
bằng cách sử dụng this.brand
và this.model
:
const car = { brand: 'Ford', model: 'Fiesta', start: function() { console.log(`Started ${this.brand} ${this.model}`) } } car.start()
Điều quan trọng cần lưu ý là sự phân biệt giữa hàm thông thường và hàm mũi tên - chúng ta không có quyền truy cập từ khoá this
nếu chúng ta sử dụng hàm mũi tên:
const car = { brand: 'Ford', model: 'Fiesta', start: () => { console.log(`Started ${this.brand} ${this.model}`) //not going to work } } car.start()
Điều này xảy ra là do hàm mũi tên không bị ràng buộc với đối tượng.
Đây là lý do tại sao hàm thông thường được sử dụng như các phương thức đối tượng.
Phương thức có thể chấp nhận các tham số, như hàm thông thường:
const car = { brand: 'Ford', model: 'Fiesta', goTo: function(destination) { console.log(`Going to ${destination}`) } } car.goTo('Rome')
Class
Chúng ta đã nói về các đối tượng, đây là một trong những phần thú vị nhất của JavaScript.
Trong chương này, chúng ta sẽ nâng cao một cấp độ bằng cách giới thiệu các lớp học.
Các lớp học là gì? Chúng là một cách để xác định một mẫu chung cho nhiều đối tượng.
Hãy lấy một đối tượng người:
Chúng ta đã nói về đối tượng, đây là một trong những phần thú vị nhất của JavaScript.
Trong phần này, chúng ta sẽ nâng cao một cấp độ bằng cách giới thiệu về lớp (class).
Vậy lớp là gì? Nó là một cách để xác định một khuôn mẫu chung cho nhiều đối tượng.
Hãy lấy ví dụ về một đối tượng person
:
const person = { name: 'Flavio' }
Chúng ta có thể tạo một lớp có tên là Person
(lưu ý chữ P phải viết hoa, một quy ước khi sử dụng lớp), có thuộc tính là name
:
class Person { name }
Bây giờ từ lớp này, chúng ta khởi tạo một đối tượng flavio
như sau:
const flavio = new Person()
flavio
được gọi là một thực thể (instance) của lớp Person
.
Chúng ta có thể đặt giá trị của thuộc tính name
:
flavio.name = 'Flavio'
và chúng ta có thể truy cập nó bằng cách sử dụng:
flavio.name
giống như chúng ta làm đối với các thuộc tính đối tượng.
Lớp có thể chứa các thuộc tính, như tên và phương thức.
Các phương thức được định nghĩa theo cách này:
class Person { hello() { return 'Hello, I am Flavio' } }
và chúng ta có thể gọi các phương thức trên một thực thể của lớp:
class Person { hello() { return 'Hello, I am Flavio' } } const flavio = new Person() flavio.hello()
Có một phương thức đặc biệt gọi là constructor()
mà chúng ta có thể sử dụng để khởi tạo các thuộc tính của lớp khi chúng ta tạo một thực thể đối tượng mới.
Nó hoạt động như thế này:
class Person { constructor(name) { this.name = name } hello() { return 'Hello, I am ' + this.name + '.' } }
Lưu ý rằng chúng ta sử dụng this
để truy cập thực thể đối tượng.
Bây giờ chúng ta có thể khởi tạo một đối tượng mới từ lớp, truyền vào một chuỗi và khi chúng ta gọi phương thức hello
, chúng ta sẽ nhận được một thông điệp:
const flavio = new Person('flavio') flavio.hello() //'Hello, I am flavio.'
Khi đối tượng được khởi tạo, phương thức constructor
được gọi với bất kỳ tham số nào được truyền vào.
Thông thường các phương thức được định nghĩa trên thực thể đối tượng, không phải trên lớp.
Thay vào đó, bạn có thể định nghĩa một phương thức static
để cho phép nó được thực thi trên lớp:
class Person { static genericHello() { return 'Hello' } } Person.genericHello() //Hello
Phương thức này thường khá hữu ích trong nhiều trường hợp.
Inheritance
Một lớp có thể kế thừa (extend) một lớp khác và các đối tượng được khởi tạo bằng cách sử dụng lớp đó kế thừa tất cả các phương thức của cả hai lớp.
Giả sử chúng ta có một lớp Person
:
class Person { hello() { return 'Hello, I am a Person' } }
Chúng ta có thể định nghĩa một lớp mới, Programmer
, lớp đó sẽ kế thừa lớp Person
:
class Programmer extends Person { }
Bây giờ nếu chúng ta khởi tạo một đối tượng mới với lớp Programmer
, nó có quyền truy cập vào phương thức hello()
:
const flavio = new Programmer() flavio.hello() //'Hello, I am a Person'
Bên trong một lớp con, bạn có thể tham chiếu đến lớp cha bằng cách gọi phương thức super()
:
class Programmer extends Person { hello() { return super.hello() + '. I am also a programmer.' } } const flavio = new Programmer() flavio.hello()
Chương trình trên sẽ in ra Hello, I am a Person. I am also a programmer.
.
Asynchonous và Callbacks
Hầu hết thời gian, code JavaScript chạy một cách đồng bộ (synchronously).
Điều này có nghĩa là một dòng lệnh được thực thi, sau đó dòng tiếp theo được thực thi.
Mọi thứ đều giống như bạn mong đợi và cách nó hoạt động trong hầu hết các ngôn ngữ lập trình.
Tuy nhiên, có những lúc bạn không thể chỉ đợi một dòng lệnh thực thi.
Bạn không thể chỉ đợi 2 giây để tải một tệp lớn và tạm dừng chương trình hoàn toàn.
Bạn không thể chỉ đợi tài nguyên mạng được tải xuống trước khi làm việc khác.
JavaScript giải quyết vấn đề này bằng cách sử dụng các lệnh gọi lại (callback).
Một trong những ví dụ đơn giản nhất về cách sử dụng lệnh gọi lại là với bộ hẹn giờ (timer). Bộ hẹn giờ không phải là một phần của JavaScript, nhưng chúng được cung cấp bởi trình duyệt và Node.js. Hãy để mình nói về một trong những bộ hẹn giờ mà chúng ta có: setTimeout()
.
Hàm setTimeout()
chấp nhận 2 đối số: một hàm và một số. Số ở đây là thời gian (được tính bằng mili giây) phải trôi qua trước khi chạy hàm.
Ví dụ:
setTimeout(() => { // runs after 2 seconds console.log('inside the function') }, 2000)
Hàm chứa dòng lệnh console.log('inside the function')
sẽ được thực thi sau 2 giây.
Nếu bạn thêm console.log('before')
vào trước hàm và console.log ('after')
sau nó:
console.log('before') setTimeout(() => { // runs after 2 seconds console.log('inside the function') }, 2000) console.log('after')
Bạn sẽ thấy điều này xảy ra trong bảng điều khiển của mình:
before after inside the function
Hàm gọi lại được thực thi không đồng bộ (asynchronously).
Đây là một khuôn mẫu rất phổ biến khi làm việc với tập tin hệ thống, mạng, sự kiện (event) hoặc DOM trong trình duyệt.
Đây là cách chúng ta có thể triển khai các lệnh gọi lại trong code của mình.
Chúng ta định nghĩa một hàm chấp nhận một tham số callback
.
Khi code đã sẵn sàng để gọi lại, chúng ta gọi nó bằng cách truyền vào kết quả:
const doSomething = callback => { //do things //do things const result = /* .. */ callback(result) }
Code sử dụng hàm này sẽ dùng nó như thế này:
doSomething(result => { console.log(result) })
Promise
Lời hứa (promise) là một cách thay thế để đối phó với những đoạn code không đồng bộ.
Như chúng ta đã thấy trong phần trước, với các lệnh gọi lại, chúng ta sẽ truyền vào một hàm sang một hàm khác mà nó sẽ được gọi khi hàm xử lý xong.
Như thế này:
doSomething(result => { console.log(result) })
Khi mà hàm doSomething()
kết thúc, nó gọi hàm nhận được dưới dạng tham số:
const doSomething = callback => { //do things //do things const result = /* .. */ callback(result) }
Vấn đề chính với cách tiếp cận này là nếu chúng ta cần sử dụng kết quả của hàm này trong phần còn lại của code, tất cả code của chúng ta phải được lồng vào bên trong lệnh gọi lại và nếu chúng ta phải thực hiện 2-3 lệnh gọi lại, chúng ta sẽ bị rơi vào tình huống thường được gọi là "địa ngục gọi lại" với nhiều cấp hàm được thụt vào trong các hàm khác:
doSomething(result => { doSomethingElse(anotherResult => { doSomethingElseAgain(yetAnotherResult => { console.log(result) }) }) })
Lời hứa là một cách để đối phó với điều này.
Thay vì làm cách này:
doSomething(result => { console.log(result) })
Chúng ta sẽ gọi một hàm dựa trên lời hứa bằng cách này:
doSomething() .then(result => { console.log(result) })
Đầu tiên chúng ta gọi hàm, sau đó chúng ta có một phương thức then()
được gọi khi hàm kết thúc.
Thường thì thụt đầu dòng không quan trọng, nhưng bạn sẽ được khuyến khích sử dụng kiểu này để code được rõ ràng hơn.
Thông thường để phát hiện lỗi thì chúng ta sẽ sử dụng phương thức catch()
:
doSomething() .then(result => { console.log(result) }) .catch(error => { console.log(error) })
Bây giờ, để có thể sử dụng cú pháp này, việc triển khai hàm doSomething()
phải đặc biệt một chút. Nó phải sử dụng bộ Promise API.
Thay vì khai báo nó như một hàm bình thường:
const doSomething = () => { }
Chúng ta sẽ khai báo nó như một đối tượng lời hứa:
const doSomething = new Promise()
và chúng ta truyền vào một hàm trong ```constructor``` của ```Promise```:
const doSomething = new Promise(() => { })
Hàm này nhận 2 tham số. Hàm đầu tiên là một hàm chúng ta gọi để giải quyết (resolve) lời hứa, hàm thứ hai là một hàm chúng ta gọi để từ chối (reject) lời hứa.
const doSomething = new Promise( (resolve, reject) => { })
Giải quyết một lời hứa có nghĩa là hoàn thành nó thành công (dẫn đến việc gọi phương thức then()
trong bất kỳ phương thức nào sử dụng nó).
Từ chối một lời hứa có nghĩa là kết thúc nó với một lỗi (dẫn đến việc gọi phương thức catch()
trong bất cứ phương thức nào sử dụng nó).
Dưới đây là cách sử dụng chúng:
const doSomething = new Promise( (resolve, reject) => { //some code const success = /* ... */ if (success) { resolve('ok') } else { reject('this error occurred') } } )
Chúng ta có thể truyền vào một tham số cho hàm giải quyết và từ chối, thuộc bất kỳ loại nào chúng ta muốn.
Async và Await
Hàm async
là một hàm trừu tượng (abstraction) có cấp cao hơn của hàm lời hứa.
Và hàm async
trả về một Promise
, như trong ví dụ này:
const getData = () => { return new Promise((resolve, reject) => { setTimeout(() => resolve('some data'), 2000) }) }
Bất kỳ đoạn code nào muốn sử dụng hàm này sẽ sử dụng từ khóa await
ngay trước hàm:
const data = await getData()
và làm như vậy, bất kỳ dữ liệu nào được trả về bởi lời hứa sẽ được gán cho biến data
.
Trong trường hợp của chúng tôi, dữ liệu là chuỗi some data
.
Với một lưu ý nữa: bất cứ khi nào chúng ta sử dụng từ khóa await
, chúng ta phải làm như vậy bên trong một hàm được định nghĩa là async
.
Giống như này:
const doSomething = async () => { const data = await getData() console.log(data) }
Bộ đôi async/await cho phép chúng ta có code sạch hơn và mô hình đơn giản để làm việc với code không đồng bộ.
Như bạn có thể thấy trong ví dụ trên, code của chúng ta trông rất đơn giản. So sánh nó với đoạn code sử dụng lời hứa hoặc hàm gọi lại.
Và đây là một ví dụ rất đơn giản, những lợi ích chính sẽ phát sinh khi code phức tạp hơn nhiều.
Ví dụ: đây là cách bạn có được tài nguyên JSON bằng cách sử dụng hàm fetch()
để lấy API và phân tích dữ liệu đó, sử dụng bằng lời hứa:
const getFirstUserData = () => { // get users list return fetch('/users.json') // parse JSON .then(response => response.json()) // pick first user .then(users => users[0]) // get user data .then(user => fetch(`/users/${user.name}`)) // parse JSON .then(userResponse => response.json()) } getFirstUserData()
Và đây là hàm tương tự được sử dụng bằng await/async:
const getFirstUserData = async () => { // get users list const response = await fetch('/users.json') // parse JSON const users = await response.json() // pick first user const user = users[0] // get user data const userResponse = await fetch(`/users/${user.name}`) // parse JSON const userData = await user.json() return userData } getFirstUserData()
Variable Scope
Khi mình giới thiệu về biến, mình đã nói về việc sử dụng const
, let
và var
.
Phạm vi (scope) là tập hợp các biến hiển thị cho một phần của chương trình.
Trong JavaScript, chúng ta có phạm vi toàn cục (global scope), phạm vi khối (block scope) và phạm vi hàm (function scope).
Nếu một biến được định nghĩa bên ngoài một hàm hoặc khối, thì nó được gắn liền với đối tượng toàn cục và nó có phạm vi toàn cục, nghĩa là nó có sẵn trong mọi phần của chương trình.
Có một sự khác biệt rất quan trọng giữa các khai báo var
, let
và const
.
Một biến được định nghĩa là var
bên trong một hàm chỉ hiển thị bên trong hàm đó, tương tự như các đối số của một hàm.
Mặt khác, một biến được định nghĩa là const
hoặc let
chỉ hiển thị bên trong khối mà nó được xác định.
Một khối là một tập hợp các lệnh được nhóm thành một cặp dấu ngoặc nhọn, giống như những lệnh mà chúng ta có thể tìm thấy bên trong câu lệnh if
, vòng lặp for
hoặc một hàm.
Điều quan trọng là phải hiểu rằng một khối không xác định phạm vi mới cho var
, nhưng nó xác định cho let
và const
.
Điều này có ý nghĩa rất thiết thực.
Giả sử bạn xác định một biến var
bên trong một if
có điều kiện trong một hàm:
function getData() { if (true) { var data = 'some data' console.log(data) } }
Nếu bạn gọi hàm này, bạn sẽ nhận được some data
được in ra trong bảng điều khiển.
Nếu bạn cố gắng di chuyển console.log(data)
sau từ khoá if
, nó vẫn hoạt động:
function getData() { if (true) { var data = 'some data' } console.log(data) }
Nhưng nếu bạn đổi var data
thành let data
:
function getData() { if (true) { let data = 'some data' } console.log(data) }
Bạn sẽ nhận được thông báo lỗi: ReferenceError: data is not defined
.
Điều này là do var
nằm trong phạm vi hàm và có một điều đặc biệt xảy ra ở đây được gọi là hoisting. Nó có nghĩa là khi bạn khai báo var
thì nó sẽ được JavaScript chuyển lên đầu hàm gần nhất trước khi nó chạy code:
function getData() { var data if (true) { data = 'some data' } console.log(data) }
Đây là lý do tại sao bạn cũng có thể console.log(data)
ở đầu một hàm, ngay cả trước khi nó được khai báo và bạn sẽ nhận được giá trị undefined
cho biến đó:
function getData() { console.log(data) if (true) { var data = 'some data' } }
Nhưng nếu bạn chuyển sang let
, bạn sẽ nhận được thông báo lỗi: ReferenceError: data is not defined
, vì vậy hoisting không xảy ra khi khai báo let
.
const
tuân theo các quy tắc tương tự như let
: phạm vi khối.
Lúc đầu có thể hơi phức tạp, nhưng một khi bạn nhận ra sự khác biệt này, thì bạn sẽ thấy tại sao ngày nay var
được coi là một phương pháp không tốt so với let
- chúng có ít phần di chuyển hơn và phạm vi của chúng bị giới hạn trong khối, điều này cũng khiến chúng rất tốt như các biến vòng lặp vì chúng không còn tồn tại sau khi vòng lặp kết thúc:
function doLoop() { for (var i = 0; i < 10; i++) { console.log(i) } console.log(i) } doLoop()
Khi bạn thoát khỏi vòng lặp, i
sẽ là một biến hợp lệ có giá trị là ```10```.
Nếu bạn chuyển sang let
, khi bạn cố gắng console.log(i)
sẽ dẫn đến thông báo lỗi: ReferenceError: i is not defined
.
Kết luận
Cám ơn các bạn đã dành thời gian để xem hết cẩm nang này. Mình hy vọng với bản dịch của quyển sách này sẽ giúp các bạn hiểu được nền tảng cơ bản của ngôn ngữ JavaScript. Để biết thêm thông tin chi tiết, bạn có thể xem qua trang blog của tác giả flaviocopes.com.
Bạn cũng có thể xem phiên bản PDF và ePub của quyển cẩm nang này bằng cách đăng ký trang web flaviocopes.com.