Trong bài viết này, chúng ta sẽ gặp lại 2 khái niệm: Hàm function
và Vùng scope
- đã được giới thiệu trong bài viết JavaScript số 3 mà chúng ta đã thực hiện trước đây. Chúng ta đã có một phần giới thiệu về 2 khái niệm này ở một giao diện bề mặt đơn giản, phù hợp với thời điểm khởi đầu. Tuy nhiên ở thời điểm hiện tại, chúng ta đã khá quen với JavaScript
và công việc lập trình nói chung; Và tất nhiên là đã có thể tự tin hơn để tìm hiểu về các khái niệm ở một giao diện có chiều sâu và chi tiết hơn. Hãy cùng gặp gỡ những điều mới mẻ của những khái niệm cũ.
Một cách khác để khởi tạo hàm trong JavaScript
Bên cạnh cú pháp khai báo hàm bằng từ khóa function
mà chúng ta đã biết, JavaScript còn cung cấp cho chúng ta một cú pháp khác để khởi tạo một hàm như sau:
function.js
const doubleIt = (num) => num * 2;
console.log( doubleIt(9) );
// kết quả: 18
Như bạn thấy thì cú pháp mới không sử dụng từ khóa mà thay vào đó chúng ta có một ký hiệu =>
ở sau cặp ngoặc đơn khai báo các biến nhận dữ liệu vào hàm (num)
. Cũng vì ký hiệu =>
này mà các hàm được tạo ra bằng cú pháp mới được gọi là các hàm mũi tên arrow function
. Nó có tên gọi riêng để phân biệt với cái cũ thì hiển nhiên cũng sẽ có những điểm khác biệt nào đó nữa để người ta cần phải đặt tên mới như vậy. Chúng ta sẽ xem có những gì khác nữa nào.
Hàm doubleIt
của chúng ta sẽ trả về một giá trị được khuếch đại lên gấp 2 lần như tên hàm đã mô tả. Vậy ở đây chúng ta có phần phía sau ký hiệu =>
chính là phần thân hàm num * 2
. Chúng ta không thấy từ khóa return
để trả về giá trị và cũng không có cặp ngoặc đơn {}
thường thấy để khoanh vùng phần thân hàm. Vậy có lẽ cú pháp này được thiết kế để viết hàm ở những vị trí nào đó mà chúng ta cần sự ngắn gọn, xúc tích.
Bây giờ giả sử chúng ta có một mảng các giá trị số học và muốn tạo ra một mảng mới có các phần tử được nhân đôi
so với các giá trị ở mảng cũ. Có lẽ cú pháp mới này sẽ rất phù hợp.
function.js
var numArray = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
var newArray = numArray.map((num) => num * 2);
console.log(newArray);
// kết quả: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
Ồ... như vậy là đối với những trường hợp cần truyền một hàm đơn giản
vào một hàm khác, có lẽ chúng ta sẽ không cần phải cố gắng nghĩ ra thêm một cái tên cho một hàm mà chúng ta không có nhu cầu sử dụng lại. Thật tuyệt.
Tuy nhiên, đó - chưa phải là tất cả. Cú pháp =>
còn cho phép chúng ta viết hàm ngắn gọn hơn trong trường hợp chúng ta muốn tạo ra một hàm nhận vào nhiều giá trị, và muốn áp dụng các giá trị đầu vào theo từng phần.
partial.js
const multiply = (a) => (b) => a * b;
const doubleIt = multiply(2);
const tripleIt = multiply(3); console.log( doubleIt(0) ); // kết quả: 0
console.log( doubleIt(1) ); // kết quả: 2
console.log( tripleIt(0) ); // kết quả: 0
console.log( tripleIt(1) ); // kết quả: 3
Ở đây hàm multiply
được định nghĩa là một hàm nhận vào duy nhất một giá trị (a)
và trả về một giá trị nào đó sau ký hiệu =>
đầu tiên. Sau đó giá trị trả về ở đây lại là một hàm được định nghĩa là nhận vào duy nhất một giá trị (b)
và trả về kết quả là a * b
. Do hàm thứ 2 được tạo ra xếp chồng ở bên trong hàm đầu tiên, nên có thể tham chiếu tới biến a
được khai báo ở hàm thứ nhắt.
Trong trường hợp sử dụng cú pháp function
để khởi tạo hàm multiply
như trên, chúng ta sẽ phải viết dài hơn khá nhiều.
partial.js
const multiply = function(a) { return function(b) { return a * b; };
}; // multiply
Tuy nhiên thì mọi thứ đều có ưu điểm và nhược điểm riêng. Nếu đứng từ góc độ của một người chưa từng viết code, rõ ràng từ khóa function
có tính mô tả tốt hơn và chỉ ra rất rõ: đây là một hàm
; Và không yêu cầu người đọc phải tìm hiểu thông tin về ý nghĩa của cú pháp đang sử dụng là để làm gì
? Trong khi đó thì ký hiệu =>
chỉ nói được lên rằng: có một sự liên quan
giữa ký hiệu a
với vế bên phải, và tương tự với ký hiệu b
sau đó.
Vì vậy nên trường hợp sử dụng với các hàm lặp của mảng thì cú pháp mới khá phù hợp, còn ở trường hợp thứ 2 thì chúng ta sẽ cần phải cân nhắc xem code mà chúng ta viết ra sẽ chia sẻ cho những ai nữa. Bản thân mình thì rất ưa thích cú pháp =>
để sử dụng cho trường hợp số 2 vì nó cho phép định dạng code với các tham số thẳng hàng và rất dễ theo dõi. Tuy nhiên trong Series Tự Học Lập Trình Web Một Cách Thật Tự Nhiên thì chúng ta cần dự trù thêm trường hợp có ai đó bất chợt ghé qua và tham gia học cùng; Do đó bạn thông cảm là các ví dụ từ đây về sau này mình vẫn sẽ dùng chủ yếu là cú pháp function
nhé.
partial.js
const multiply = (a = 0) => (b = 0) => { return a * b }
Các giá trị đầu vào mặc định
Một điểm mới nữa về các hàm, đó là chúng ta sẽ có thể thiết lập giá trị mặc định cho các biến đầu vào của hàm.
Điều này sẽ giúp chúng ta đảm bảo là hàm sẽ hoạt động tốt trong trường hợp không có
dữ liệu truyền vào hàm ở những tình huống cụ thể, chứ không báo lỗi hay trả về một giá trị vô nghĩa đối với đoạn code tiếp theo sau lời gọi hàm đó.
Bên cạnh đó thì việc thiết lập giá trị mặc định cho các biến đầu vào sẽ giúp cho các trình soạn thảo code nhận diện được kiểu giá trị của biến đó, và sẽ gợi ý cho chúng ta các hàm liên quan có thể muốn sử dụng khi chúng ta gõ tên biến. Tới đây thì mình muốn giới thiệu tới bạn một trình soạn thảo code miễn phí khác là Visual Studio Code của Microsoft. Nó thực sự rất tuyệt khi làm việc với JavaScript; Còn với Atom thì chúng ta sẽ cần phải cài thêm một vài plug-in
hỗ trợ để được gợi ý code JavaScript tốt hơn.
parameter.js
const fixNum =
function(a = 0) { return a.toFixed(2);
}; console.log( fixNum() );
// kết quả: '0.00' console.log( fixNum(10.0123456789) );
// kết quả: '10.01'
Thực ra vẫn còn một vài điều nữa mà chúng ta chưa tìm hiểu hết về các hàm được tạo ra bởi function
và =>
. Tuy nhiên những kiến thức đó lại có phần liên quan tới object
nên chúng ta sẽ để dành sang bài viết tiếp theo về Object & Everything
. Còn bây giờ thì chúng ta sẽ gặp gỡ một kiểu hàm mới.
Các hàm function*
Đây là một kiểu hàm mới chứ không phải là một cú pháp thay thế như cú pháp =>
. Các hàm function*
còn được gọi là các generator
- các hàm sản xuất ra các giá trị. Trước khi nói thêm về kiểu hàm mới này, chúng ta cần một đoạn code ví dụ:
generator.js
function* createNumbers() { yield 1; yield 2; yield 3;
}; var numberGenerator = createNumbers(); var one = numberGenerator.next().value;
console.log(one);
// kết quả: 1 var two = numberGenerator.next().value;
console.log(two);
// kết quả: 2 var three = numberGenerator.next().value;
console.log(three);
// kết quả: 3
Thay vì trả về một giá trị cố định, một hàm function*
sẽ trả về một object thực thể của class Generator. Hàm createNumbers()
ban đầu sẽ được lưu lại trong object numberGenerator
này nhưng code bên trong hàm chưa được thực thi ngay.
Khi chúng ta gọi hàm next()
(đi tiếp) lần đầu tiên, hàm createNumbers()
mới thực sự được thực thi và dừng lại ở câu lệnh yield
đầu tiên trả về giá trị là 1
và được lưu vào biến value
của object numberGenerator
. Lời gọi hàm next()
sau khi chạy hàm createNumbers()
xong sẽ trả về chính object numberGenerator
và chúng ta tiếp tục truy xuất tới biến value
để lấy ra kết quả của lần chạy hàm đầu tiên.
Ở lần gọi hàm next()
tiếp theo thì hàm numberGenerator()
bắt đầu chạy tiếp từ điểm dừng lần trước và đi tới câu lệnh yield
tiếp theo để trả về giá trị là 2
và được lưu tiếp vào biến value
của numberGenerator
. Và cứ như vậy...
Như bạn thấy thì các hàm function*
sẽ rất hữu ích khi chúng ta muốn trả về các giá trị theo từng phần. Ngược lại với logic truyền các giá trị vào hàm từng phần ở phía trên.
Hoặc, trong trường hợp chúng ta muốn trả về một tập giá trị vô hạn, nhưng chỉ khi chúng ta cần sử dụng tới đâu thì generator
mới thực hiện tính toán và cung cấp ra giá trị tới đó. Tuy nhiên để làm vậy thì chúng ta sẽ cần tới sự hỗ trợ của một cấu trúc lặp có thể lặp vô hạn mà chúng ta chưa được gặp gỡ. Hãy tạm ghi nhớ điểm này và nhắc mình khi chúng ta tìm hiểu về các cấu trúc lặp trong một bài viết sau này nhé.
Lưu ý cuối cùng về các object Generator
, đó là để kiểm tra trạng thái của hàm thực thi bên trong generator
, chúng ta truy xuất tới biến done
. Nếu thu được giá trị true
thì có nghĩa là hàm thực thi bên trong đã được chạy đến hết code định nghĩa. Còn nếu là false
thì chúng ta vẫn còn có thể tiếp tục gọi hàm next()
để lấy ra giá trị tiếp theo.
Để để kết thúc generator
trước khi hàm thực thi bên trong được chạy xong thì chúng ta có thể gọi hàm return(oneValue)
, giá trị oneValue
sẽ được gán cho biến value
của generator
.
generator
function* createNumbers() { yield 1; yield 2; yield 3;
}; var numberGenerator = createNumbers(); var one = numberGenerator.next().value;
console.log(one);
// kết quả: 1 numberGenerator.return(10);
console.log( numberGenerator.value ); // 10
console.log( numberGenerator.done ); // true
Nói thêm về khái niệm Vùng scope
Lần này gặp lại khái niệm Vùng scope
chúng ta sẽ đi qua nhanh thôi vì căn bản chúng ta đã hiểu scope
là gì rồi.
Trong bài viết trước thì chúng ta được gặp 2 từ khóa mới const
và let
giúp chúng ta tạo ra những chiếc hộp lưu giá trị - bên cạnh từ khóa var
đã biết. Điểm khác biệt cơ bản là const
và let
có khả năng tạo vùng hoạt động nhỏ hơn so với cấp độ của hàm function
. Hãy cùng xem xét ví dụ sau.
scope.js
const printNumbers = function() { var theVar = 1; let theLet = 2; if (true) { var theVar = 3; let theLet = 4; } console.log(theVar); // 3 console.log(theLet); // 2
}; printNumbers();
Như bạn thấy trong kết quả được in ra, biến theVar
ban đầu được ghi đè lại bởi đoạn khai báo bên trong khối if
nhưng biến theLet
ban đầu thì không bị ghi đè. Lý do là vì đoạn khai báo theLet
bên trong khối if
sẽ tạo ra một biến mới với phạm vi hoạt động scope
giới hạn bên trong cặp ngoặc xoắn {}
của khối if
.
Vì vậy nên các khóa const
và let
được xem là các giá trị hỗ trợ block scope
(các khối lệnh được khoanh vùng bởi một cặp ngoặc xoắn {}
bất kỳ), còn var
thì không.
scope.js
var theVar = 1;
let theLet = 2; { var theVar = 3; // ghi đè let theLet = 4; // biến mới
}
Vùng scope
lớn nhất
Code JavaScript ban đầu, được viết ở bên ngoài tất cả các cặp ngoặc xoắn {}
, thuộc scope
lớn nhất và thuộc về object
mô tả môi trường chạy JavaScript. Trong các trình duyệt web thì đó là window
, còn ở môi trường NodeJS thì là một object
rỗng.
Khi chúng ta khai báo các biến var
ở scope
này thì nó trở thành thuộc tính gắn với object
môi trường và có thể truy xuất ở bất kỳ đâu. Do đó còn được gọi là các biến toàn cục global
. Tuy nhiên khóa const
và let
ở mặt khác lại được thiết kế để không
hoạt động như vậy.
global.js
var globalBox = 'something';
console.log( window.globalBox );
// 'something'
Vậy chúng ta nên dùng var
, let
, hay const
?
Tùy vào mục đích sử dụng trong từng tình huống và người viết code thôi. Bạn có thể dùng bất kỳ cái nào bạn ưa thích và với mục đích nào đó để đem lại tiện ích nhất định cho code của bạn.
Mình thì hay dùng const
để khai báo hàm ở global scope
để tránh bị khai báo đè, hoặc ghi đè giá trị nào đó vào ở đoạn code xa xa sau khi đã hơi quên đoạn code trước đó; Và dùng var
cho các biến khai báo trong hàm để sử dụng vì mình thường viết các hàm ngắn nên có ít biến và dễ theo dõi nên không ngại bị khai báo lặp; Và vì từ khóa var
có tính mô tả tốt hơn let
nên người đọc không biết gì về JavaScript thì cũng lơ mơ đoán ra được var
nó là variable
cũng giống như const
thì chắc là constant
. Việc sử dụng var
thay vì let
cũng là một dạng ràng buộc mình muốn tạo ra để bản thân buộc phải suy nghĩ về việc chia nhỏ tác vụ cần thực hiện thành các hàm nhỏ hơn, như vậy nó giúp mình tư duy tốt hơn khi viết code nữa.
Ok, như vậy là bài viết gặp lại các khái niệm Hàm & Vùng
của chúng ta tới đây đã hoàn thành. Trong bài viết tiếp theo, chúng ta sẽ được gặp lại Object & Everything
. Hãy nghỉ giải lao một chút trước khi tiếp tục. Hẹn gặp lại bạn trong bài viết tiếp theo.
(Sắp đăng tải) [JavaScript] Bài 12 - Object & Everything