Tại sao JavaScript lại có nhiều điểm lập dị đến vậy!? Ví dụ, tại sao
Có rất nhiều quyết định khó hiểu trong JavaScript có vẻ vô nghĩa; một số bị hiểu lầm, trong khi một số khác là những sai lầm trực tiếp trong thiết kế. Bất kể thế nào, bạn cũng nên biết những điều kỳ lạ này là gì và tại sao chúng lại có trong ngôn ngữ này. Tôi sẽ chia sẻ những điều tôi tin là kỳ quặc nhất về JavaScript và giải thích chúng.
Nhiều người trong chúng ta đã chế nhạo JavaScript bằng cách viết
Điều mà nhiều nhà phát triển có thể không biết là kết quả kỳ lạ này thực sự không phải là lỗi của JavaScript! JavaScript chỉ tuân thủ Tiêu chuẩn IEEE cho số học dấu phẩy động mà hầu như mọi máy tính và ngôn ngữ lập trình khác sử dụng để biểu diễn số.
Nhưng chính xác thì số học dấu phẩy động là gì?
Máy tính phải biểu diễn số ở mọi kích thước, từ khoảng cách giữa các hành tinh và thậm chí giữa các nguyên tử. Trên giấy, thật dễ dàng để viết một số lớn hoặc một số lượng nhỏ mà không cần lo lắng về kích thước mà nó sẽ chiếm. Máy tính không có sự xa xỉ đó vì chúng phải lưu tất cả các loại số dưới dạng nhị phân và một không gian nhỏ trong bộ nhớ.
Lấy một số nguyên 8 bit làm ví dụ. Trong hệ nhị phân, nó có thể chứa các số nguyên từ

Từ khóa ở đây là số nguyên. Nó không thể biểu diễn bất kỳ số thập phân nào giữa chúng. Để khắc phục điều này, chúng ta có thể thêm một dấu thập phân tưởng tượng ở đâu đó dọc theo 8 bit của mình để các bit trước dấu thập phân được sử dụng để biểu diễn phần nguyên và phần còn lại được sử dụng cho phần thập phân. Vì dấu thập phân luôn ở cùng một vị trí tưởng tượng, nên nó được gọi là dấu thập phân cố định. Nhưng nó đi kèm với một cái giá rất lớn vì phạm vi bị giảm từ

Có độ chính xác cao hơn có nghĩa là phải hy sinh phạm vi và ngược lại. Chúng ta cũng phải cân nhắc rằng máy tính cần phải làm hài lòng một lượng lớn người dùng với các yêu cầu khác nhau. Một kỹ sư xây cầu không quá lo lắng nếu các phép đo sai lệch chỉ một chút, chẳng hạn như một phần trăm centimet. Nhưng mặt khác, cùng một phần trăm centimet đó có thể khiến chi phí sản xuất vi mạch tăng cao hơn nhiều. Độ chính xác cần thiết là khác nhau và hậu quả của một lỗi có thể khác nhau.
Một cân nhắc khác là kích thước nơi lưu trữ các con số trong bộ nhớ vì việc lưu trữ các con số dài trong một thứ gì đó như megabyte là không khả thi.
Định dạng dấu phẩy động ra đời từ nhu cầu biểu diễn cả số lượng lớn và nhỏ một cách chính xác và hiệu quả. Nó thực hiện như vậy theo ba phần:

Định dạng dấu phẩy động 8 bit có thể biểu diễn các số từ
Động lực chính xác phức tạp hơn nhiều, nhưng hiện tại, chúng ta chỉ cần hiểu rằng trong khi định dạng này cho phép chúng ta biểu diễn các số trong phạm vi lớn, thì nó sẽ mất độ chính xác (khoảng cách giữa các giá trị có thể biểu diễn trở nên lớn hơn) khi chúng trở nên quá lớn. Ví dụ, các số JavaScript được trình bày theo định dạng dấu phẩy động độ chính xác kép, tức là mỗi số được biểu diễn bằng 64 bit trong bộ nhớ, để lại 53 bit để biểu diễn phần thập phân. Điều đó có nghĩa là JavaScript chỉ có thể biểu diễn an toàn các số nguyên trong khoảng từ –(253 — 1) đến 253 — 1 mà không mất độ chính xác. Ngoài ra, phép tính số học không còn có ý nghĩa nữa. Đó là lý do tại sao chúng ta có thuộc tính dữ liệu tĩnh
Nhưng
Để thấy điều này, chúng ta hãy biểu diễn một phần ba (1⁄3) theo cơ số 10.
Cho dù chúng ta cố gắng viết bao nhiêu chữ số, kết quả sẽ không bao giờ là chính xác một phần ba. Tương tự như vậy, chúng ta không thể biểu diễn chính xác một số số thập phân theo cơ số 2 hoặc nhị phân. Lấy ví dụ,
Rõ ràng là chúng ta không thể có một số lớn vô hạn, vì vậy tại một thời điểm nào đó, phần định trị phải bị cắt bớt, khiến cho không thể không mất độ chính xác trong quá trình này. Nếu chúng ta thử chuyển đổi
Không phải là 0.2! Chúng ta không thể biểu diễn nhiều giá trị phân số — không chỉ trong JavaScript mà còn trong hầu hết các máy tính. Vậy tại sao chạy
Đây là giá trị thực được lưu khi viết
Nếu chúng ta cộng thủ công các giá trị thực của
Giá trị đó được làm tròn thành
Số thực có những nhược điểm đã biết, nhưng ưu điểm của nó lớn hơn nhiều, và nó là chuẩn mực trên toàn thế giới. Theo nghĩa đó, thực sự nhẹ nhõm khi tất cả các hệ thống hiện đại sẽ cung cấp cho chúng ta cùng một kết quả
Ví dụ: JavaScript sẽ ép một chuỗi thành một số khi so sánh với một số khác:
Điều ngược lại cũng áp dụng cho toán tử dấu cộng (
Đó là lý do tại sao chúng ta chỉ nên sử dụng toán tử dấu cộng (
Lý do các phép ép như vậy nằm trong ngôn ngữ thực sự là vô lý. Khi người sáng tạo ra JavaScript Brendan Eich được hỏi rằng ông sẽ làm gì khác trong thiết kế của JavaScript, câu trả lời của ông là tỉ mỉ hơn trong việc triển khai mà những người dùng đầu tiên của ngôn ngữ này mong muốn:
Có rất nhiều quy tắc khác điều khiển toán tử bằng nhau lỏng lẻo (và các câu lệnh khác kiểm tra điều kiện) khiến các nhà phát triển JavaScript phải đau đầu. Chúng phức tạp, tẻ nhạt và vô nghĩa, vì vậy chúng ta nên tránh toán tử bằng nhau lỏng lẻo (
Tại sao chúng ta lại có hai toán tử bằng nhau ngay từ đầu? Rất nhiều yếu tố, nhưng chúng ta có thể chỉ ra Guy L. Steele, đồng sáng tạo ra ngôn ngữ lập trình Scheme. Ông đảm bảo với Eich rằng chúng ta luôn có thể thêm một toán tử bằng nhau khác vì có những phương ngữ với năm toán tử bằng nhau riêng biệt trong ngôn ngữ Lisp! Tâm lý này rất nguy hiểm và ngày nay, tất cả các tính năng đều phải được phân tích chặt chẽ vì chúng ta luôn có thể thêm các tính năng mới, nhưng một khi chúng đã có trong ngôn ngữ, chúng không thể bị xóa bỏ.
ASI có thể khiến một số mã hoạt động, nhưng hầu hết thời gian thì không. Hãy xem đoạn mã sau:
Bạn có thể thấy dấu chấm phẩy nằm ở đâu và nếu chúng ta định dạng đúng, nó sẽ kết thúc như sau:
Nhưng nếu chúng ta đưa mã trước đó trực tiếp vào JavaScript, tất cả các loại ngoại lệ sẽ được đưa ra vì nó sẽ giống như khi viết thế này:
Cuối cùng, hãy biết dấu chấm phẩy của bạn.
Mọi thứ trong JavaScript đều có thể được coi là một đối tượng, ngoại trừ hai giá trị bottom là
Lưu ý rằng, xét một cách nghiêm ngặt, tất cả các giá trị nguyên thủy đều không phải là đối tượng. Nhưng chỉ có
Chúng ta thậm chí có thể coi
Mặt khác,
Không có cách nào khác trừ khi chúng ta kiểm tra từng giá trị thuộc tính trước khi cố gắng truy cập giá trị tiếp theo, bằng cách sử dụng AND logic (
Tôi đã nói rằng
Chúng ta có thể kiểm tra cả ba giá trị dưới cùng bằng phép thử sau:
Tăng (
Là nhà phát triển, chúng ta có xu hướng dành nhiều thời gian để đọc mã hơn là viết mã. Cho dù chúng ta đang đọc tài liệu, xem xét công việc của người khác hay kiểm tra công việc của chính mình, khả năng đọc mã sẽ làm tăng năng suất của chúng ta hơn là sự ngắn gọn. Nói cách khác, khả năng đọc tiết kiệm thời gian về lâu dài.
Đó là lý do tại sao tôi thích sử dụng
Thật phi logic khi có một cú pháp khác dành riêng cho việc tăng giá trị thêm một ngoài việc có dạng tiền tăng và dạng hậu tăng, tùy thuộc vào vị trí đặt toán tử. Rất dễ đảo ngược chúng và điều đó có thể khó gỡ lỗi. Chúng không nên có một vị trí trong mã của bạn hoặc thậm chí trong toàn bộ ngôn ngữ khi chúng ta xem xét các toán tử gia tăng đến từ đâu.
Như chúng ta đã thấy trong bài viết trước, cú pháp JavaScript lấy cảm hứng rất nhiều từ ngôn ngữ C, ngôn ngữ sử dụng các biến con trỏ. Các biến con trỏ được thiết kế để lưu trữ địa chỉ bộ nhớ của các biến khác, cho phép phân bổ và thao tác bộ nhớ động. Các toán tử
Ngày nay, số học con trỏ đã được chứng minh là có hại và có thể gây ra truy cập vô tình vào các vị trí bộ nhớ vượt ra ngoài ranh giới dự định của mảng hoặc bộ đệm, dẫn đến lỗi bộ nhớ, một nguồn lỗi và lỗ hổng bảo mật khét tiếng. Bất kể thế nào, cú pháp đã đi vào JavaScript và vẫn ở đó cho đến ngày nay.
Mặc dù việc sử dụng
Nhìn chung, đây không phải là tình huống sống còn mà là cách hay để làm cho mã của bạn dễ đọc hơn.
Dù bằng cách nào, tôi cũng khuyến khích mọi nhà phát triển nghiên cứu và tìm hiểu thêm về ngôn ngữ này. Và nếu bạn quan tâm, tôi sẽ đi sâu hơn một chút vào các lĩnh vực đáng ngờ trong thiết kế của JavaScript trong một bài viết khác được xuất bản tại đây trên Tạp chí Smashing!
0,2 + 0,1
lại bằng 0,30000000000000004
? Hoặc, tại sao "" == false
lại được đánh giá là true
?Có rất nhiều quyết định khó hiểu trong JavaScript có vẻ vô nghĩa; một số bị hiểu lầm, trong khi một số khác là những sai lầm trực tiếp trong thiết kế. Bất kể thế nào, bạn cũng nên biết những điều kỳ lạ này là gì và tại sao chúng lại có trong ngôn ngữ này. Tôi sẽ chia sẻ những điều tôi tin là kỳ quặc nhất về JavaScript và giải thích chúng.
0.1 + 0.2
và Định dạng Dấu phẩy động
Nhiều người trong chúng ta đã chế nhạo JavaScript bằng cách viết 0.1 + 0.2
trong bảng điều khiển và thấy nó không đạt được 0.3
mà là một giá trị 0.30000000000000004
trông rất buồn cười.Điều mà nhiều nhà phát triển có thể không biết là kết quả kỳ lạ này thực sự không phải là lỗi của JavaScript! JavaScript chỉ tuân thủ Tiêu chuẩn IEEE cho số học dấu phẩy động mà hầu như mọi máy tính và ngôn ngữ lập trình khác sử dụng để biểu diễn số.
Nhưng chính xác thì số học dấu phẩy động là gì?
Máy tính phải biểu diễn số ở mọi kích thước, từ khoảng cách giữa các hành tinh và thậm chí giữa các nguyên tử. Trên giấy, thật dễ dàng để viết một số lớn hoặc một số lượng nhỏ mà không cần lo lắng về kích thước mà nó sẽ chiếm. Máy tính không có sự xa xỉ đó vì chúng phải lưu tất cả các loại số dưới dạng nhị phân và một không gian nhỏ trong bộ nhớ.
Lấy một số nguyên 8 bit làm ví dụ. Trong hệ nhị phân, nó có thể chứa các số nguyên từ
0
đến 255
.
Từ khóa ở đây là số nguyên. Nó không thể biểu diễn bất kỳ số thập phân nào giữa chúng. Để khắc phục điều này, chúng ta có thể thêm một dấu thập phân tưởng tượng ở đâu đó dọc theo 8 bit của mình để các bit trước dấu thập phân được sử dụng để biểu diễn phần nguyên và phần còn lại được sử dụng cho phần thập phân. Vì dấu thập phân luôn ở cùng một vị trí tưởng tượng, nên nó được gọi là dấu thập phân cố định. Nhưng nó đi kèm với một cái giá rất lớn vì phạm vi bị giảm từ
0
đến 255
xuống chính xác 0
đến 15.9375
.
Có độ chính xác cao hơn có nghĩa là phải hy sinh phạm vi và ngược lại. Chúng ta cũng phải cân nhắc rằng máy tính cần phải làm hài lòng một lượng lớn người dùng với các yêu cầu khác nhau. Một kỹ sư xây cầu không quá lo lắng nếu các phép đo sai lệch chỉ một chút, chẳng hạn như một phần trăm centimet. Nhưng mặt khác, cùng một phần trăm centimet đó có thể khiến chi phí sản xuất vi mạch tăng cao hơn nhiều. Độ chính xác cần thiết là khác nhau và hậu quả của một lỗi có thể khác nhau.
Một cân nhắc khác là kích thước nơi lưu trữ các con số trong bộ nhớ vì việc lưu trữ các con số dài trong một thứ gì đó như megabyte là không khả thi.
Định dạng dấu phẩy động ra đời từ nhu cầu biểu diễn cả số lượng lớn và nhỏ một cách chính xác và hiệu quả. Nó thực hiện như vậy theo ba phần:
- Một bit duy nhất biểu thị số đó là số dương hay số âm (
0
là số dương,1
là số âm). - Một significand hoặc mantissa chứa các chữ số của số đó.
- Một số mũ chỉ định vị trí dấu thập phân (hoặc nhị phân) được đặt so với phần đầu của mantissa, tương tự như cách ký hiệu khoa học hoạt động. Do đó, điểm có thể di chuyển đến bất kỳ vị trí nào, do đó có điểm nổi.

Định dạng dấu phẩy động 8 bit có thể biểu diễn các số từ
0,0078
đến 480
(và các số âm của nó), nhưng hãy lưu ý rằng biểu diễn dấu phẩy động không thể biểu diễn tất cả các số trong phạm vi đó. Điều đó là không thể vì 8 bit chỉ có thể biểu diễn 256 giá trị riêng biệt. Không thể tránh khỏi, nhiều số không thể được biểu diễn chính xác. Có khoảng trống dọc theo phạm vi. Tất nhiên, máy tính hoạt động với nhiều bit hơn để tăng độ chính xác và phạm vi, thường là 32 bit và 64 bit, nhưng không thể biểu diễn chính xác tất cả các số, một cái giá nhỏ phải trả nếu chúng ta xem xét phạm vi chúng ta đạt được và bộ nhớ chúng ta tiết kiệm được.Động lực chính xác phức tạp hơn nhiều, nhưng hiện tại, chúng ta chỉ cần hiểu rằng trong khi định dạng này cho phép chúng ta biểu diễn các số trong phạm vi lớn, thì nó sẽ mất độ chính xác (khoảng cách giữa các giá trị có thể biểu diễn trở nên lớn hơn) khi chúng trở nên quá lớn. Ví dụ, các số JavaScript được trình bày theo định dạng dấu phẩy động độ chính xác kép, tức là mỗi số được biểu diễn bằng 64 bit trong bộ nhớ, để lại 53 bit để biểu diễn phần thập phân. Điều đó có nghĩa là JavaScript chỉ có thể biểu diễn an toàn các số nguyên trong khoảng từ –(253 — 1) đến 253 — 1 mà không mất độ chính xác. Ngoài ra, phép tính số học không còn có ý nghĩa nữa. Đó là lý do tại sao chúng ta có thuộc tính dữ liệu tĩnh
Number.MAX_SAFE_INTEGER
để biểu diễn số nguyên an toàn lớn nhất trong JavaScript, đó là (253 — 1) hoặc 9007199254740991
.Nhưng
0,3
rõ ràng là thấp hơn ngưỡng MAX_SAFE_INTEGER
, vậy tại sao chúng ta không thể lấy được khi thêm 0,1
và 0,2
? Định dạng dấu phẩy động gặp khó khăn với một số số thập phân. Đây không phải là vấn đề với định dạng dấu phẩy động, nhưng chắc chắn là vấn đề với bất kỳ hệ thống số nào.Để thấy điều này, chúng ta hãy biểu diễn một phần ba (1⁄3) theo cơ số 10.
Mã:
0,3
Mã:
0,33
Mã:
0,3333333 [...]
0,2
. Chúng ta có thể viết nó mà không có vấn đề gì ở hệ cơ số 10, nhưng nếu chúng ta thử viết nó ở hệ nhị phân, chúng ta sẽ nhận được một 1001
tuần hoàn ở cuối và lặp lại vô hạn.
Mã:
0.001 1001 1001 1001 1001 1001 1001 10 [...]
0.2
từ số dấu phẩy động độ chính xác kép trở lại cơ số 10, chúng ta sẽ thấy giá trị thực được lưu trong bộ nhớ:
Mã:
0.200000000000000011102230246251565404236316680908203125
0.2 + 0.2
lại tính đúng 0.4
? Trong trường hợp này, độ không chính xác quá nhỏ đến mức được Javascript làm tròn (ở chữ số thập phân thứ 16), nhưng đôi khi độ không chính xác đủ để thoát khỏi cơ chế làm tròn, như trường hợp của 0.2 + 0.1
. Chúng ta có thể thấy điều gì đang xảy ra bên dưới nếu chúng ta cố gắng cộng các giá trị thực của 0,1
và 0,2
.Đây là giá trị thực được lưu khi viết
0,1
:
Mã:
0,100000000000000055511151231257827021181583404541015625
0,1
và 0,2
, chúng ta sẽ thấy thủ phạm:
Mã:
0,3000000000000000444089209850062616169452667236328125
0,30000000000000004
. Bạn có thể kiểm tra các giá trị thực được lưu tại float.exposed.Số thực có những nhược điểm đã biết, nhưng ưu điểm của nó lớn hơn nhiều, và nó là chuẩn mực trên toàn thế giới. Theo nghĩa đó, thực sự nhẹ nhõm khi tất cả các hệ thống hiện đại sẽ cung cấp cho chúng ta cùng một kết quả
0,30000000000000004
trên mọi kiến trúc. Có thể đây không phải là kết quả bạn mong đợi, nhưng đó là kết quả bạn có thể dự đoán được.Ép kiểu
JavaScript là ngôn ngữ được gõ động, nghĩa là chúng ta không cần phải khai báo kiểu của biến và có thể thay đổi sau trong mã.Vấn đề xuất phát từ việc được gõ yếu vì có nhiều trường hợp ngôn ngữ sẽ cố gắng thực hiện chuyển đổi ngầm giữa các kiểu khác nhau, ví dụ, từ chuỗi sang số hoặc các giá trị falsy và truthy. Điều này đặc biệt đúng khi sử dụng toán tử bằng (
==
) và dấu cộng (+
). Các quy tắc để ép kiểu rất phức tạp, khó nhớ và thậm chí không chính xác trong một số trường hợp. Tốt hơn hết là tránh sử dụng ==
và luôn ưu tiên toán tử bằng nghiêm ngặt (===
).Ví dụ: JavaScript sẽ ép một chuỗi thành một số khi so sánh với một số khác:
Mã:
console.log("2" == 2); // true
+
). Nó sẽ cố gắng ép một số thành một chuỗi khi có thể:
Mã:
console.log(2 + "2"); // "22"
+
) nếu chúng ta chắc chắn rằng các giá trị là số. Khi nối các chuỗi, tốt hơn là sử dụng phương thức concat()
hoặc template literals.Lý do các phép ép như vậy nằm trong ngôn ngữ thực sự là vô lý. Khi người sáng tạo ra JavaScript Brendan Eich được hỏi rằng ông sẽ làm gì khác trong thiết kế của JavaScript, câu trả lời của ông là tỉ mỉ hơn trong việc triển khai mà những người dùng đầu tiên của ngôn ngữ này mong muốn:
Ví dụ rõ ràng nhất là lý do tại sao chúng ta có hai toán tử bằng nhau,“Tôi sẽ tránh một số thỏa hiệp mà tôi đã thực hiện khi lần đầu tiên có những người dùng đầu tiên và họ nói, "Bạn có thể thay đổi điều này không?"
— Brendan Eich
==
và ===
. Khi một người dùng JavaScript đầu tiên nhắc nhở anh ta về nhu cầu so sánh một số với một chuỗi mà không cần phải thay đổi mã của mình để thực hiện chuyển đổi, Brendan đã thêm toán tử bằng nhau lỏng lẻo để đáp ứng những nhu cầu đó.Có rất nhiều quy tắc khác điều khiển toán tử bằng nhau lỏng lẻo (và các câu lệnh khác kiểm tra điều kiện) khiến các nhà phát triển JavaScript phải đau đầu. Chúng phức tạp, tẻ nhạt và vô nghĩa, vì vậy chúng ta nên tránh toán tử bằng nhau lỏng lẻo (
==
) bằng mọi giá và thay thế nó bằng từ đồng âm chặt chẽ của nó (===
).Tại sao chúng ta lại có hai toán tử bằng nhau ngay từ đầu? Rất nhiều yếu tố, nhưng chúng ta có thể chỉ ra Guy L. Steele, đồng sáng tạo ra ngôn ngữ lập trình Scheme. Ông đảm bảo với Eich rằng chúng ta luôn có thể thêm một toán tử bằng nhau khác vì có những phương ngữ với năm toán tử bằng nhau riêng biệt trong ngôn ngữ Lisp! Tâm lý này rất nguy hiểm và ngày nay, tất cả các tính năng đều phải được phân tích chặt chẽ vì chúng ta luôn có thể thêm các tính năng mới, nhưng một khi chúng đã có trong ngôn ngữ, chúng không thể bị xóa bỏ.
Chèn dấu chấm phẩy tự động
Khi viết mã trong JavaScript, dấu chấm phẩy (;
) là bắt buộc ở cuối một số câu lệnh, bao gồm:-
var
,let
,const
; - Câu lệnh biểu thức;
-
do...while
; -
continue
,break
,return
,throw
; -
debugger
; - Khai báo trường lớp (public hoặc private);
-
import
,export
.
ASI có thể khiến một số mã hoạt động, nhưng hầu hết thời gian thì không. Hãy xem đoạn mã sau:
Mã:
const a = 1(1).toString()const b = 1[1, 2, 3].forEach(console.log)
Mã:
const a = 1;(1).toString();const b = 1;[(1, 2, 3)].forEach(console.log);
Mã:
const a = 1(1).toString();const b = (1)[(1, 2, 3)].forEach(console.log);
Tại sao lại có nhiều giá trị bottom như vậy?
Thuật ngữ "bottom" thường được dùng để biểu thị một giá trị không tồn tại hoặc không xác định. Nhưng tại sao chúng ta lại có hai loại giá trị bottom trong JavaScript?Mọi thứ trong JavaScript đều có thể được coi là một đối tượng, ngoại trừ hai giá trị bottom là
null
và undefined
(mặc dù typeof null
trả về object
). Việc cố gắng lấy giá trị thuộc tính từ chúng sẽ gây ra một ngoại lệ.Lưu ý rằng, xét một cách nghiêm ngặt, tất cả các giá trị nguyên thủy đều không phải là đối tượng. Nhưng chỉ có
null
và undefined
không phải chịu boxing.Chúng ta thậm chí có thể coi
NaN
là giá trị đáy thứ ba biểu thị sự vắng mặt của một số. Sự phong phú của các giá trị đáy nên được coi là một lỗi thiết kế. Không có lý do trực tiếp nào giải thích cho sự tồn tại của hai giá trị đáy, nhưng chúng ta có thể thấy sự khác biệt trong cách JavaScript sử dụng chúng.undefined
là giá trị đáy mà JavaScript sử dụng theo mặc định, do đó, việc sử dụng nó độc quyền trong mã của bạn được coi là một thông lệ tốt. Khi chúng ta định nghĩa một biến mà không có giá trị ban đầu, việc cố gắng truy xuất nó sẽ gán giá trị undefined
. Điều tương tự cũng xảy ra khi chúng ta cố gắng truy cập một thuộc tính không tồn tại từ một đối tượng. Để khớp với hành vi của JavaScript một cách chặt chẽ nhất có thể, hãy sử dụng undefined
để biểu thị một thuộc tính hoặc biến hiện có không có giá trị.Mặt khác,
null
được sử dụng để biểu thị sự vắng mặt của một đối tượng (do đó, typeof
của nó trả về một object
mặc dù nó không phải). Tuy nhiên, điều này được coi là một lỗi thiết kế vì undefined
có thể thực hiện mục đích của nó một cách hiệu quả. JavaScript sử dụng nó để biểu thị sự kết thúc của một cấu trúc dữ liệu đệ quy. Cụ thể hơn, nó được sử dụng trong chuỗi nguyên mẫu để biểu thị sự kết thúc của nó. Hầu hết thời gian, bạn có thể sử dụng undefined
thay cho null
, nhưng có một số trường hợp chỉ có thể sử dụng null
, như trường hợp của Object.create
trong đó chúng ta chỉ có thể tạo một đối tượng mà không có nguyên mẫu truyền null
; sử dụng undefined
trả về TypeError
.null
và undefined
đều gặp phải sự cố đường dẫn. Khi cố gắng truy cập một thuộc tính từ giá trị dưới cùng — như thể chúng là các đối tượng — các ngoại lệ sẽ được đưa ra.
Mã:
let user;let userName = user.name; // Uncaught TypeErrorlet userNick = user.name.nick; // Uncaught TypeError
&&
) hoặc chuỗi tùy chọn (?
).
Mã:
let user;let userName = user?.name;let userNick = user && user.name && user.name.nick;console.log(userName); // undefinedconsole.log(userNick); // undefined
NaN
có thể được coi là giá trị dưới cùng, nhưng nó có vị trí gây nhầm lẫn riêng trong JavaScript vì nó biểu diễn các số không phải là số thực, thường là do chuyển đổi chuỗi sang số không thành công (đây là một lý do khác để tránh nó). NaN
có những trò hề riêng vì nó không bằng chính nó! Để kiểm tra xem một giá trị có phải là NaN
hay không, hãy sử dụng Number.isNaN()
.Chúng ta có thể kiểm tra cả ba giá trị dưới cùng bằng phép thử sau:
Mã:
function stringifyBottom(bottomValue) { if (bottomValue === undefined) { return "undefined"; } if (bottomValue === null) { return "null"; } if (Number.isNaN(bottomValue)) { return "NaN"; }}
Tăng (++
) và Giảm (--
)
Là nhà phát triển, chúng ta có xu hướng dành nhiều thời gian để đọc mã hơn là viết mã. Cho dù chúng ta đang đọc tài liệu, xem xét công việc của người khác hay kiểm tra công việc của chính mình, khả năng đọc mã sẽ làm tăng năng suất của chúng ta hơn là sự ngắn gọn. Nói cách khác, khả năng đọc tiết kiệm thời gian về lâu dài.Đó là lý do tại sao tôi thích sử dụng
+ 1
hoặc - 1
hơn là các toán tử tăng (++
) và giảm (--
).Thật phi logic khi có một cú pháp khác dành riêng cho việc tăng giá trị thêm một ngoài việc có dạng tiền tăng và dạng hậu tăng, tùy thuộc vào vị trí đặt toán tử. Rất dễ đảo ngược chúng và điều đó có thể khó gỡ lỗi. Chúng không nên có một vị trí trong mã của bạn hoặc thậm chí trong toàn bộ ngôn ngữ khi chúng ta xem xét các toán tử gia tăng đến từ đâu.
Như chúng ta đã thấy trong bài viết trước, cú pháp JavaScript lấy cảm hứng rất nhiều từ ngôn ngữ C, ngôn ngữ sử dụng các biến con trỏ. Các biến con trỏ được thiết kế để lưu trữ địa chỉ bộ nhớ của các biến khác, cho phép phân bổ và thao tác bộ nhớ động. Các toán tử
++
và --
ban đầu được tạo ra cho mục đích cụ thể là tiến hoặc lùi qua các vị trí bộ nhớ.Ngày nay, số học con trỏ đã được chứng minh là có hại và có thể gây ra truy cập vô tình vào các vị trí bộ nhớ vượt ra ngoài ranh giới dự định của mảng hoặc bộ đệm, dẫn đến lỗi bộ nhớ, một nguồn lỗi và lỗ hổng bảo mật khét tiếng. Bất kể thế nào, cú pháp đã đi vào JavaScript và vẫn ở đó cho đến ngày nay.
Mặc dù việc sử dụng
++
và --
vẫn là một tiêu chuẩn trong số các nhà phát triển, nhưng có thể đưa ra lập luận về khả năng đọc. Việc lựa chọn + 1
hoặc - 1
thay vì ++
và --
không chỉ phù hợp với các nguyên tắc về tính rõ ràng và minh bạch mà còn tránh phải xử lý dạng trước khi tăng dần và dạng sau khi tăng dần.Nhìn chung, đây không phải là tình huống sống còn mà là cách hay để làm cho mã của bạn dễ đọc hơn.
Kết luận
Các tính năng có vẻ vô nghĩa của JavaScript thường xuất phát từ các quyết định, thỏa hiệp và nỗ lực đáp ứng mọi nhu cầu trong quá khứ. Thật không may, không thể làm hài lòng tất cả mọi người và JavaScript cũng không ngoại lệ.Tôi hy vọng bạn thấy việc tiếp tục tìm hiểu thêm về JavaScript và lịch sử của nó để nắm bắt các tính năng bị hiểu lầm và các quyết định đáng ngờ của nó là xứng đáng. Lấy bản chất nguyên mẫu tuyệt vời của nó làm ví dụ. Nó đã bị che khuất trong quá trình phát triển hoặc những sai lầm như từ khóa
this
và hành vi đa mục đích của nó.Dù bằng cách nào, tôi cũng khuyến khích mọi nhà phát triển nghiên cứu và tìm hiểu thêm về ngôn ngữ này. Và nếu bạn quan tâm, tôi sẽ đi sâu hơn một chút vào các lĩnh vực đáng ngờ trong thiết kế của JavaScript trong một bài viết khác được xuất bản tại đây trên Tạp chí Smashing!