Table of Contents
Bạn không biết JavaScript. Chuẩn đấy. Mình xin lỗi vì cái tiêu đề hơi chướng tai gai mắt này, nhưng phải nói thật rằng là, bạn không hiểu rõ JavaScript. Cho dù bạn đang ở cấp bậc nào hay bạn có bao nhiêu năm kinh nghiệm đi chăng nữa, thì bạn phải dũng cảm lắm mới dám nói rằng mình hiểu được tường tận cái ngôn ngữ lằng nhằng phức tạp này.
Nhưng mà không sao đâu! Bạn, mình, và tất cả các lập trình viên JavaScript đều cùng hội cùng thuyền cả thôi, mò mẫm dò đường trong đại dương kiến thức mênh mông, rộng lớn này. Và đó là cái đẹp của JavaScript. Ừ, một vài lúc ta sẽ thấy nó khó đấy, rắc rối đấy, nhưng mà cũng như một bản nhạc hay phải chứa đựng cả những nốt thăng lẫn những nốt trầm, JavaScript cũng chứa đựng tất cả những cung bậc cảm xúc đáng nhớ nhất của một lập trình viên. Tận hưởng sự rắc rối của JavaScript là bước đầu tiên để gỡ sợi tơ vò của thứ ngôn ngữ này. Và vì vậy, hôm nay mình sẽ giới thiệu một số kiến thức lõi về JavaScript mà từng khiến mình đau não vãi cả chưởng ra, và rồi hãy cùng mình phân tích từng vấn đề một nhé!
Table of Contents
Ép kiểu JavaScript (Type coercion)
Ép kiểu là một thuật ngữ được sử dụng trong JavaScript để mô tả việc chuyển đổi từ kiểu dữ liệu này sang kiểu dữ liệu khác. Đây không phải là một thuật ngữ mới lạ gì và mình nghĩ rằng tất cả chúng ta khi lần đầu tiên tiếp cận ngôn ngữ này đều được giới thiệu với ví dụ này để cho thấy JavaScript kỳ dị như thế nào:
Và nếu bạn nghĩ thế là bạn đủ hiểu ép kiểu rồi, “ừ, số cộng chuỗi thì ra chuỗi, sỗ trừ chuỗi lại ra số…”, vậy thử giải thích cái này đi:
Ép kiểu JavaScript khó hiểu như vậy là vì JavaScript được coi là ngôn ngữ có kiểu dữ liệu yếu. Tính năng này cho phép các loại dữ liệu có thể thay đổi một cách linh hoạt và tự động chuyển đổi khi cần thiết. Trong JavaScript, có hai loại ép kiểu: ép kiểu tường minh (explicit) và ép kiểu ngầm định (implicit). Ép kiểu tường minh là khi lập trình viên chủ động chuyển đổi kiểu dữ liệu của một biểu thức từ kiểu này sang kiểu khác bằng cách sử dụng các toán tử hoặc hàm ép kiểu, như Number() hoặc String(). Mặt khác, ép kiểu ngầm định là sự chuyển đổi kiểu dữ liệu của một biểu thức sang kiểu dữ liệu khác khi thực hiện phép tính hoặc so sánh. Ví dụ, khi một chuỗi và một số được cộng với nhau, JavaScript sẽ tự động chuyển đổi số thành chuỗi và thực hiện phép cộng hai chuỗi mà không cần sự can thiệp của lập trình viên, hay nói cách khác, bạn tin vào JavaScript (tại sao vậy?). Nói vậy thôi, chứ ép kiểu là một trong những thuộc tính rất hữu dụng của JavaScript, miễn là ta hiểu được cách hai loại ép kiểu hoạt động để sử dụng chúng đúng cách và nhuần nhuyễn. Cheat sheet này sẽ giúp bạn hiểu rõ hơn về ép kiểu JavaScript, và nhớ rằng, hãy dùng “===” thay vì “==”, nếu có thể.
Fun fact: Nhờ có sự kỳ cục của việc chuyển đổi kiểu dữ liệu này mà bạn có thể viết được mọi dòng code JavaScript chỉ với 6 (!) ký tự. Có thời gian thì bạn hãy nghía qua JSFuck nhé, cảnh báo hại não đã được bật, proceed with caution!
Closure
Ép kiểu suy cho cùng thì cũng mới chỉ làm khó được những người mới học JavaScript thôi, nhưng mà closure thì lại là một vấn đề kha khá nâng cao đó nhé. Mới nhìn qua thì có thể cũng không quá khó hiểu, nhưng mà càng đi sâu vào, chúng ta lại càng thấy những sự rối rắm trong đó.
Cho đoạn code sau đây:
Như bạn thấy, hàm outer() tạo ra một một biến cục bộ “name”, và hàm inner() cho dù không có biến cục bộ nào, vẫn có thể truy cập các biến của hàm bên ngoài, và khi cho chạy hàm này thì ta thấy giá trị của biến “name” được in ra. Khái niệm này được gọi là “phạm vi từ vựng” (lexical scoping), là một tính năng trong ngôn ngữ lập trình mà quyết định phạm vi của một biến dựa trên nơi khai báo biến trong mã nguồn. Trong JavaScript, phạm vi từ vựng được xác định bởi vị trí mà biến được khai báo trong mã nguồn, và có lẽ những ai nắm vững kiến thức cơ bản của JS thì cũng đã nằm lòng được khái niệm này. Vậy, hãy nhìn vào đoạn code dưới đây
Đoạn code này cũng gần như tương tự, tuy nhiên, có một điểm khác biệt quan trọng: hàm inner() được trả về trong hàm outer() trước khi nó được gọi. Chính vì vậy, thoạt nhìn qua ta sẽ nghĩ là đoạn code này không hoạt động, vì các biến được xác định trong một hàm là tạm thời và chỉ khả dụng trong thời gian hàm đang chạy. Nhưng mà không. Mình nói rồi mà, chúng ta không biết rõ JavaScript! Đoạn code này chạy vẫn ngon và vẫn cho ra kết quả như đoạn code trước đó. Lý do cho hành vi này là tính năng closure của JavaScript. Closure là một tính năng trong JavaScript cho phép một hàm truy cập và sử dụng các biến được khai báo bên ngoài phạm vi của nó, và điều này cho phép lưu trữ và bảo vệ trạng thái của các biến và các giá trị trong một hàm ở giữa các lần gọi. Trong trường hợp của chúng ta, closure đã được tạo ra cho hàm inner(), gói gọn lại phạm vi từ vựng của hàm, bao gồm cả biến “name”.
Càng nghe càng rắc rối đúng không nào! Và nếu thế chưa đủ làm khó bạn, thì hãy nhìn vào đoạn code sau:
Đây là một ví dụ nho nhỏ về một chuỗi closure, chỉ để các bạn thấy là cái tính năng này đã từng làm mình khốn khổ thế nào. Bạn có thể đang tự hỏi là, thế học tất cả những mớ lý thuyết này rồi có bao giờ sử dụng chúng trong đời thực không? Thưa, có chứ! Closure đóng một vai trò quan trọng trong việc xác định phạm vi của các biến trong một hàm và cũng thiết lập các biến nào được chia sẻ giữa các hàm anh chị em trong cùng một phạm vi. Hiểu rõ ràng về mối quan hệ giữa các biến và hàm là rất quan trọng để viết nên được một đoạn code rành mạch và rõ ràng, cho dù bạn có đang lập trình hàm hay là hướng đối tượng đi chăng nữa.
Vậy, lần tiếp theo mà bạn phải đối mặt với closure hay chằng chịt những hàm chồng chéo nhau, đừng lo vì chúng ta đã cùng nhau giải thích và nắm bắt được khái niệm cốt lõi này rồi, và mình tin là giờ bạn đã sẵn sàng để ứng dụng nó ngoài đời thực rồi đó. Ừ, về lý thuyết là thế…
Fun fact: Khi mình đang viết dở bài này thì mình có hỏi một thằng em đồng nghiệp về khái niệm closure trong JavaScript. Nó hỏi mình ngay lại closure nghĩa là kết bài à. Vãi cả chưởng!
Event loop
Event loop là một khái niệm nâng cao trong JavaScript, vậy nên trước khi đi sâu vào vấn đề này thì ta hãy cùng làm nóng một chút chứ nhỉ. Hãy xem thử đoạn code sau và thử đoán xem các dòng tin dưới đây sẽ được in ra theo thứ tự nào.
Có lẽ ban đầu ta đều nghĩ đơn giản: “start” – “setTimeout” – “Promise” – “end” nhỉ, vì setTimeout() có giá trị thời gian đợi mặc định là 0 ms, còn đoạn promise thì được hoàn thành ngay lập tức. Okay, vậy ta hãy cùng bàn về event loop và quay lại đối chiếu với kết quả này nhé,
Event loop là một cơ chế trong JavaScript để xử lý các sự kiện và callback function một cách đồng bộ và không chặn luồng thực thi. Vì JavaScript hoạt động theo nguyên tắc đơn luồng, nơi code được xử lý từng phần liền mạch nhau, event loop cho phép JavaScript có thể xử lý các tác vụ bất đồng bộ, chẳng hạn như gọi một API hoặc tải một tài nguyên từ mạng mà không làm treo trình duyệt hoặc ứng dụng. Event loop dõi theo các hàm được gọi trong ngăn xếp (call stack) và các sự kiện trong hàng sự kiện (event queue). Khi chạy một đoạn code, trước tiên JavaScript chạy tất cả các hàm trong call stack, sau đó quay lại và loop qua event queue, liên tục đẩy các sự kiện trong event queue vào call stack cho đến khi nào tất cả các sự kiện đều đã được thực thi.
Chúng ta còn có thể chia nhỏ event queue ra thành hai nhánh: macrotask (tác vụ vĩ mô) và microtask (tác vụ vi mô). Macrotask là các tác vụ lớn, ví dụ như các sự kiện I/O, các hàm thời gian và render. Microtask thì là các tác vụ nhỏ hơn, thường được thực hiện ngay sau khi các tác vụ chính hoàn thành nhưng trước khi người dùng có thể tương tác với trang, ví dụ như promise hay mutation observer. Vì vậy, event loop trước tiên sẽ kiểm tra và thực hiện các microtask chưa được hoàn thành trước khi chuyển sự chú ý sang macrotask.
Bây giờ, hãy quay lại với câu hỏi ở đầu mục này nhé. Khi chạy lần đầu, event loop thực thi các hàm ở ngăn xếp chính, in ra “start” và “end”. Cho dù setTimeout mặc định thời gian là 0, và promise thì được resolved ngay lập tức, nhưng vì chúng không thuộc ngăn xếp chính, nên JavaScript đẩy xuống thực hiện sau cùng. Sau đó, event loop chạy qua microtask, nơi chứa promise, resolve và in ra câu lệnh bên trong. Cuối cùng, event loop một lần nữa kiểm tra macrotask và hoàn thành tác vụ setTimeout. Quy trình in ra của đoạn code trở thành:
Trông thì kinh hoàng thật đấy, nhưng event loop lại vô cùng hữu dụng để xử lý các sự kiện bất đồng bộ, vốn là một đặc tính cốt lõi của JavaScript, và khi bạn đã thành thục kĩ năng này rồi thì, mình tin là, chẳng có đoạn code JavaScript nào làm khó được bạn nữa đâu (thật ra nói xong câu này mình cũng hơi hối hận đấy… nhưng mà thôi cứ phải lạc quan tí chứ nhỉ).
Conclusion
Vậy là hôm nay chúng ta đã nói về ba tính chất khá lằng nhằng của JavaScript, theo từng cấp độ khó. Tất nhiên là, blog này chẳng thể liệt kê ra hết những sự rối rắm của JavaScript vì nếu thế mình phải viết thành sách mất. Ơ NHƯNG MÀ thật sự có một, không phải một cuốn sách, mà là một series sách cùng tiêu đề với bài viết này, chỉ để nói về những thứ củ chuối nhất của JavaScript. Đó, bạn thấy không, dù là một anh thực tập sinh chân ướt chân ráo code chiếc web đầu tiên, hay một vị sư phụ luyện code trên đỉnh Yên Tử mấy chục năm, thì JavaScript vẫn có thể khiến bạn hét vào màn hình, khóc không ngừng lo lắng về tương lai coding của mình.
Nhưng mà nói vậy thôi, chứ JavaScript quả thực là đẹp theo cách riêng của nó. Cũng như người yêu của bạn ý, cho dù cô ấy có chọc ghẹo bạn đến mức nào thì bạn đâu thể ngừng yêu cô ấy được đúng không nào. JavaScript đẹp, đẹp vì cái cách ta viết được nên những dòng code ngắn gọn mà tinh tế, kiến tạo nên những trang web chạy mượt mà, đẹp mỹ miều. Như thể là Picasso của ngôn ngữ lập trình vậy, một mớ lằng nhằng rắc rối lại trở thành một tạo phẩm nghệ thuật đầy tính năng. Vậy, hãy cùng nhau tận hưởng sự dở hơi của JavaScript và tạo nên những sản phẩm tuyệt vời nào!
AnCX